diff --git a/pillar/web/projects/routes.py b/pillar/web/projects/routes.py index 1c5598de..aa6a71e6 100644 --- a/pillar/web/projects/routes.py +++ b/pillar/web/projects/routes.py @@ -1,3 +1,4 @@ +import datetime import json import logging import itertools @@ -7,6 +8,7 @@ from pillarsdk import Node from pillarsdk import Project from pillarsdk.exceptions import ResourceNotFound from pillarsdk.exceptions import ForbiddenAccess +import flask from flask import Blueprint from flask import render_template from flask import request @@ -78,6 +80,19 @@ def index(): 'sort': '-_created' }, api=api) + show_deleted_projects = request.args.get('deleted') is not None + if show_deleted_projects: + timeframe = utils.datetime_now() - datetime.timedelta(days=31) + projects_deleted = Project.all({ + 'where': {'user': current_user.objectid, + 'category': {'$ne': 'home'}, + '_deleted': True, + '_updated': {'$gt': timeframe}}, + 'sort': '-_created' + }, api=api) + else: + projects_deleted = {'_items': []} + projects_shared = Project.all({ 'where': {'user': {'$ne': current_user.objectid}, 'permissions.groups.group': {'$in': current_user.groups}, @@ -87,17 +102,17 @@ def index(): }, api=api) # Attach project images - for project in projects_user['_items']: - utils.attach_project_pictures(project, api) - - for project in projects_shared['_items']: - utils.attach_project_pictures(project, api) + for project_list in (projects_user, projects_deleted, projects_shared): + for project in project_list['_items']: + utils.attach_project_pictures(project, api) return render_template( 'projects/index_dashboard.html', gravatar=utils.gravatar(current_user.email, size=128), projects_user=projects_user['_items'], + projects_deleted=projects_deleted['_items'], projects_shared=projects_shared['_items'], + show_deleted_projects=show_deleted_projects, api=api) @@ -847,3 +862,37 @@ def edit_extension(project: Project, extension_name): return ext.project_settings(project, ext_pages=find_extension_pages()) + + +@blueprint.route('/undelete', methods=['POST']) +@login_required +def undelete(): + """Undelete a deleted project. + + Can only be done by the owner of the project or an admin. + """ + # This function takes an API-style approach, even though it's a web + # endpoint. Undeleting via a REST approach would mean GETting the + # deleted project, which now causes a 404 exception to bubble to the + # client. + from pillar.api.utils import mongo, remove_private_keys + from pillar.api.utils.authorization import check_permissions + + project_id = request.form.get('project_id') + if not project_id: + raise wz_exceptions.BadRequest('missing project ID') + + # Check that the user has PUT permissions on the project itself. + project = mongo.find_one_or_404('projects', project_id) + check_permissions('projects', project, 'PUT') + + pid = project['_id'] + log.info('Undeleting project %s on behalf of %s', pid, current_user.email) + r, _, _, status = current_app.put_internal('projects', remove_private_keys(project), _id=pid) + if status != 200: + log.warning('Error %d un-deleting project %s: %s', status, pid, r) + return 'Error un-deleting project', 500 + + resp = flask.Response('', status=204) + resp.location = flask.url_for('projects.view', project_url=project['url']) + return resp diff --git a/src/scripts/project-edit.js b/src/scripts/project-edit.js index 544affae..4d4fa498 100644 --- a/src/scripts/project-edit.js +++ b/src/scripts/project-edit.js @@ -182,11 +182,13 @@ $( document ).ready(function() { $('#item_delete').click(function(e){ e.preventDefault(); if (ProjectUtils.isProject()) { - $.post(urlProjectDelete, {project_id: ProjectUtils.projectId()}, - function (data) { - // Feedback logic - }).done(function () { - window.location.replace('/p/'); + $.post(urlProjectDelete, {project_id: ProjectUtils.projectId()}) + .done(function () { + // Redirect to the /p/ URL that shows deleted projects. + window.location.replace('/p/?deleted=1'); + }) + .fail(function(err) { + toastr.error(xhrErrorResponseMessage(err), 'Project deletion failed'); }); } else { $.post(urlNodeDelete, {node_id: ProjectUtils.nodeId()}, diff --git a/src/styles/_project-dashboard.sass b/src/styles/_project-dashboard.sass index 0df2bd98..c07c0dca 100644 --- a/src/styles/_project-dashboard.sass +++ b/src/styles/_project-dashboard.sass @@ -245,7 +245,13 @@ box-shadow: 1px 1px 0 rgba(black, .1) display: flex margin: 10px 15px - padding: 10px 0 + padding: 10px 10px + + &.deleted + background-color: $color-background-light + + .title + color: $color-text-dark-hint !important &:hover cursor: pointer @@ -259,9 +265,9 @@ .projects__list-details a.title color: $color-primary - a.projects__list-thumbnail + .projects__list-thumbnail position: relative - margin: 0 15px + margin-right: 15px width: 50px height: 50px border-radius: 3px @@ -280,7 +286,7 @@ display: flex flex-direction: column - a.title + .title font-size: 1.2em padding-bottom: 2px color: $color-text-dark-primary diff --git a/src/templates/projects/index_dashboard.pug b/src/templates/projects/index_dashboard.pug index d51d7f4b..c721a4f5 100644 --- a/src/templates/projects/index_dashboard.pug +++ b/src/templates/projects/index_dashboard.pug @@ -18,6 +18,25 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba | {{current_user.full_name}} | {% endblock %} +| {% block css %} +| {{ super() }} +style. + .deleted-projects-toggle { + z-index: 10; + position: absolute; + right: 0; + font-size: 20px; + padding: 3px; + text-shadow: 0 0 2px white; + } + .deleted-projects-toggle .show-deleted { + color: #aaa; + } + .deleted-projects-toggle .hide-deleted { + color: #bbb; + } +| {% endblock %} + | {% block body %} .dashboard-container section.dashboard-main @@ -54,7 +73,36 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba | {% endif %} nav.nav-tabs__tab.active#own_projects + .deleted-projects-toggle + | {% if show_deleted_projects %} + a.hide-deleted(href="{{ request.base_url }}", title='Hide deleted projects') + i.pi-trash + | {% else %} + a.show-deleted(href="{{ request.base_url }}?deleted=1", title='Show deleted projects') + i.pi-trash + | {% endif %} + ul.projects__list + | {% for project in projects_deleted %} + li.projects__list-item.deleted + span.projects__list-thumbnail + | {% if project.picture_square %} + img(src="{{ project.picture_square.thumbnail('s', api=api) }}") + | {% else %} + i.pi-blender-cloud + | {% endif %} + .projects__list-details + span.title {{ project.name }} + ul.meta + li.status.deleted Deleted + li.edit + a(href="javascript:undelete_project('{{ project._id }}')") Restore project + | {% else %} + | {% if show_deleted_projects %} + li.projects__list-item.deleted You have no recenly deleted projects. Deleted projects can be restored within a month after deletion. + | {% endif %} + | {% endfor %} + | {% for project in projects_user %} li.projects__list-item( data-url="{{ url_for('projects.view', project_url=project.url) }}") @@ -105,7 +153,7 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba | {% endif %} | {% endfor %} - section.nav-tabs__tab#shared + section.nav-tabs__tab#shared(style='display: none') ul.projects__list | {% if projects_shared %} | {% for project in projects_shared %} @@ -278,4 +326,15 @@ script. hopToTop(); // Display jump to top button }); + + function undelete_project(project_id) { + console.log('undeleting project', project_id); + $.post('{{ url_for('projects.undelete') }}', {project_id: project_id}) + .done(function(data, textStatus, jqXHR) { + location.href = jqXHR.getResponseHeader('Location'); + }) + .fail(function(err) { + toastr.error(xhrErrorResponseMessage(err), 'Undeletion failed'); + }) + } | {% endblock %} diff --git a/src/templates/projects/view.pug b/src/templates/projects/view.pug index 540a8efb..66a12e41 100644 --- a/src/templates/projects/view.pug +++ b/src/templates/projects/view.pug @@ -222,7 +222,7 @@ link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v li.button-delete a#item_delete( href="javascript:void(0);", - title="Delete (Warning: no undo)", + title="Can be undone within a month", data-toggle="tooltip", data-placement="left") i.pi-trash