Update comments app

Add transaction handling. Add comment view link. Update views to
user ExternalObjectMixin. Add event tests.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-05-09 20:22:12 -04:00
parent fffcf4d3da
commit cbd51c5f26
10 changed files with 216 additions and 130 deletions

View File

@@ -8,8 +8,8 @@ from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from .permissions import ( from .permissions import (
permission_comment_create, permission_comment_delete, permission_document_comment_create, permission_document_comment_delete,
permission_comment_view permission_document_comment_view
) )
from .serializers import CommentSerializer, WritableCommentSerializer from .serializers import CommentSerializer, WritableCommentSerializer
@@ -21,9 +21,9 @@ class APICommentListView(generics.ListCreateAPIView):
""" """
def get_document(self): def get_document(self):
if self.request.method == 'GET': if self.request.method == 'GET':
permission_required = permission_comment_view permission_required = permission_document_comment_view
else: else:
permission_required = permission_comment_create permission_required = permission_document_comment_create
document = get_object_or_404( document = get_object_or_404(
klass=Document, pk=self.kwargs['document_pk'] klass=Document, pk=self.kwargs['document_pk']
@@ -76,9 +76,9 @@ class APICommentView(generics.RetrieveDestroyAPIView):
def get_document(self): def get_document(self):
if self.request.method == 'GET': if self.request.method == 'GET':
permission_required = permission_comment_view permission_required = permission_document_comment_view
else: else:
permission_required = permission_comment_delete permission_required = permission_document_comment_delete
document = get_object_or_404( document = get_object_or_404(
klass=Document, pk=self.kwargs['document_pk'] klass=Document, pk=self.kwargs['document_pk']

View File

@@ -5,20 +5,26 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission from mayan.apps.acls.classes import ModelPermission
from mayan.apps.common.apps import MayanAppConfig 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.documents.search import document_page_search, document_search
from mayan.apps.events.classes import ModelEventType 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 mayan.apps.navigation.classes import SourceColumn
from .events import ( from .events import (
event_document_comment_create, event_document_comment_delete event_document_comment_created, event_document_comment_deleted
) )
from .links import ( from .links import (
link_comment_add, link_comment_delete, link_comments_for_document link_comment_add, link_comment_delete, link_comments_for_document
) )
from .permissions import ( from .permissions import (
permission_comment_create, permission_comment_delete, permission_document_comment_create, permission_document_comment_delete,
permission_comment_view permission_document_comment_view
) )
@@ -32,6 +38,7 @@ class DocumentCommentsApp(MayanAppConfig):
def ready(self): def ready(self):
super(DocumentCommentsApp, self).ready() super(DocumentCommentsApp, self).ready()
from actstream import registry
Document = apps.get_model( Document = apps.get_model(
app_label='documents', model_name='Document' app_label='documents', model_name='Document'
@@ -41,14 +48,20 @@ class DocumentCommentsApp(MayanAppConfig):
ModelEventType.register( ModelEventType.register(
model=Document, event_types=( 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( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_comment_create, permission_comment_delete, permission_document_comment_create, permission_document_comment_delete,
permission_comment_view permission_document_comment_view
) )
) )
@@ -68,6 +81,16 @@ class DocumentCommentsApp(MayanAppConfig):
label=_('Comments') 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( menu_secondary.bind_links(
links=(link_comment_add,), links=(link_comment_add,),
sources=( sources=(
@@ -75,9 +98,9 @@ class DocumentCommentsApp(MayanAppConfig):
'comments:comment_delete', 'comments:comment_multiple_delete' 'comments:comment_delete', 'comments:comment_multiple_delete'
) )
) )
menu_object.bind_links( menu_object.bind_links(
links=(link_comment_delete,), sources=(Comment,) links=(link_comment_delete,), sources=(Comment,)
) )
menu_facet.bind_links(
links=(link_comments_for_document,), sources=(Document,) registry.register(Comment)
)

View File

@@ -8,9 +8,9 @@ namespace = EventTypeNamespace(
label=_('Document comments'), name='document_comments' 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' 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' label=_('Document comment deleted'), name='delete'
) )

View File

@@ -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 .icons import icon_comment_add, icon_comment_delete, icon_comments_for_document
from .permissions import ( from .permissions import (
permission_comment_create, permission_comment_delete, permission_document_comment_create, permission_document_comment_delete,
permission_comment_view permission_document_comment_view
) )
link_comment_add = Link( link_comment_add = Link(
args='object.pk', icon_class=icon_comment_add, 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', view='comments:comment_add',
) )
link_comment_delete = Link( link_comment_delete = Link(
args='object.pk', icon_class=icon_comment_delete, 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', text=_('Delete'), view='comments:comment_delete',
) )
link_comments_for_document = Link( link_comments_for_document = Link(
args='resolved_object.pk', icon_class=icon_comments_for_document, 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', view='comments:comments_for_document',
) )

View File

@@ -3,14 +3,14 @@ from __future__ import unicode_literals
import logging import logging
from django.conf import settings 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.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from .events import ( from .events import (
event_document_comment_create, event_document_comment_delete event_document_comment_created, event_document_comment_deleted
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -46,31 +46,20 @@ class Comment(models.Model):
return self.comment return self.comment
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
user = kwargs.pop('_user', None) _user = kwargs.pop('_user', None)
with transaction.atomic():
super(Comment, self).delete(*args, **kwargs) super(Comment, self).delete(*args, **kwargs)
if user: event_document_comment_deleted.commit(
event_document_comment_delete.commit( actor=_user, target=self.document
actor=user, target=self.document
) )
else:
event_document_comment_delete.commit(target=self.document)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
user = kwargs.pop('_user', None) or self.user _user = kwargs.pop('_user', None) or self.user
is_new = not self.pk created = not self.pk
with transaction.atomic():
super(Comment, self).save(*args, **kwargs) super(Comment, self).save(*args, **kwargs)
if is_new: if created:
if user: event_document_comment_created.commit(
event_document_comment_create.commit( action_object=self, actor=_user, target=self.document,
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
) )

View File

@@ -6,12 +6,12 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Comments'), name='comments') 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' 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' 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' label=_('View comments'), name='comment_view'
) )

View File

@@ -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
},
)

View File

@@ -7,19 +7,17 @@ from mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import Comment from ..models import Comment
from ..permissions import ( from ..permissions import (
permission_comment_create, permission_comment_delete, permission_document_comment_create, permission_document_comment_delete,
permission_comment_view permission_document_comment_view
) )
from .literals import TEST_COMMENT_TEXT from .literals import TEST_COMMENT_TEXT
from .mixins import DocumentCommentTestMixin
class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase): class CommentAPITestCase(
def _create_test_comment(self): DocumentCommentTestMixin, DocumentTestMixin, BaseAPITestCase
return self.test_document.comments.create( ):
comment=TEST_COMMENT_TEXT, user=self._test_case_user
)
def _request_comment_create_view(self): def _request_comment_create_view(self):
return self.post( return self.post(
viewname='rest_api:comment-list', kwargs={ viewname='rest_api:comment-list', kwargs={
@@ -37,7 +35,7 @@ class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_comment_create_view_with_access(self): def test_comment_create_view_with_access(self):
self.grant_access( 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() response = self._request_comment_create_view()
@@ -51,53 +49,53 @@ class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase):
return self.delete( return self.delete(
viewname='rest_api:comment-detail', kwargs={ viewname='rest_api:comment-detail', kwargs={
'document_pk': self.test_document.pk, '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): 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() response = self._request_comment_delete_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 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): def test_comment_delete_view_with_access(self):
self.test_comment = self._create_test_comment() self._create_test_comment()
self.grant_access( 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() response = self._request_comment_delete_view()
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 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): def _request_comment_view(self):
return self.get( return self.get(
viewname='rest_api:comment-detail', kwargs={ viewname='rest_api:comment-detail', kwargs={
'document_pk': self.test_document.pk, '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): def test_comment_detail_view_no_access(self):
self.test_comment = self._create_test_comment() self._create_test_comment()
response = self._request_comment_view() response = self._request_comment_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_comment_detail_view_with_access(self): def test_comment_detail_view_with_access(self):
self.test_comment = self._create_test_comment() self._create_test_comment()
self.grant_access( 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() response = self._request_comment_view()
self.assertEqual(response.status_code, status.HTTP_200_OK) 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): def _request_comment_list_view(self):
return self.get( return self.get(
@@ -107,19 +105,19 @@ class CommentAPITestCase(DocumentTestMixin, BaseAPITestCase):
) )
def test_comment_list_view_no_access(self): 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() response = self._request_comment_list_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_comment_list_view_with_access(self): def test_comment_list_view_with_access(self):
self.test_comment = self._create_test_comment() self._create_test_comment()
self.grant_access( 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() response = self._request_comment_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data['results'][0]['comment'], self.test_comment.comment response.data['results'][0]['comment'], self.test_document_comment.comment
) )

View File

@@ -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)

View File

@@ -1,51 +1,40 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.shortcuts import get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView
) )
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from .icons import icon_comments_for_document from .icons import icon_comments_for_document
from .links import link_comment_add from .links import link_comment_add
from .models import Comment from .models import Comment
from .permissions import ( from .permissions import (
permission_comment_create, permission_comment_delete, permission_document_comment_create, permission_document_comment_delete,
permission_comment_view 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',) fields = ('comment',)
model = 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): def get_extra_context(self):
return { return {
'object': self.get_document(), 'object': self.external_object,
'title': _('Add comment to document: %s') % self.get_document(), 'title': _('Add comment to document: %s') % self.external_object,
} }
def get_instance_extra_data(self): def get_instance_extra_data(self):
return { return {
'document': self.get_document(), 'user': self.request.user, 'document': self.external_object, 'user': self.request.user,
} }
def get_post_action_redirect(self): def get_post_action_redirect(self):
@@ -63,65 +52,47 @@ class DocumentCommentCreateView(SingleObjectCreateView):
class DocumentCommentDeleteView(SingleObjectDeleteView): class DocumentCommentDeleteView(SingleObjectDeleteView):
model = Comment model = Comment
pk_url_kwarg = 'pk'
def dispatch(self, request, *args, **kwargs): object_permission = permission_document_comment_delete
AccessControlList.objects.check_access(
obj=self.get_object().document,
permissions=(permission_comment_delete,), user=request.user
)
return super(
DocumentCommentDeleteView, self
).dispatch(request, *args, **kwargs)
def get_delete_extra_data(self): def get_delete_extra_data(self):
return {'_user': self.request.user} return {'_user': self.request.user}
def get_extra_context(self): def get_extra_context(self):
return { return {
'comment': self.get_object(), 'object': self.object.document,
'navigation_object_list': ('object', 'comment'), 'title': _('Delete comment: %s?') % self.object,
'object': self.get_object().document,
'title': _('Delete comment: %s?') % self.get_object(),
} }
def get_post_action_redirect(self): def get_post_action_redirect(self):
return reverse( return reverse(
viewname='comments:comments_for_document', kwargs={ viewname='comments:comments_for_document', kwargs={
'pk': self.get_object().document.pk 'pk': self.object.document.pk
} }
) )
class DocumentCommentListView(SingleObjectListView): class DocumentCommentListView(ExternalObjectMixin, SingleObjectListView):
def get_document(self): external_object_class = Document
return get_object_or_404(klass=Document, pk=self.kwargs['pk']) external_object_permission = permission_document_comment_view
external_object_pk_url_kwarg = 'pk'
def get_extra_context(self): def get_extra_context(self):
return { return {
'hide_link': True, 'hide_link': True,
'hide_object': True, 'hide_object': True,
'no_results_icon': icon_comments_for_document, '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': _( 'no_results_text': _(
'Document comments are timestamped text entries from users. ' 'Document comments are timestamped text entries from users. '
'They are great for collaboration.' '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'), 'no_results_title': _('There are no comments'),
'object': self.get_document(), 'object': self.external_object,
'title': _('Comments for document: %s') % self.get_document(), 'title': _('Comments for document: %s') % self.external_object,
} }
def get_source_queryset(self): def get_source_queryset(self):
AccessControlList.objects.check_access( return self.external_object.comments.all()
obj=self.get_document(), permissions=(permission_comment_view,),
user=self.request.user
)
return self.get_document().comments.all()