diff --git a/admin/nodes/forms.py b/admin/nodes/forms.py index 553c465da4a..ea2383babe4 100644 --- a/admin/nodes/forms.py +++ b/admin/nodes/forms.py @@ -1,6 +1,10 @@ from django import forms +class AddSystemTagForm(forms.Form): + system_tag_to_add = forms.CharField(label='system_tag_to_add', min_length=1, max_length=1024, required=True) + + class RegistrationDateForm(forms.Form): registered_date = forms.DateTimeField( widget=forms.DateTimeInput(attrs={'class': 'form-control'}), diff --git a/admin/nodes/templatetags/node_extras.py b/admin/nodes/templatetags/node_extras.py index 4fb9606f22e..be53d2f7506 100644 --- a/admin/nodes/templatetags/node_extras.py +++ b/admin/nodes/templatetags/node_extras.py @@ -88,3 +88,8 @@ def get_spam_status(resource): return mark_safe('Spam') elif resource.spam_status == SpamStatus.HAM: return mark_safe('Ham') + + +@register.filter +def get_class_name(resource): + return resource.__class__.__name__.lower() diff --git a/admin/nodes/urls.py b/admin/nodes/urls.py index ae7a30a009e..ac31ec0dc9b 100644 --- a/admin/nodes/urls.py +++ b/admin/nodes/urls.py @@ -47,5 +47,7 @@ re_path(r'^(?P[a-z0-9]+)/update_moderation_state/$', views.NodeUpdateModerationStateView.as_view(), name='node-update-mod-state'), re_path(r'^(?P[a-z0-9]+)/resync_datacite/$', views.NodeResyncDataCiteView.as_view(), name='resync-datacite'), re_path(r'^(?P[a-z0-9]+)/revert/$', views.NodeRevertToDraft.as_view(), name='revert-to-draft'), + re_path(r'^(?P[a-z0-9]+)/system_tags/add/$', views.NodeAddSystemTag.as_view(), name='add-system-tag'), + re_path(r'^(?P[a-z0-9]+)/system_tags/(?P[a-z0-9]+)/remove/$', views.NodeRemoveSystemTag.as_view(), name='remove-system-tag'), re_path(r'^(?P[a-z0-9]+)/update_permissions/$', views.NodeUpdatePermissionsView.as_view(), name='update-permissions'), ] diff --git a/admin/nodes/views.py b/admin/nodes/views.py index 3750247bdc6..690faffb377 100644 --- a/admin/nodes/views.py +++ b/admin/nodes/views.py @@ -22,7 +22,7 @@ from admin.base.views import GuidView from admin.base.forms import GuidForm from admin.notifications.views import detect_duplicate_notifications, delete_selected_notifications -from admin.nodes.forms import RegistrationDateForm +from admin.nodes.forms import AddSystemTagForm, RegistrationDateForm from api.share.utils import update_share from api.caching.tasks import update_storage_usage_cache @@ -921,3 +921,32 @@ def post(self, request, *args, **kwargs): registration = self.get_object() registration.to_draft() return redirect(self.get_success_url()) + + +class NodeAddSystemTag(NodeMixin, FormView): + """ Allows authorized users to add system tags to a node. + """ + permission_required = 'osf.change_node' + raise_exception = True + form_class = AddSystemTagForm + + def form_valid(self, form): + resource = self.get_object() + system_tag_to_add = form.cleaned_data['system_tag_to_add'] + resource.add_system_tag(system_tag_to_add) + resource.save() + + return super().form_valid(form) + + +class NodeRemoveSystemTag(NodeMixin, View): + """ Allows authorized users to remove system tags from a node. + """ + permission_required = 'osf.change_node' + raise_exception = True + + def post(self, request, *args, **kwargs): + resource = self.get_object() + tag = resource.system_tags_objects.get(id=kwargs['tag_id']) + resource.remove_tag(tag.name, auth=request.user) + return redirect(self.get_success_url()) diff --git a/admin/preprints/urls.py b/admin/preprints/urls.py index 8f27bf0892f..d6eabdd870c 100644 --- a/admin/preprints/urls.py +++ b/admin/preprints/urls.py @@ -30,5 +30,7 @@ re_path(r'^(?P\w+)/resync_crossref/$', views.PreprintResyncCrossRefView.as_view(), name='resync-crossref'), re_path(r'^(?P\w+)/make_published/$', views.PreprintMakePublishedView.as_view(), name='make-published'), re_path(r'^(?P\w+)/unwithdraw/$', views.PreprintUnwithdrawView.as_view(), name='unwithdraw'), + re_path(r'^(?P\w+)/system_tags/add/$', views.PreprintAddSystemTag.as_view(), name='add-system-tag'), + re_path(r'^(?P\w+)/system_tags/(?P[a-z0-9]+)/remove/$', views.PreprintRemoveSystemTag.as_view(), name='remove-system-tag'), re_path(r'^(?P\w+)/update_permissions/$', views.PreprintUpdatePermissionsView.as_view(), name='update-permissions'), ] diff --git a/admin/preprints/views.py b/admin/preprints/views.py index 6d26108dba2..3868f3bce88 100644 --- a/admin/preprints/views.py +++ b/admin/preprints/views.py @@ -15,7 +15,7 @@ from admin.base.views import GuidView from admin.base.forms import GuidForm -from admin.nodes.views import NodeRemoveContributorView, NodeUpdatePermissionsView +from admin.nodes.views import NodeRemoveContributorView, NodeAddSystemTag, NodeRemoveSystemTag, NodeUpdatePermissionsView from admin.preprints.forms import ChangeProviderForm, MachineStateForm from admin.base.utils import osf_staff_check @@ -743,3 +743,15 @@ def post(self, request, *args, **kwargs): preprint.save() return redirect(self.get_success_url()) + + +class PreprintAddSystemTag(PreprintMixin, NodeAddSystemTag): + """ Allows authorized users to add system tags to a preprint. + """ + permission_required = 'osf.change_preprint' + + +class PreprintRemoveSystemTag(PreprintMixin, NodeRemoveSystemTag): + """ Allows authorized users to remove system tags from a preprint. + """ + permission_required = 'osf.change_preprint' diff --git a/admin/templates/nodes/add_system_tags.html b/admin/templates/nodes/add_system_tags.html new file mode 100644 index 00000000000..e8deb10a9c1 --- /dev/null +++ b/admin/templates/nodes/add_system_tags.html @@ -0,0 +1,59 @@ +{% load node_extras %} + + System tags + + + {% for system_tag in resource.system_tags_objects %} + {% if resource|get_class_name == 'node' or resource|get_class_name == 'registration' %} + + {% elif resource|get_class_name == 'preprint' %} + + {% else %} + + {% endif %} + + {% csrf_token %} + + + + {% endfor %} + + + + Add system tag + + + + + {% if resource|get_class_name == 'node' or resource|get_class_name == 'registration' %} + + {% elif resource|get_class_name == 'preprint' %} + + {% else %} + + {% endif%} + + x + Add a system tag to this resource + + + {% csrf_token %} + + + + + + + + + + diff --git a/admin/templates/nodes/node.html b/admin/templates/nodes/node.html index 177b29ddaf6..1c791ce2b76 100644 --- a/admin/templates/nodes/node.html +++ b/admin/templates/nodes/node.html @@ -119,6 +119,7 @@ {{ node.type|cut:'osf.'|title }}: {{ node.title }} Preprint: {{ preprint.title }} + {% include "nodes/add_system_tags.html" with resource=preprint %} {% include "preprints/contributors.html" with preprint=preprint %} {% include "nodes/spam_status.html" with resource=preprint %} {% include "preprints/withdraw_request.html" with preprint=preprint %} diff --git a/admin/templates/users/add_system_tags.html b/admin/templates/users/add_system_tags.html deleted file mode 100644 index db685cb7e34..00000000000 --- a/admin/templates/users/add_system_tags.html +++ /dev/null @@ -1,35 +0,0 @@ - - System tags - - {% for system_tag in user.system_tags %} - {{ system_tag }}{% if not forloop.last %}, {% endif %} - {% endfor %} - - add system tag - - - - - - - x - Add a system tag to this user - - - {% csrf_token %} - - - - - - - - - - diff --git a/admin/templates/users/user.html b/admin/templates/users/user.html index f8afa6bdf59..921900d01c8 100644 --- a/admin/templates/users/user.html +++ b/admin/templates/users/user.html @@ -186,7 +186,7 @@ User: {{ user.username }} ({{user. {{ user.is_staff }} - {% include "users/add_system_tags.html" with user=user %} + {% include "nodes/add_system_tags.html" with resource=user %} {% include "nodes/spam_status.html" with resource=user %} Preprints diff --git a/admin/users/forms.py b/admin/users/forms.py index e64a648ff77..01327a4eb73 100644 --- a/admin/users/forms.py +++ b/admin/users/forms.py @@ -19,7 +19,3 @@ class UserSearchForm(forms.Form): class MergeUserForm(forms.Form): user_guid_to_be_merged = forms.CharField(label='user_guid_to_be_merged', min_length=5, max_length=5, required=True) # TODO: Move max to 6 when needed - - -class AddSystemTagForm(forms.Form): - system_tag_to_add = forms.CharField(label='system_tag_to_add', min_length=1, max_length=1024, required=True) diff --git a/admin/users/urls.py b/admin/users/urls.py index 7f5d55ddb9b..309ba6bcd35 100644 --- a/admin/users/urls.py +++ b/admin/users/urls.py @@ -21,6 +21,7 @@ re_path(r'^(?P[a-z0-9]+)/get_claim_urls/$', views.GetUserClaimLinks.as_view(), name='get-claim-urls'), re_path(r'^(?P[a-z0-9]+)/two-factor/disable/$', views.User2FactorDeleteView.as_view(), name='remove2factor'), re_path(r'^(?P[a-z0-9]+)/system_tags/add/$', views.UserAddSystemTag.as_view(), name='add-system-tag'), + re_path(r'^(?P[a-z0-9]+)/system_tags/(?P[a-z0-9]+)/remove/$', views.UserRemoveSystemTag.as_view(), name='remove-system-tag'), re_path(r'^(?P[a-z0-9]+)/get_confirmation/$', views.GetUserConfirmationLink.as_view(), name='get-confirmation'), re_path(r'^(?P[a-z0-9]+)/get_reset_password/$', views.GetPasswordResetLink.as_view(), name='get-reset-password'), re_path(r'^(?P[a-z0-9]+)/reindex_elastic_user/$', views.UserReindexElastic.as_view(), diff --git a/admin/users/views.py b/admin/users/views.py index 9072c66a989..bc4e550f88f 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -42,9 +42,9 @@ from admin.users.forms import ( EmailResetForm, UserSearchForm, - MergeUserForm, - AddSystemTagForm + MergeUserForm ) +from admin.nodes.views import NodeAddSystemTag, NodeRemoveSystemTag from admin.base.views import GuidView from api.users.services import send_password_reset_email from website.settings import DOMAIN @@ -383,20 +383,16 @@ def post(self, request, *args, **kwargs): return redirect(self.get_success_url()) -class UserAddSystemTag(UserMixin, FormView): +class UserAddSystemTag(UserMixin, NodeAddSystemTag): """ Allows authorized users to add system tags to a user. """ permission_required = 'osf.change_osfuser' - raise_exception = True - form_class = AddSystemTagForm - def form_valid(self, form): - user = self.get_object() - system_tag_to_add = form.cleaned_data['system_tag_to_add'] - user.add_system_tag(system_tag_to_add) - user.save() - return super().form_valid(form) +class UserRemoveSystemTag(UserMixin, NodeRemoveSystemTag): + """ Allows authorized users to remove system tags from a user. + """ + permission_required = 'osf.change_osfuser' class UserMergeAccounts(UserMixin, FormView): diff --git a/osf/models/node.py b/osf/models/node.py index 205a7e7df4a..2345708934e 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -991,13 +991,17 @@ def all_tags(self): # Tag's default manager only returns non-system tags, so we can't use self.tags return Tag.all_tags.filter(abstractnode_tagged=self) + @property + def system_tags_objects(self): + return self.all_tags.filter(system=True) + @property def system_tags(self): """The system tags associated with this node. This currently returns a list of string names for the tags, for compatibility with v1. Eventually, we can just return the QuerySet. """ - return self.all_tags.filter(system=True).values_list('name', flat=True) + return self.system_tags_objects.values_list('name', flat=True) # Override Taggable def add_tag_log(self, tag, auth): @@ -1019,10 +1023,15 @@ def on_tag_added(self, tag): def remove_tag(self, tag, auth, save=True): if not tag: raise InvalidTagError - elif not self.tags.filter(name=tag).exists(): + + tag_obj = self.tags.filter(name=tag).first() or self.all_tags.filter(name=tag).first() + if not tag_obj: raise TagNotFoundError + + if tag_obj.system: + # because system tags are hidden by default TagManager + tag_obj.delete() else: - tag_obj = Tag.objects.get(name=tag) self.tags.remove(tag_obj) self.add_log( action=NodeLog.TAG_REMOVED, @@ -1034,10 +1043,12 @@ def remove_tag(self, tag, auth, save=True): auth=auth, save=False, ) - if save: - self.save() - self.update_search() - return True + + if save: + self.save() + + self.update_search() + return True def remove_tags(self, tags, auth, save=True): """ diff --git a/osf/models/preprint.py b/osf/models/preprint.py index f12060bbce2..3217668f539 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -1107,13 +1107,17 @@ def all_tags(self): # Tag's default manager only returns non-system tags, so we can't use self.tags return Tag.all_tags.filter(preprint_tagged=self) + @property + def system_tags_objects(self): + return self.all_tags.filter(system=True) + @property def system_tags(self): """The system tags associated with this node. This currently returns a list of string names for the tags, for compatibility with v1. Eventually, we can just return the QuerySet. """ - return self.all_tags.filter(system=True).values_list('name', flat=True) + return self.system_tags_objects.values_list('name', flat=True) # Override Taggable def add_tag_log(self, tag, auth): @@ -1134,10 +1138,15 @@ def on_tag_added(self, tag): def remove_tag(self, tag, auth, save=True): if not tag: raise InvalidTagError - elif not self.tags.filter(name=tag).exists(): + + tag_obj = self.tags.filter(name=tag).first() or self.all_tags.filter(name=tag).first() + if not tag_obj: raise TagNotFoundError + + if tag_obj.system: + # because system tags are hidden by default TagManager + tag_obj.delete() else: - tag_obj = Tag.objects.get(name=tag) self.tags.remove(tag_obj) self.add_log( action=PreprintLog.TAG_REMOVED, @@ -1148,10 +1157,12 @@ def remove_tag(self, tag, auth, save=True): auth=auth, save=False, ) - if save: - self.save() - update_or_enqueue_on_preprint_updated(preprint_id=self._id, saved_fields=['tags']) - return True + + if save: + self.save() + + update_or_enqueue_on_preprint_updated(preprint_id=self._id, saved_fields=['tags']) + return True @require_permission([WRITE]) def set_supplemental_node(self, node, auth, save=False, ignore_node_permissions=False, **kwargs): diff --git a/osf/models/registrations.py b/osf/models/registrations.py index da63a0ca1f1..79e6547c939 100644 --- a/osf/models/registrations.py +++ b/osf/models/registrations.py @@ -1317,13 +1317,17 @@ def all_tags(self): # Tag's default manager only returns non-system tags, so we can't use self.tags return Tag.all_tags.filter(draftregistration_tagged=self) + @property + def system_tags_objects(self): + return self.all_tags.filter(system=True) + @property def system_tags(self): """The system tags associated with this draft registration. This currently returns a list of string names for the tags, for compatibility with v1. Eventually, we can just return the QuerySet. """ - return self.all_tags.filter(system=True).values_list('name', flat=True) + return self.system_tags_objects.values_list('name', flat=True) @classmethod def create_from_node(cls, user, schema, node=None, data=None, provider=None): diff --git a/osf/models/user.py b/osf/models/user.py index f312c715e2c..6fcfbb6e159 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -40,7 +40,13 @@ translations as gv_translations, ) from osf.utils.requests import get_current_request -from osf.exceptions import reraise_django_validation_errors, UserStateError +from osf.exceptions import ( + reraise_django_validation_errors, + UserStateError, + InvalidTagError, + TagNotFoundError +) + from .base import BaseModel, GuidMixin, GuidMixinQuerySet from .notable_domain import NotableDomain from .contributor import Contributor, RecentlyAddedContributor @@ -530,13 +536,17 @@ def all_tags(self): # Tag's default manager only returns non-system tags, so we can't use self.tags return Tag.all_tags.filter(osfuser=self) + @property + def system_tags_objects(self): + return self.all_tags.filter(system=True) + @property def system_tags(self): """The system tags associated with this node. This currently returns a list of string names for the tags, for compatibility with v1. Eventually, we can just return the QuerySet. """ - return self.all_tags.filter(system=True).values_list('name', flat=True) + return self.system_tags_objects.values_list('name', flat=True) @property def csl_given_name(self): @@ -1600,6 +1610,25 @@ def add_system_tag(self, tag): self.tags.add(tag_instance) return tag_instance + def remove_tag(self, tag, auth, save=True): + if not tag: + raise InvalidTagError + + tag_instance = self.all_tags.filter(name=tag).first() + if not tag_instance: + raise TagNotFoundError + + if not tag_instance.system: + raise ValueError('Non-system tag passed to add_system_tag') + + # because system tags are hidden by default TagManager + tag_instance.delete() + if save: + self.save() + + self.update_search() + return True + def get_recently_added(self): return ( each.contributor