Allow project undeletion, fixes T51244
Projects can be undeleted within a month of deletion.
This commit is contained in:
parent
46612a9f68
commit
8f73dab36e
@ -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
|
||||||
|
@ -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()},
|
||||||
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user