diff --git a/mayan/apps/document_comments/api_views.py b/mayan/apps/document_comments/api_views.py index 7ae0ed67db..383b1b26cb 100644 --- a/mayan/apps/document_comments/api_views.py +++ b/mayan/apps/document_comments/api_views.py @@ -8,8 +8,8 @@ from mayan.apps.acls.models import AccessControlList from mayan.apps.documents.models import Document from .permissions import ( - permission_comment_create, permission_comment_delete, - permission_comment_view + permission_document_comment_create, permission_document_comment_delete, + permission_document_comment_view ) from .serializers import CommentSerializer, WritableCommentSerializer @@ -21,9 +21,9 @@ class APICommentListView(generics.ListCreateAPIView): """ def get_document(self): if self.request.method == 'GET': - permission_required = permission_comment_view + permission_required = permission_document_comment_view else: - permission_required = permission_comment_create + permission_required = permission_document_comment_create document = get_object_or_404( klass=Document, pk=self.kwargs['document_pk'] @@ -76,9 +76,9 @@ class APICommentView(generics.RetrieveDestroyAPIView): def get_document(self): if self.request.method == 'GET': - permission_required = permission_comment_view + permission_required = permission_document_comment_view else: - permission_required = permission_comment_delete + permission_required = permission_document_comment_delete document = get_object_or_404( klass=Document, pk=self.kwargs['document_pk'] diff --git a/mayan/apps/document_comments/apps.py b/mayan/apps/document_comments/apps.py index d01a8ebbd3..643e9f726d 100644 --- a/mayan/apps/document_comments/apps.py +++ b/mayan/apps/document_comments/apps.py @@ -5,20 +5,26 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.classes import ModelPermission from mayan.apps.common.apps import MayanAppConfig -from mayan.apps.common.menus import menu_facet, menu_object, menu_secondary +from mayan.apps.common.menus import ( + menu_facet, menu_list_facet, menu_object, menu_secondary +) from mayan.apps.documents.search import document_page_search, document_search from mayan.apps.events.classes import ModelEventType +from mayan.apps.events.links import ( + link_events_for_object, +) +from mayan.apps.events.permissions import permission_events_view from mayan.apps.navigation.classes import SourceColumn from .events import ( - event_document_comment_create, event_document_comment_delete + event_document_comment_created, event_document_comment_deleted ) from .links import ( link_comment_add, link_comment_delete, link_comments_for_document ) from .permissions import ( - permission_comment_create, permission_comment_delete, - permission_comment_view + permission_document_comment_create, permission_document_comment_delete, + permission_document_comment_view ) @@ -32,6 +38,7 @@ class DocumentCommentsApp(MayanAppConfig): def ready(self): super(DocumentCommentsApp, self).ready() + from actstream import registry Document = apps.get_model( app_label='documents', model_name='Document' @@ -41,14 +48,20 @@ class DocumentCommentsApp(MayanAppConfig): ModelEventType.register( model=Document, event_types=( - event_document_comment_create, event_document_comment_delete + event_document_comment_created, event_document_comment_deleted ) ) + ModelPermission.register( + model=Comment, permissions=(permission_events_view,) + ) + ModelPermission.register_inheritance( + model=Comment, related='document', + ) ModelPermission.register( model=Document, permissions=( - permission_comment_create, permission_comment_delete, - permission_comment_view + permission_document_comment_create, permission_document_comment_delete, + permission_document_comment_view ) ) @@ -68,6 +81,16 @@ class DocumentCommentsApp(MayanAppConfig): label=_('Comments') ) + menu_facet.bind_links( + links=(link_comments_for_document,), sources=(Document,) + ) + + menu_list_facet.bind_links( + links=( + link_events_for_object, + ), sources=(Comment,) + ) + menu_secondary.bind_links( links=(link_comment_add,), sources=( @@ -75,9 +98,9 @@ class DocumentCommentsApp(MayanAppConfig): 'comments:comment_delete', 'comments:comment_multiple_delete' ) ) + menu_object.bind_links( links=(link_comment_delete,), sources=(Comment,) ) - menu_facet.bind_links( - links=(link_comments_for_document,), sources=(Document,) - ) + + registry.register(Comment) diff --git a/mayan/apps/document_comments/events.py b/mayan/apps/document_comments/events.py index b5647fa14e..9157fd1761 100644 --- a/mayan/apps/document_comments/events.py +++ b/mayan/apps/document_comments/events.py @@ -8,9 +8,9 @@ namespace = EventTypeNamespace( label=_('Document comments'), name='document_comments' ) -event_document_comment_create = namespace.add_event_type( +event_document_comment_created = namespace.add_event_type( label=_('Document comment created'), name='create' ) -event_document_comment_delete = namespace.add_event_type( +event_document_comment_deleted = namespace.add_event_type( label=_('Document comment deleted'), name='delete' ) diff --git a/mayan/apps/document_comments/links.py b/mayan/apps/document_comments/links.py index a113be3415..da655c196c 100644 --- a/mayan/apps/document_comments/links.py +++ b/mayan/apps/document_comments/links.py @@ -6,22 +6,22 @@ from mayan.apps.navigation.classes import Link from .icons import icon_comment_add, icon_comment_delete, icon_comments_for_document from .permissions import ( - permission_comment_create, permission_comment_delete, - permission_comment_view + permission_document_comment_create, permission_document_comment_delete, + permission_document_comment_view ) link_comment_add = Link( args='object.pk', icon_class=icon_comment_add, - permissions=(permission_comment_create,), text=_('Add comment'), + permissions=(permission_document_comment_create,), text=_('Add comment'), view='comments:comment_add', ) link_comment_delete = Link( args='object.pk', icon_class=icon_comment_delete, - permissions=(permission_comment_delete,), tags='dangerous', + permissions=(permission_document_comment_delete,), tags='dangerous', text=_('Delete'), view='comments:comment_delete', ) link_comments_for_document = Link( args='resolved_object.pk', icon_class=icon_comments_for_document, - permissions=(permission_comment_view,), text=_('Comments'), + permissions=(permission_document_comment_view,), text=_('Comments'), view='comments:comments_for_document', ) diff --git a/mayan/apps/document_comments/models.py b/mayan/apps/document_comments/models.py index 94bde4e8d5..dd420ee661 100644 --- a/mayan/apps/document_comments/models.py +++ b/mayan/apps/document_comments/models.py @@ -3,14 +3,14 @@ from __future__ import unicode_literals import logging from django.conf import settings -from django.db import models +from django.db import models, transaction from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from mayan.apps.documents.models import Document from .events import ( - event_document_comment_create, event_document_comment_delete + event_document_comment_created, event_document_comment_deleted ) logger = logging.getLogger(__name__) @@ -46,31 +46,20 @@ class Comment(models.Model): return self.comment def delete(self, *args, **kwargs): - user = kwargs.pop('_user', None) - super(Comment, self).delete(*args, **kwargs) - if user: - event_document_comment_delete.commit( - actor=user, target=self.document + _user = kwargs.pop('_user', None) + with transaction.atomic(): + super(Comment, self).delete(*args, **kwargs) + event_document_comment_deleted.commit( + actor=_user, target=self.document ) - else: - event_document_comment_delete.commit(target=self.document) def save(self, *args, **kwargs): - user = kwargs.pop('_user', None) or self.user - is_new = not self.pk - super(Comment, self).save(*args, **kwargs) - if is_new: - if user: - event_document_comment_create.commit( - actor=user, target=self.document - ) - logger.info( - 'Comment "%s" added to document "%s" by user "%s"', - self.comment, self.document, user - ) - else: - event_document_comment_create.commit(target=self.document) - logger.info( - 'Comment "%s" added to document "%s"', self.comment, - self.document + _user = kwargs.pop('_user', None) or self.user + created = not self.pk + + with transaction.atomic(): + super(Comment, self).save(*args, **kwargs) + if created: + event_document_comment_created.commit( + action_object=self, actor=_user, target=self.document, ) diff --git a/mayan/apps/document_comments/permissions.py b/mayan/apps/document_comments/permissions.py index a5fa60a7d2..f4109a26f1 100644 --- a/mayan/apps/document_comments/permissions.py +++ b/mayan/apps/document_comments/permissions.py @@ -6,12 +6,12 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Comments'), name='comments') -permission_comment_create = namespace.add_permission( +permission_document_comment_create = namespace.add_permission( label=_('Create new comments'), name='comment_create' ) -permission_comment_delete = namespace.add_permission( +permission_document_comment_delete = namespace.add_permission( label=_('Delete comments'), name='comment_delete' ) -permission_comment_view = namespace.add_permission( +permission_document_comment_view = namespace.add_permission( label=_('View comments'), name='comment_view' ) diff --git a/mayan/apps/document_comments/tests/mixins.py b/mayan/apps/document_comments/tests/mixins.py new file mode 100644 index 0000000000..1aa7612324 --- /dev/null +++ b/mayan/apps/document_comments/tests/mixins.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals + +from .literals import TEST_COMMENT_TEXT + + +class DocumentCommentTestMixin(object): + def _create_test_comment(self): + self.test_document_comment = self.test_document.comments.create( + comment=TEST_COMMENT_TEXT, user=self._test_case_user + ) + + +class DocumentCommentViewTestMixin(object): + def _request_test_comment_create_view(self): + return self.post( + viewname='comments:comment_add', kwargs={ + 'pk': self.test_document.pk + }, data={'comment': TEST_COMMENT_TEXT} + ) + + def _request_test_comment_delete_view(self): + return self.post( + viewname='comments:comment_delete', kwargs={ + 'pk': self.test_document_comment.pk + }, + ) diff --git a/mayan/apps/document_comments/tests/test_api.py b/mayan/apps/document_comments/tests/test_api.py index 666d700598..a2f63b5982 100644 --- a/mayan/apps/document_comments/tests/test_api.py +++ b/mayan/apps/document_comments/tests/test_api.py @@ -7,19 +7,17 @@ from mayan.apps.rest_api.tests import BaseAPITestCase from ..models import Comment from ..permissions import ( - permission_comment_create, permission_comment_delete, - permission_comment_view + permission_document_comment_create, permission_document_comment_delete, + permission_document_comment_view ) from .literals import TEST_COMMENT_TEXT +from .mixins import DocumentCommentTestMixin -class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase): - def _create_test_comment(self): - return self.test_document.comments.create( - comment=TEST_COMMENT_TEXT, user=self._test_case_user - ) - +class CommentAPITestCase( + DocumentCommentTestMixin, DocumentTestMixin, BaseAPITestCase +): def _request_comment_create_view(self): return self.post( viewname='rest_api:comment-list', kwargs={ @@ -37,7 +35,7 @@ class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_comment_create_view_with_access(self): self.grant_access( - obj=self.test_document, permission=permission_comment_create + obj=self.test_document, permission=permission_document_comment_create ) response = self._request_comment_create_view() @@ -51,53 +49,53 @@ class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase): return self.delete( viewname='rest_api:comment-detail', kwargs={ 'document_pk': self.test_document.pk, - 'comment_pk': self.test_comment.pk, + 'comment_pk': self.test_document_comment.pk, } ) def test_comment_delete_view_no_access(self): - self.test_comment = self._create_test_comment() + self._create_test_comment() response = self._request_comment_delete_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertTrue(self.test_comment in Comment.objects.all()) + self.assertTrue(self.test_document_comment in Comment.objects.all()) def test_comment_delete_view_with_access(self): - self.test_comment = self._create_test_comment() + self._create_test_comment() self.grant_access( - obj=self.test_document, permission=permission_comment_delete + obj=self.test_document, permission=permission_document_comment_delete ) response = self._request_comment_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse(self.test_comment in Comment.objects.all()) + self.assertFalse(self.test_document_comment in Comment.objects.all()) def _request_comment_view(self): return self.get( viewname='rest_api:comment-detail', kwargs={ 'document_pk': self.test_document.pk, - 'comment_pk': self.test_comment.pk + 'comment_pk': self.test_document_comment.pk } ) def test_comment_detail_view_no_access(self): - self.test_comment = self._create_test_comment() + self._create_test_comment() response = self._request_comment_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_comment_detail_view_with_access(self): - self.test_comment = self._create_test_comment() + self._create_test_comment() self.grant_access( - obj=self.test_document, permission=permission_comment_view + obj=self.test_document, permission=permission_document_comment_view ) response = self._request_comment_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['comment'], self.test_comment.comment) + self.assertEqual(response.data['comment'], self.test_document_comment.comment) def _request_comment_list_view(self): return self.get( @@ -107,19 +105,19 @@ class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase): ) def test_comment_list_view_no_access(self): - self.test_comment = self._create_test_comment() + self._create_test_comment() response = self._request_comment_list_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_comment_list_view_with_access(self): - self.test_comment = self._create_test_comment() + self._create_test_comment() self.grant_access( - obj=self.test_document, permission=permission_comment_view + obj=self.test_document, permission=permission_document_comment_view ) response = self._request_comment_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - response.data['results'][0]['comment'], self.test_comment.comment + response.data['results'][0]['comment'], self.test_document_comment.comment ) diff --git a/mayan/apps/document_comments/tests/test_events.py b/mayan/apps/document_comments/tests/test_events.py new file mode 100644 index 0000000000..7aa33b732c --- /dev/null +++ b/mayan/apps/document_comments/tests/test_events.py @@ -0,0 +1,79 @@ +from __future__ import unicode_literals + +from actstream.models import Action + +from mayan.apps.documents.tests import GenericDocumentViewTestCase + +from ..events import ( + event_document_comment_created, event_document_comment_deleted +) +from ..models import Comment +from ..permissions import ( + permission_document_comment_create, permission_document_comment_delete +) + +from .mixins import DocumentCommentTestMixin, DocumentCommentViewTestMixin + + +class CommentEventsTestCase( + DocumentCommentTestMixin, DocumentCommentViewTestMixin, + GenericDocumentViewTestCase +): + def test_comment_create_event_no_permissions(self): + action_count = Action.objects.count() + + response = self._request_test_comment_create_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(Action.objects.count(), action_count) + + def test_comment_create_event_with_permissions(self): + self.grant_permission(permission=permission_document_comment_create) + + action_count = Action.objects.count() + + response = self._request_test_comment_create_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(Action.objects.count(), action_count + 1) + + event = Action.objects.first() + + comment = Comment.objects.first() + + self.assertEqual(event.action_object, comment) + self.assertEqual(event.actor, self._test_case_user) + self.assertEqual(event.target, self.test_document) + self.assertEqual(event.verb, event_document_comment_created.id) + + def test_comment_delete_event_no_permissions(self): + self._create_test_comment() + + action_count = Action.objects.count() + + response = self._request_test_comment_delete_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(Action.objects.count(), action_count) + + def test_comment_delete_event_with_access(self): + self._create_test_comment() + + self.grant_access( + obj=self.test_document, + permission=permission_document_comment_delete + ) + + action_count = Action.objects.count() + + response = self._request_test_comment_delete_view() + self.assertEqual(response.status_code, 302) + # Total count remains the same. Document comment created is removed due + # to cascade delete, document comment deleted event is added. + self.assertEqual(Action.objects.count(), action_count) + + event = Action.objects.first() + + self.assertEqual(event.actor, self._test_case_user) + self.assertEqual(event.target, self.test_document) + self.assertEqual(event.verb, event_document_comment_deleted.id) diff --git a/mayan/apps/document_comments/views.py b/mayan/apps/document_comments/views.py index b30ad5785a..8277436432 100644 --- a/mayan/apps/document_comments/views.py +++ b/mayan/apps/document_comments/views.py @@ -1,51 +1,40 @@ from __future__ import absolute_import, unicode_literals -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document from .icons import icon_comments_for_document from .links import link_comment_add from .models import Comment from .permissions import ( - permission_comment_create, permission_comment_delete, - permission_comment_view + permission_document_comment_create, permission_document_comment_delete, + permission_document_comment_view ) -class DocumentCommentCreateView(SingleObjectCreateView): +class DocumentCommentCreateView(ExternalObjectMixin, SingleObjectCreateView): + external_object_class = Document + external_object_permission = permission_document_comment_create + external_object_pk_url_kwarg = 'pk' fields = ('comment',) model = Comment - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_document(), permissions=(permission_comment_create,), - user=request.user - ) - - return super( - DocumentCommentCreateView, self - ).dispatch(request, *args, **kwargs) - - def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) - def get_extra_context(self): return { - 'object': self.get_document(), - 'title': _('Add comment to document: %s') % self.get_document(), + 'object': self.external_object, + 'title': _('Add comment to document: %s') % self.external_object, } def get_instance_extra_data(self): return { - 'document': self.get_document(), 'user': self.request.user, + 'document': self.external_object, 'user': self.request.user, } def get_post_action_redirect(self): @@ -63,65 +52,47 @@ class DocumentCommentCreateView(SingleObjectCreateView): class DocumentCommentDeleteView(SingleObjectDeleteView): model = Comment - - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_object().document, - permissions=(permission_comment_delete,), user=request.user - ) - - return super( - DocumentCommentDeleteView, self - ).dispatch(request, *args, **kwargs) + pk_url_kwarg = 'pk' + object_permission = permission_document_comment_delete def get_delete_extra_data(self): return {'_user': self.request.user} def get_extra_context(self): return { - 'comment': self.get_object(), - 'navigation_object_list': ('object', 'comment'), - 'object': self.get_object().document, - 'title': _('Delete comment: %s?') % self.get_object(), + 'object': self.object.document, + 'title': _('Delete comment: %s?') % self.object, } def get_post_action_redirect(self): return reverse( viewname='comments:comments_for_document', kwargs={ - 'pk': self.get_object().document.pk + 'pk': self.object.document.pk } ) -class DocumentCommentListView(SingleObjectListView): - def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) +class DocumentCommentListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Document + external_object_permission = permission_document_comment_view + external_object_pk_url_kwarg = 'pk' def get_extra_context(self): return { 'hide_link': True, 'hide_object': True, 'no_results_icon': icon_comments_for_document, + 'no_results_external_link': link_comment_add.resolve( + RequestContext(self.request, {'object': self.external_object}) + ), 'no_results_text': _( 'Document comments are timestamped text entries from users. ' 'They are great for collaboration.' ), - 'no_results_main_link': link_comment_add.resolve( - RequestContext( - request=self.request, dict_={ - 'object': self.get_document() - } - ) - ), 'no_results_title': _('There are no comments'), - 'object': self.get_document(), - 'title': _('Comments for document: %s') % self.get_document(), + 'object': self.external_object, + 'title': _('Comments for document: %s') % self.external_object, } def get_source_queryset(self): - AccessControlList.objects.check_access( - obj=self.get_document(), permissions=(permission_comment_view,), - user=self.request.user - ) - - return self.get_document().comments.all() + return self.external_object.comments.all()