Skip to content
1 change: 1 addition & 0 deletions admin/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@
re_path(r'^(?P<guid>[a-z0-9]+)/update_moderation_state/$', views.NodeUpdateModerationStateView.as_view(), name='node-update-mod-state'),
re_path(r'^(?P<guid>[a-z0-9]+)/resync_datacite/$', views.NodeResyncDataCiteView.as_view(), name='resync-datacite'),
re_path(r'^(?P<guid>[a-z0-9]+)/revert/$', views.NodeRevertToDraft.as_view(), name='revert-to-draft'),
re_path(r'^(?P<guid>[a-z0-9]+)/update_permissions/$', views.NodeUpdatePermissionsView.as_view(), name='update-permissions'),
]
75 changes: 74 additions & 1 deletion admin/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
REINDEX_SHARE,
REINDEX_ELASTIC,
)
from osf.utils.permissions import ADMIN
from osf.utils.permissions import ADMIN, API_CONTRIBUTOR_PERMISSIONS

from scripts.approve_registrations import approve_past_pendings

Expand Down Expand Up @@ -109,7 +109,11 @@ def get_context_data(self, **kwargs):
'SPAM_STATUS': SpamStatus,
'STORAGE_LIMITS': settings.StorageLimits,
'node': node,
# to edit contributors we should have guid as django prohibits _id usage as it starts with an underscore
'annotated_contributors': node.contributor_set.prefetch_related('user__guids').annotate(guid=F('user__guids___id')),
'children': children,
'permissions': API_CONTRIBUTOR_PERMISSIONS,
'has_update_permission': node.is_admin_contributor(self.request.user)
})

return context
Expand Down Expand Up @@ -195,6 +199,75 @@ def add_contributor_removed_log(self, node, user):
).save()


class NodeUpdatePermissionsView(NodeMixin, View):
permission_required = ('osf.view_node', 'osf.change_node')
raise_exception = True
redirect_view = NodeRemoveContributorView

def post(self, request, *args, **kwargs):
data = dict(request.POST)
contributor_id_to_remove = data.get('remove-user')
resource = self.get_object()

if contributor_id_to_remove:
contributor_id = contributor_id_to_remove[0]
# html renders form into form incorrectly,
# so this view handles contributors deletion and permissions update
return self.redirect_view(
request=request,
kwargs={'guid': resource.guid, 'user_id': contributor_id}
).post(request, user_id=contributor_id)

new_emails_to_add = data.get('new-emails', [])
new_permissions_to_add = data.get('new-permissions', [])

new_permission_indexes_to_remove = []
for email, permission in zip(new_emails_to_add, new_permissions_to_add):
contributor_user = OSFUser.objects.filter(emails__address=email.lower()).first()
if not contributor_user:
new_permission_indexes_to_remove.append(new_emails_to_add.index(email))
messages.error(self.request, f'Email {email} is not registered in OSF.')
continue
elif resource.is_contributor(contributor_user):
new_permission_indexes_to_remove.append(new_emails_to_add.index(email))
messages.error(self.request, f'User with email {email} is already a contributor.')
continue

resource.add_contributor_registered_or_not(
auth=request,
user_id=contributor_user._id,
permissions=permission,
save=True
)
messages.success(self.request, f'User with email {email} was successfully added.')

# should remove permissions of invalid emails because
# admin can make all existing contributors non admins
# and enter an invalid email with the only admin permission
for permission_index in new_permission_indexes_to_remove:
new_permissions_to_add.pop(permission_index)

updated_permissions = data.get('updated-permissions', [])
all_permissions = updated_permissions + new_permissions_to_add
has_admin = list(filter(lambda permission: ADMIN in permission, all_permissions))
if not has_admin:
messages.error(self.request, 'Must be at least one admin on this node.')
return redirect(self.get_success_url())

for contributor_permission in updated_permissions:
guid, permission = contributor_permission.split('-')
user = OSFUser.load(guid)
resource.update_contributor(
user,
permission,
resource.get_visible(user),
request,
save=True
)

return redirect(self.get_success_url())


class NodeDeleteView(NodeMixin, View):
""" Allows authorized users to mark nodes as deleted.
"""
Expand Down
1 change: 1 addition & 0 deletions admin/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@
re_path(r'^(?P<guid>\w+)/resync_crossref/$', views.PreprintResyncCrossRefView.as_view(), name='resync-crossref'),
re_path(r'^(?P<guid>\w+)/make_published/$', views.PreprintMakePublishedView.as_view(), name='make-published'),
re_path(r'^(?P<guid>\w+)/unwithdraw/$', views.PreprintUnwithdrawView.as_view(), name='unwithdraw'),
re_path(r'^(?P<guid>\w+)/update_permissions/$', views.PreprintUpdatePermissionsView.as_view(), name='update-permissions'),
]
13 changes: 12 additions & 1 deletion admin/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from admin.base.views import GuidView
from admin.base.forms import GuidForm
from admin.nodes.views import NodeRemoveContributorView
from admin.nodes.views import NodeRemoveContributorView, NodeUpdatePermissionsView
from admin.preprints.forms import ChangeProviderForm, MachineStateForm

from api.share.utils import update_share
Expand Down Expand Up @@ -48,6 +48,7 @@
UNFLAG_SPAM,
)
from osf.utils.workflows import DefaultStates
from osf.utils.permissions import API_CONTRIBUTOR_PERMISSIONS
from website import search
from website.files.utils import copy_files
from website.preprints.tasks import on_preprint_updated
Expand Down Expand Up @@ -75,9 +76,13 @@ def get_context_data(self, **kwargs):
preprint = self.get_object()
return super().get_context_data(**{
'preprint': preprint,
# to edit contributors we should have guid as django prohibits _id usage as it starts with an underscore
'annotated_contributors': preprint.contributor_set.prefetch_related('user__guids').annotate(guid=F('user__guids___id')),
'SPAM_STATUS': SpamStatus,
'change_provider_form': ChangeProviderForm(instance=preprint),
'change_machine_state_form': MachineStateForm(instance=preprint),
'permissions': API_CONTRIBUTOR_PERMISSIONS,
'has_update_permission': preprint.is_admin_contributor(self.request.user)
}, **kwargs)


Expand Down Expand Up @@ -272,6 +277,12 @@ def add_contributor_removed_log(self, preprint, user):
).save()


class PreprintUpdatePermissionsView(PreprintMixin, NodeUpdatePermissionsView):
permission_required = ('osf.view_preprint', 'osf.change_preprint')
raise_exception = True
redirect_view = PreprintRemoveContributorView


class PreprintDeleteView(PreprintMixin, View):
""" Allows authorized users to mark preprints as deleted.
"""
Expand Down
34 changes: 2 additions & 32 deletions admin/templates/nodes/contributors.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
<tr>
<td>Email</td>
<td>Name</td>
<td>Permissions</td>
<td>Actions</td>
{% if perms.osf.change_node %}
<td></td>
{% endif %}
<td>Permission</td>
</tr>
</thead>
<tbody>
Expand All @@ -26,37 +22,11 @@
</td>
<td>{{ user.fullname }}</td>
<td>{% get_permissions user node %}</td>
{% if perms.osf.change_node %}
<td>
<a data-toggle="modal" data-target="#{{ user.id }}Modal" class="btn btn-danger">Remove</a>
<div class="modal" id="{{ user.id }}Modal">
<div class="modal-dialog">
<div class="modal-content">
<form class="well" method="post" action="{% url 'nodes:remove-user' guid=node.guid user_id=user.id %}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Removing contributor: {{ user.username }}</h3>
</div>
<div class="modal-body">
User will be removed. Currently only an admin on this node type will be able to add them back.
{% csrf_token %}
</div>
<div class="modal-footer">
<input class="btn btn-danger" type="submit" value="Confirm" />
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% include 'nodes/edit_contributors.html' with contributors=annotated_contributors resource=node %}
</div>
</td>
</tr>
137 changes: 137 additions & 0 deletions admin/templates/nodes/edit_contributors.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<a data-toggle="modal" data-target="#editContributors" class="btn btn-info">
Edit Contributors
</a>
<div class="modal" id="editContributors" style="width: 100%;">
<div class="modal-dialog" style="width: 60vw; margin: 50px 20vw;">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" onclick="resetModal()" data-dismiss="modal">x</button>
<h3>Edit Contributors</h3>
</div>
{% if resource.type == 'osf.node' or resource.type == 'osf.registration' %}
<form action="{% url 'nodes:update-permissions' guid=resource.guid %}" method="post" id="contributors-form">
{% else %}
<form action="{% url 'preprints:update-permissions' guid=resource.guid %}" method="post" id="contributors-form">
{% endif %}
{% csrf_token %}
<table class="table table-bordered table-hover" id="contributors-table">
<thead>
<tr>
<td>Email</td>
<td>Name</td>
<td>Permission</td>
<td></td>
</tr>
</thead>
<tbody>
{% for contributor in contributors %}
<tr>
<td><a href="{% url 'users:user' guid=contributor.guid %}">{{ contributor.user.email }}</a></td>
<td>{{ contributor.user.fullname }}</td>
<td style="padding: 10px; margin: 0; text-align: center; align-items: center; display: grid;">
<select name="updated-permissions">
{% for permission in permissions %}
{% if contributor.permission == permission %}
<option value="{{ contributor.guid }}-{{ permission }}" selected>{{ permission }}</option>
{% else %}
<option value="{{ contributor.guid }}-{{ permission }}">{{ permission }}</option>
{% endif %}
{% endfor %}
</select>
</td>
{% if has_update_permission %}
<td style="text-align: center;">
<a data-toggle="modal" data-target="#{{ contributor.user.id }}Modal" class="btn btn-danger">Remove</a>
<div class="modal" id="{{ contributor.user.id }}Modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Removing contributor: {{ contributor.user.username }}</h3>
</div>
<div class="modal-body">
User will be removed. Currently only an admin on this node type will be able to add them back.
</div>
<div class="modal-footer">
<button type="submit" onclick="resetModal()" name="remove-user" value="{{ contributor.user.id }}" class="btn btn-danger">Delete</button>
<button type="button" onclick="resetModal()" name="{{ contributor.user.id }}" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</div>
</div>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<div class="modal-footer" style="display: grid; grid-template: 'left right'; grid-template-columns: 1fr 5fr;">
<div class="left-button" style="grid-area: left; display: flex">
<input name="add-contributor" class="btn btn-success" onclick="addRow()" type="button" value="Add Contributor" />
</div>
<div class="right-buttons" style="grid-area: right">
<button type="button" class="btn btn-default" onclick="resetModal()" data-dismiss="modal">
Cancel
</button>
<input class="btn btn-success" type="submit" value="Save" />
</div>
</div>
</form>
</div>
</div>
</div>

<script>
let new_rows_counter = 0;

function addRow() {
const tableBody = document.getElementById("contributors-table").getElementsByTagName('tbody')[0];
const newRow = document.createElement("tr");
newRow.id = `new-contributor-row-${new_rows_counter}`;
new_rows_counter += 1;
const cell1 = document.createElement("td");
const cell2 = document.createElement("td");
const cell3 = document.createElement("td");
const cell4 = document.createElement("td");

cell1.innerHTML = '<input type="email" required name="new-emails" placeholder="Add email">'
cell3.innerHTML = `
<select name="new-permissions">
{% for permission in permissions %}
{% if contributor.permission == permission %}
<option value="{{ permission }}" selected>{{ permission }}</option>
{% else %}
<option value="{{ permission }}">{{ permission }}</option>
{% endif %}
{% endfor %}
</select>
`;
cell3.style="padding: 10px; margin: 0; text-align: center; align-items: center; display: grid;"
cell4.innerHTML = `<button type="button" class="btn btn-danger" onclick="removeRow('${newRow.id}')" data-dismiss="modal">Remove row</button>`;
cell4.style = 'text-align: center;'

newRow.appendChild(cell1);
newRow.appendChild(cell2);
newRow.appendChild(cell3);
newRow.appendChild(cell4)

tableBody.appendChild(newRow);
}

function removeRow(id) {
try {
document.getElementById(id).remove();
} catch {};
}

function resetModal() {
const table = document.getElementById("contributors-form");
table.reset();
for (let i = 0; i < new_rows_counter; i++) {
removeRow(`new-contributor-row-${i}`);
}
new_rows_counter = 0;
}
</script>
Loading
Loading