Allow project undeletion, fixes T51244

Projects can be undeleted within a month of deletion.
This commit is contained in:
Sybren A. Stüvel 2017-12-22 16:27:05 +01:00
parent 46612a9f68
commit 8f73dab36e
5 changed files with 132 additions and 16 deletions

View File

@ -1,3 +1,4 @@
import datetime
import json import json
import logging import logging
import itertools import itertools
@ -7,6 +8,7 @@ from pillarsdk import Node
from pillarsdk import Project from pillarsdk import Project
from pillarsdk.exceptions import ResourceNotFound from pillarsdk.exceptions import ResourceNotFound
from pillarsdk.exceptions import ForbiddenAccess from pillarsdk.exceptions import ForbiddenAccess
import flask
from flask import Blueprint from flask import Blueprint
from flask import render_template from flask import render_template
from flask import request from flask import request
@ -78,6 +80,19 @@ def index():
'sort': '-_created' 'sort': '-_created'
}, api=api) }, 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({ projects_shared = Project.all({
'where': {'user': {'$ne': current_user.objectid}, 'where': {'user': {'$ne': current_user.objectid},
'permissions.groups.group': {'$in': current_user.groups}, 'permissions.groups.group': {'$in': current_user.groups},
@ -87,17 +102,17 @@ def index():
}, api=api) }, api=api)
# Attach project images # Attach project images
for project in projects_user['_items']: for project_list in (projects_user, projects_deleted, projects_shared):
utils.attach_project_pictures(project, api) for project in project_list['_items']:
utils.attach_project_pictures(project, api)
for project in projects_shared['_items']:
utils.attach_project_pictures(project, api)
return render_template( return render_template(
'projects/index_dashboard.html', 'projects/index_dashboard.html',
gravatar=utils.gravatar(current_user.email, size=128), gravatar=utils.gravatar(current_user.email, size=128),
projects_user=projects_user['_items'], projects_user=projects_user['_items'],
projects_deleted=projects_deleted['_items'],
projects_shared=projects_shared['_items'], projects_shared=projects_shared['_items'],
show_deleted_projects=show_deleted_projects,
api=api) api=api)
@ -847,3 +862,37 @@ def edit_extension(project: Project, extension_name):
return ext.project_settings(project, return ext.project_settings(project,
ext_pages=find_extension_pages()) 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

View File

@ -182,11 +182,13 @@ $( document ).ready(function() {
$('#item_delete').click(function(e){ $('#item_delete').click(function(e){
e.preventDefault(); e.preventDefault();
if (ProjectUtils.isProject()) { if (ProjectUtils.isProject()) {
$.post(urlProjectDelete, {project_id: ProjectUtils.projectId()}, $.post(urlProjectDelete, {project_id: ProjectUtils.projectId()})
function (data) { .done(function () {
// Feedback logic // Redirect to the /p/ URL that shows deleted projects.
}).done(function () { window.location.replace('/p/?deleted=1');
window.location.replace('/p/'); })
.fail(function(err) {
toastr.error(xhrErrorResponseMessage(err), 'Project deletion failed');
}); });
} else { } else {
$.post(urlNodeDelete, {node_id: ProjectUtils.nodeId()}, $.post(urlNodeDelete, {node_id: ProjectUtils.nodeId()},

View File

@ -245,7 +245,13 @@
box-shadow: 1px 1px 0 rgba(black, .1) box-shadow: 1px 1px 0 rgba(black, .1)
display: flex display: flex
margin: 10px 15px margin: 10px 15px
padding: 10px 0 padding: 10px 10px
&.deleted
background-color: $color-background-light
.title
color: $color-text-dark-hint !important
&:hover &:hover
cursor: pointer cursor: pointer
@ -259,9 +265,9 @@
.projects__list-details a.title .projects__list-details a.title
color: $color-primary color: $color-primary
a.projects__list-thumbnail .projects__list-thumbnail
position: relative position: relative
margin: 0 15px margin-right: 15px
width: 50px width: 50px
height: 50px height: 50px
border-radius: 3px border-radius: 3px
@ -280,7 +286,7 @@
display: flex display: flex
flex-direction: column flex-direction: column
a.title .title
font-size: 1.2em font-size: 1.2em
padding-bottom: 2px padding-bottom: 2px
color: $color-text-dark-primary color: $color-text-dark-primary

View File

@ -18,6 +18,25 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba
| {{current_user.full_name}} | {{current_user.full_name}}
| {% endblock %} | {% 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 %} | {% block body %}
.dashboard-container .dashboard-container
section.dashboard-main section.dashboard-main
@ -54,7 +73,36 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba
| {% endif %} | {% endif %}
nav.nav-tabs__tab.active#own_projects 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 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 %} | {% for project in projects_user %}
li.projects__list-item( li.projects__list-item(
data-url="{{ url_for('projects.view', project_url=project.url) }}") 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 %} | {% endif %}
| {% endfor %} | {% endfor %}
section.nav-tabs__tab#shared section.nav-tabs__tab#shared(style='display: none')
ul.projects__list ul.projects__list
| {% if projects_shared %} | {% if projects_shared %}
| {% for project in projects_shared %} | {% for project in projects_shared %}
@ -278,4 +326,15 @@ script.
hopToTop(); // Display jump to top button 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 %} | {% endblock %}

View File

@ -222,7 +222,7 @@ link(href="{{ url_for('static_pillar', filename='assets/css/project-main.css', v
li.button-delete li.button-delete
a#item_delete( a#item_delete(
href="javascript:void(0);", href="javascript:void(0);",
title="Delete (Warning: no undo)", title="Can be undone within a month",
data-toggle="tooltip", data-toggle="tooltip",
data-placement="left") data-placement="left")
i.pi-trash i.pi-trash