Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fixes #2149 and #2191] Project editing permissions #2201

Merged
merged 14 commits into from
May 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions akvo/rsr/models/project_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,6 @@ def img(self, value=''):
return value
img.allow_tags = True

def edit_window_has_expired(self):
"""Determine whether or not update timeout window has expired.
The timeout is controlled by settings.PROJECT_UPDATE_TIMEOUT and
defaults to 30 minutes.
"""
return (datetime.now() - self.created_at) > self.edit_timeout

@property
def expires_at(self):
return to_gmt(self.created_at + self.edit_timeout)

@property
def edit_timeout(self):
timeout_minutes = getattr(settings, 'PROJECT_UPDATE_TIMEOUT', 30)
return timedelta(minutes=timeout_minutes)

@property
def edit_time_remaining(self):
return self.edit_timeout - self.created_at

@property
def time_gmt(self):
return to_gmt(self.created_at)
Expand Down
152 changes: 152 additions & 0 deletions akvo/rsr/static/scripts-src/my-updates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Akvo RSR is covered by the GNU Affero General Public License.
// See more details in the license.txt file located at the root folder of the
// Akvo RSR module. For additional details on the GNU license please see
// < http://www.gnu.org/licenses/agpl.html >.

// DEFAULT VALUES
var defaultValues = JSON.parse(document.getElementById("default-values").innerHTML);

// CSRF TOKEN
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}

var csrftoken = getCookie('csrftoken');

// display error message in empty div below title
function setError(message) {
var errorNode = document.getElementById('projectUpdateError');
errorNode.innerHTML = message;
}

// display confirmation message before deleting update
function confirmDeleteUpdate(node) {
return function(e) {
e.preventDefault();

// check if delete button is enabled
if ((' ' + node.className + ' ').indexOf(' disabled ') < 0) {

var updateId = node.id.split('-')[1];
var confirmId = 'confirm-delete-' + updateId;

var confirmNode = document.getElementById(confirmId);
node.setAttribute('class', 'delete-update disabled');

var sureNode = document.createElement('span');
sureNode.innerHTML = defaultValues.sure_message;

var yesNode = document.createElement('a');
yesNode.setAttribute('style', 'color: green; margin-left: 5px;');
yesNode.onclick = confirmDelete(yesNode, updateId);
yesNode.innerHTML = defaultValues.yes;

var noNode = document.createElement('a');
noNode.setAttribute('style', 'color: red; margin-left: 5px;');
noNode.onclick = dismissConfirmationNo(sureNode, updateId);
noNode.innerHTML = defaultValues.no;

sureNode.appendChild(yesNode);
sureNode.appendChild(noNode);
confirmNode.appendChild(sureNode);

}

};
}

function dismissConfirmationNo(sureNode, updateId) {
return function(e) {
e.preventDefault();

dismissConfirmation(sureNode, updateId);
};
}

function dismissConfirmation(sureNode, updateId) {
var parentNode = sureNode.parentNode;
parentNode.removeChild(sureNode);

var updateNodeId = 'update-' + updateId;
deleteButtonNode = document.getElementById(updateNodeId);
deleteButtonNode.setAttribute('class', 'delete-update');
}

function confirmDelete(yesNode, updateId) {
return function(e) {
e.preventDefault();
var sureNode = yesNode.parentNode;
sureNode.innerHTML = defaultValues.delete_progress;

deleteUpdate(sureNode, updateId);
};
}

// make api call to delete specified update
function deleteUpdate(sureNode, updateId) {

var api_url = '/rest/v1/project_update/' + updateId + '/?format=json',
request = new XMLHttpRequest();

request.open('DELETE', api_url, true);
request.setRequestHeader("X-CSRFToken", csrftoken);
request.setRequestHeader("Content-type", "application/json");

request.onload = function() {
if (request.status >= 204 && request.status < 300) {
// Successfully deleted update, remove container.
removeUpdateContainer(updateId);
} else if (request.status == 404) {
// Update not found, most likely already deleted
dismissConfirmation(sureNode, updateId);
setError(defaultValues.error_delete);
} else {
// We reached our target server, but it returned an error
dismissConfirmation(sureNode, updateId);
setError(request.status + defaultValues.error_misc);
}
};

request.onerror = function() {
// There was a connection error of some sort
setError(defaultValues.error_connection);
return false;
};

request.send();
}

// remove update from screen once it has been delete
function removeUpdateContainer(updateId) {
var nodeId = 'update-' + updateId + '-container';
var removeNode = document.getElementById(nodeId);
var parentNode = removeNode.parentNode;
parentNode.removeChild(removeNode);
}

// add onlick to all delete buttons
function setDeleteUpdateOnClick() {
var deleteUpdateNodes = document.querySelectorAll('.delete-update');

if (deleteUpdateNodes !== null) {
for (var i = 0; i < deleteUpdateNodes.length; i++) {
deleteUpdateNodes[i].onclick = confirmDeleteUpdate(deleteUpdateNodes[i]);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
setDeleteUpdateOnClick();
});
26 changes: 26 additions & 0 deletions akvo/rsr/static/styles-src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,32 @@ nav.navbar-fixed-top {
position: relative;
right: auto; } }

div.updateListMenu ul {
margin-top: -10px; }
div.updateListMenu ul li {
display: inline-block; }
@media only screen and (max-width: 992px) {
div.updateListMenu ul li {
display: block; } }
div.updateListMenu ul li a:last-child {
margin-right: 0; }
div.updateListMenu ul li a.disabled {
background-color: #e6e6e6;
color: #cccccc;
pointer-events: initial;
cursor: not-allowed;
border: none;
-moz-border-radius: 5px;
-o-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px; }
div.updateListMenu ul li a.disabled:hover {
text-decoration: none; }

div.update-confirm-container {
margin-top: 5px;
font-size: 85%; }

.navbar-form {
padding-left: 0;
margin-bottom: 25px; }
Expand Down
36 changes: 36 additions & 0 deletions akvo/rsr/static/styles-src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,42 @@ nav.navbar-fixed-top {
}
}

div.updateListMenu {
ul {
margin-top: -10px;
li {
display: inline-block;
@include responsive (mediums-max-screens) {
display: block;
}
@include responsive (small-max-screens) {}
a {
&:last-child {
margin-right: 0;
}
&.disabled {
background-color: lighten($primary4, 30%);
color: lighten($primary4, 20%);
pointer-events: initial;
cursor: not-allowed;
border: none;
@include border-radius(5px);
&:hover {
text-decoration: none;
}
}
}
@include responsive (wide-screens) {}
> a {}
}
}
}

div.update-confirm-container {
margin-top: 5px;
font-size: 85%;
}

.navbar-form {
padding-left: 0;
margin-bottom: 25px;
Expand Down
5 changes: 3 additions & 2 deletions akvo/rsr/views/my_rsr.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ def project_editor(request, project_id):
except Project.DoesNotExist:
return Http404

if not request.user.has_perm('rsr.change_project', project):
if (not request.user.has_perm('rsr.change_project', project) or project.status == 'C') and not \
(request.user.is_superuser or request.user.is_admin):
raise PermissionDenied

# Custom fields
Expand Down Expand Up @@ -480,7 +481,7 @@ def my_results(request, project_id):
project = get_object_or_404(Project, pk=project_id)
user = request.user

if not user.has_perm('rsr.change_project', project):
if not user.has_perm('rsr.change_project', project) or project.status == 'C' or not project.is_published():
raise PermissionDenied

me_managers_group = Group.objects.get(name='M&E Managers')
Expand Down
7 changes: 4 additions & 3 deletions akvo/rsr/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,10 @@ def set_update(request, project_id, edit_mode=False, form_class=ProjectUpdateFor
request.error_message = u'You can only edit your own updates.'
raise PermissionDenied

if update.edit_window_has_expired():
request.error_message = u'You cannot edit this update anymore, the 30 minutes time limit has passed.'
raise PermissionDenied
# Prevent adding update if project is completed or unpublished
elif project.status == 'C' or not project.is_published():
request.error_message = u'Cannot add updates to completed or unpublished projects.'
raise PermissionDenied

if request.method == 'POST':
updateform = form_class(request.POST, request.FILES, instance=update)
Expand Down
6 changes: 6 additions & 0 deletions akvo/settings/40-pipeline.conf
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ PIPELINE_JS = {
),
'output_filename': 'scripts/my-results-select.min.js',
},
'my_updates': {
'source_filenames': (
'scripts-src/my-updates.js',
),
'output_filename': 'scripts/my-updates.min.js',
},
'more_partners': {
'source_filenames': (
'scripts-src/more-partners.js',
Expand Down
12 changes: 5 additions & 7 deletions akvo/templates/myrsr/my_projects.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
{% has_perm 'rsr.change_project' user project as can_edit_project %}
<tr class="{% if not project.is_published %}notPublished{% elif not project.is_public %}isPrivate{% endif %}">
<td class="media">
<a class="pull-left" href="{% if can_edit_project %}{% url 'project_editor' project.pk %}{% else %}{% url 'project-main' project.id %}{% endif %}">
<a class="pull-left" href="{% if can_edit_project and project.status != 'C' %}{% url 'project_editor' project.pk %}{% else %}{% url 'project-main' project.id %}{% endif %}">
{% img project 64 64 project.title %}
</a>
<div class="media-body">
<a href="{% if can_edit_project %}{% url 'project_editor' project.pk %}{% else %}{% url 'project-main' project.id %}{% endif %}">
<a href="{% if can_edit_project and project.status != 'C' %}{% url 'project_editor' project.pk %}{% else %}{% url 'project-main' project.id %}{% endif %}">
<h4 class="media-heading">{% if project.title %}{{ project.title }}{% else %}&lt;{% trans 'Untitled project' %}&gt;{% endif %}</h4>
</a>
<p class="small">{{ project.subtitle }}</p>
Expand All @@ -70,21 +70,19 @@ <h4 class="media-heading">{% if project.title %}{{ project.title }}{% else %}&lt
<ul class="nav">
<li><a href="{% url 'project-main' project.pk %}">{% trans 'View' %}</a></li>
{% if can_edit_project %}
{% if project.status != "C" or request.user.is_superuser %}
{% if project.status != 'C' or request.user.is_superuser or request.user.is_admin %}
<li><a href="{% url 'project_editor' project.pk %}">{% trans 'Edit' %}</a></li>
{% else %}
<li> <a class="addUpdate disabled" title="{% trans "Can't edit a completed project" %}"> {% trans 'Edit' %} </a></li>
{% endif %}
{% if project.is_impact_project and project.is_published %}
{% if project.is_impact_project and project.is_published and project.status != 'C' %}
<li><a href="{% url 'my_results' project.pk %}">{% trans 'Results data' %}</a></li>
{% else %}
<li><a class="addUpdate disabled" title="{% trans "Can't update an unpublished project" %}">{% trans 'Results data' %}</a></li>
<li><a class="addUpdate disabled" title="{% trans "Can't update an unpublished or completed project" %}">{% trans 'Results data' %}</a></li>
{% endif %}
{% endif %}
{% if project.is_published and project.is_public and project.status != "C" %}
<li><a href="{% url 'add-update' project.pk %}">{% trans 'Update' %}</a></li>
{% elif request.user.is_superuser %}
<li><a href="{% url 'add-update' project.pk %}">{% trans 'Update' %}</a></li>
{% elif project.status == "C" %}
<li> <a class="addUpdate disabled" title="{% trans "Can't update a completed project" %}"> {% trans "Update" %} </a></li>
{% else %}
Expand Down
Loading