diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b5f288a..eca3114a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog # +## 2.2.0 ??? (unreleased) +### Features +- [API] edit comment endpoint: comment owners and project admins can edit existing comments + ## 2.1.0 Ursus Americanus (2016-05-03) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index d10194ce..3f1ca240 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -23,6 +23,7 @@ from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ReadOnlyListViewSet from taiga.base.api.utils import get_object_or_404 +from taiga.mdrender.service import render as mdrender from . import permissions from . import serializers @@ -56,42 +57,93 @@ class HistoryViewSet(ReadOnlyListViewSet): return response.Ok(serializer.data) + @detail_route(methods=['get']) + def comment_versions(self, request, pk): + obj = self.get_object() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + + self.check_permissions(request, 'comment_versions', history_entry) + + if history_entry is None: + return response.NotFound() + + history_entry.attach_user_info_to_comment_versions() + return response.Ok(history_entry.comment_versions) + + @detail_route(methods=['post']) + def edit_comment(self, request, pk): + obj = self.get_object() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + obj = services.get_instance_from_key(history_entry.key) + comment = request.DATA.get("comment", None) + + self.check_permissions(request, 'edit_comment', history_entry) + + if history_entry is None: + return response.NotFound() + + if comment is None: + return response.BadRequest({"error": _("comment is required")}) + + if history_entry.delete_comment_date or history_entry.delete_comment_user: + return response.BadRequest({"error": _("deleted comments can't be edited")}) + + # comment_versions can be None if there are no historic versions of the comment + comment_versions = history_entry.comment_versions or [] + comment_versions.append({ + "date": history_entry.created_at, + "comment": history_entry.comment, + "comment_html": history_entry.comment_html, + "user": { + "id": request.user.pk, + } + }) + + history_entry.edit_comment_date = timezone.now() + history_entry.comment = comment + history_entry.comment_html = mdrender(obj.project, comment) + history_entry.comment_versions = comment_versions + history_entry.save() + return response.Ok() + @detail_route(methods=['post']) def delete_comment(self, request, pk): obj = self.get_object() - comment_id = request.QUERY_PARAMS.get('id', None) - comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() - self.check_permissions(request, 'delete_comment', comment) + self.check_permissions(request, 'delete_comment', history_entry) - if comment is None: + if history_entry is None: return response.NotFound() - if comment.delete_comment_date or comment.delete_comment_user: + if history_entry.delete_comment_date or history_entry.delete_comment_user: return response.BadRequest({"error": _("Comment already deleted")}) - comment.delete_comment_date = timezone.now() - comment.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()} - comment.save() + history_entry.delete_comment_date = timezone.now() + history_entry.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()} + history_entry.save() return response.Ok() @detail_route(methods=['post']) def undelete_comment(self, request, pk): obj = self.get_object() - comment_id = request.QUERY_PARAMS.get('id', None) - comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() - self.check_permissions(request, 'undelete_comment', comment) + self.check_permissions(request, 'undelete_comment', history_entry) - if comment is None: + if history_entry is None: return response.NotFound() - if not comment.delete_comment_date and not comment.delete_comment_user: + if not history_entry.delete_comment_date and not history_entry.delete_comment_user: return response.BadRequest({"error": _("Comment not deleted")}) - comment.delete_comment_date = None - comment.delete_comment_user = None - comment.save() + history_entry.delete_comment_date = None + history_entry.delete_comment_user = None + history_entry.save() return response.Ok() # Just for restframework! Because it raises diff --git a/taiga/projects/history/migrations/0009_auto_20160512_1110.py b/taiga/projects/history/migrations/0009_auto_20160512_1110.py new file mode 100644 index 00000000..0cf39023 --- /dev/null +++ b/taiga/projects/history/migrations/0009_auto_20160512_1110.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-12 11:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import django_pgjson.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0008_auto_20150508_1028'), + ] + + operations = [ + migrations.AddField( + model_name='historyentry', + name='comment_versions', + field=django_pgjson.fields.JsonField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='historyentry', + name='edit_comment_date', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index e947c6fe..39012f60 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -67,6 +67,10 @@ class HistoryEntry(models.Model): delete_comment_date = models.DateTimeField(null=True, blank=True, default=None) delete_comment_user = JsonField(null=True, blank=True, default=None) + # Historic version of comments + comment_versions = JsonField(null=True, blank=True, default=None) + edit_comment_date = models.DateTimeField(null=True, blank=True, default=None) + # Flag for mark some history entries as # hidden. Hidden history entries are important # for save but not important to preview. @@ -111,6 +115,20 @@ class HistoryEntry(models.Model): self._owner = owner self._prefetched_owner = True + def attach_user_info_to_comment_versions(self): + if not self.comment_versions: + return + + from taiga.users.serializers import UserSerializer + + user_ids = [v["user"]["id"] for v in self.comment_versions if "user" in v and "id" in v["user"]] + users_by_id = {u.id: u for u in get_user_model().objects.filter(id__in=user_ids)} + + for version in self.comment_versions: + user = users_by_id.get(version["user"]["id"], None) + if user: + version["user"] = UserSerializer(user).data + @cached_property def values_diff(self): result = {} diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index 015ac22c..fdce68cd 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -33,32 +33,41 @@ class IsCommentOwner(PermissionComponent): return obj.user and obj.user.get("pk", "not-pk") == request.user.pk -class IsCommentProjectOwner(PermissionComponent): +class IsCommentProjectAdmin(PermissionComponent): def check_permissions(self, request, view, obj=None): model = get_model_from_key(obj.key) pk = get_pk_from_key(obj.key) project = model.objects.get(pk=pk) return is_project_admin(request.user, project) + class UserStoryHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() class TaskHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() class IssueHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() class WikiHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index 72b3c763..f231f29c 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -33,9 +33,11 @@ class HistoryEntrySerializer(serializers.ModelSerializer): values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) user = serializers.SerializerMethodField("get_user") delete_comment_user = JsonField() + comment_versions = JsonField() class Meta: model = models.HistoryEntry + exclude = ("comment_versions",) def get_user(self, entry): user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 22839ec8..61004471 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -91,6 +91,20 @@ def get_pk_from_key(key:str) -> object: return pk +def get_instance_from_key(key:str) -> object: + """ + Get instance from key + """ + model = get_model_from_key(key) + pk = get_pk_from_key(key) + try: + obj = model.objects.get(pk=pk) + return obj + except model.DoesNotExist: + # Catch simultaneous DELETE request + return None + + def register_values_implementation(typename:str, fn=None): """ Register values implementation for specified typename. diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index eda08031..c0f1dffa 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -44,7 +44,6 @@ def _clean_description_fields(values_diff): def on_new_history_entry(sender, instance, created, **kwargs): - if instance._importing: return @@ -81,6 +80,10 @@ def on_new_history_entry(sender, instance, created, **kwargs): if instance.delete_comment_date: extra_data["comment_deleted"] = True + # Detect edited comment + if instance.comment_versions is not None and len(instance.comment_versions)>0: + extra_data["comment_edited"] = True + created_datetime = instance.created_at _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data=extra_data) diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index b0991080..be17510c 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -1,6 +1,11 @@ from django.core.urlresolvers import reverse +from django.utils import timezone +from taiga.base.utils import json from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects.history.models import HistoryEntry +from taiga.projects.history.choices import HistoryType +from taiga.projects.history.services import make_key_from_model_object from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -21,11 +26,11 @@ def teardown_module(module): def data(): m = type("Models", (object,), {}) - m.registered_user = f.UserFactory.create() - m.project_member_with_perms = f.UserFactory.create() - m.project_member_without_perms = f.UserFactory.create() - m.project_owner = f.UserFactory.create() - m.other_user = f.UserFactory.create() + m.registered_user = f.UserFactory.create(full_name="registered_user") + m.project_member_with_perms = f.UserFactory.create(full_name="project_member_with_perms") + m.project_member_without_perms = f.UserFactory.create(full_name="project_member_without_perms") + m.project_owner = f.UserFactory.create(full_name="project_owner") + m.other_user = f.UserFactory.create(full_name="other_user") m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), @@ -76,39 +81,33 @@ def data(): return m +######################################################### +## User stories +######################################################### + + @pytest.fixture def data_us(data): m = type("Models", (object,), {}) m.public_user_story = f.UserStoryFactory(project=data.public_project, ref=1) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_user_story), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_user_story1 = f.UserStoryFactory(project=data.private_project1, ref=5) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_user_story1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9) - return m - - -@pytest.fixture -def data_task(data): - m = type("Models", (object,), {}) - m.public_task = f.TaskFactory(project=data.public_project, ref=2) - m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) - m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) - return m - - -@pytest.fixture -def data_issue(data): - m = type("Models", (object,), {}) - m.public_issue = f.IssueFactory(project=data.public_project, ref=3) - m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) - m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) - return m - - -@pytest.fixture -def data_wiki(data): - m = type("Models", (object,), {}) - m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) - m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) - m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_user_story2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) return m @@ -133,6 +132,222 @@ def test_user_story_history_retrieve(client, data, data_us): assert results == [401, 403, 403, 200, 200] +def test_user_story_action_edit_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_action_delete_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_us.public_history_entry.delete_comment_date = None + data_us.public_history_entry.delete_comment_user = None + data_us.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry1.delete_comment_date = None + data_us.private_history_entry1.delete_comment_user = None + data_us.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry2.delete_comment_date = None + data_us.private_history_entry2.delete_comment_user = None + data_us.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_user_story_action_undelete_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_us.public_history_entry.delete_comment_date = timezone.now() + data_us.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry1.delete_comment_date = timezone.now() + data_us.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry2.delete_comment_date = timezone.now() + data_us.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_user_story_action_comment_versions(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Tasks +######################################################### + + +@pytest.fixture +def data_task(data): + m = type("Models", (object,), {}) + m.public_task = f.TaskFactory(project=data.public_project, ref=2) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_task), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_task1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_task2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_task_history_retrieve(client, data, data_task): public_url = reverse('task-history-detail', kwargs={"pk": data_task.public_task.pk}) private_url1 = reverse('task-history-detail', kwargs={"pk": data_task.private_task1.pk}) @@ -154,6 +369,222 @@ def test_task_history_retrieve(client, data, data_task): assert results == [401, 403, 403, 200, 200] +def test_task_action_edit_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_action_delete_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_task.public_history_entry.delete_comment_date = None + data_task.public_history_entry.delete_comment_user = None + data_task.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry1.delete_comment_date = None + data_task.private_history_entry1.delete_comment_user = None + data_task.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry2.delete_comment_date = None + data_task.private_history_entry2.delete_comment_user = None + data_task.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_task_action_undelete_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_task.public_history_entry.delete_comment_date = timezone.now() + data_task.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry1.delete_comment_date = timezone.now() + data_task.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry2.delete_comment_date = timezone.now() + data_task.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_task_action_comment_versions(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Issues +######################################################### + + +@pytest.fixture +def data_issue(data): + m = type("Models", (object,), {}) + m.public_issue = f.IssueFactory(project=data.public_project, ref=3) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_issue), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_issue1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_issue2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_issue_history_retrieve(client, data, data_issue): public_url = reverse('issue-history-detail', kwargs={"pk": data_issue.public_issue.pk}) private_url1 = reverse('issue-history-detail', kwargs={"pk": data_issue.private_issue1.pk}) @@ -175,6 +606,222 @@ def test_issue_history_retrieve(client, data, data_issue): assert results == [401, 403, 403, 200, 200] +def test_issue_action_edit_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_action_delete_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_issue.public_history_entry.delete_comment_date = None + data_issue.public_history_entry.delete_comment_user = None + data_issue.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry1.delete_comment_date = None + data_issue.private_history_entry1.delete_comment_user = None + data_issue.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry2.delete_comment_date = None + data_issue.private_history_entry2.delete_comment_user = None + data_issue.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_issue_action_undelete_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_issue.public_history_entry.delete_comment_date = timezone.now() + data_issue.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry1.delete_comment_date = timezone.now() + data_issue.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry2.delete_comment_date = timezone.now() + data_issue.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_issue_action_comment_versions(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Wiki pages +######################################################### + + +@pytest.fixture +def data_wiki(data): + m = type("Models", (object,), {}) + m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_wiki), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_wiki1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_wiki2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_wiki_history_retrieve(client, data, data_wiki): public_url = reverse('wiki-history-detail', kwargs={"pk": data_wiki.public_wiki.pk}) private_url1 = reverse('wiki-history-detail', kwargs={"pk": data_wiki.private_wiki1.pk}) @@ -194,3 +841,189 @@ def test_wiki_history_retrieve(client, data, data_wiki): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_wiki_action_edit_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_action_delete_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_wiki.public_history_entry.delete_comment_date = None + data_wiki.public_history_entry.delete_comment_user = None + data_wiki.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry1.delete_comment_date = None + data_wiki.private_history_entry1.delete_comment_user = None + data_wiki.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry2.delete_comment_date = None + data_wiki.private_history_entry2.delete_comment_user = None + data_wiki.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_wiki_action_undelete_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_wiki.public_history_entry.delete_comment_date = timezone.now() + data_wiki.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry1.delete_comment_date = timezone.now() + data_wiki.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry2.delete_comment_date = timezone.now() + data_wiki.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_wiki_action_comment_versions(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index 01469c0a..5d936c0d 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -17,6 +17,8 @@ # along with this program. If not, see . import pytest +import datetime + from unittest.mock import patch from django.core.urlresolvers import reverse @@ -235,3 +237,84 @@ def test_delete_comment_by_project_owner(client): url = "%s?id=%s" % (url, history_entry.id) response = client.post(url, content_type="application/json") assert 200 == response.status_code, response.status_code + + +def test_edit_comment(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing", + key=key, + diff={}, + user={"pk": project.owner.id}) + + history_entry_created_at = history_entry.created_at + assert history_entry.comment_versions == None + assert history_entry.edit_comment_date == None + + client.login(project.owner) + url = reverse("userstory-history-edit-comment", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + data = json.dumps({"comment": "testing update comment"}) + response = client.post(url, data, content_type="application/json") + assert 200 == response.status_code, response.status_code + + + history_entry = HistoryEntry.objects.get(id=history_entry.id) + assert len(history_entry.comment_versions) == 1 + assert history_entry.comment == "testing update comment" + assert history_entry.comment_versions[0]["comment"] == "testing" + assert history_entry.edit_comment_date != None + assert history_entry.comment_versions[0]["user"]["id"] == project.owner.id + + +def test_get_comment_versions(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create( + type=HistoryType.change, + comment="testing", + key=key, + diff={}, + user={"pk": project.owner.id}, + edit_comment_date=datetime.datetime.now(), + comment_versions = [{ + "comment_html": "

test

", + "date": "2016-05-09T09:34:27.221Z", + "comment": "test", + "user": { + "id": project.owner.id, + }}]) + + client.login(project.owner) + url = reverse("userstory-history-comment-versions", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + response = client.get(url, content_type="application/json") + assert 200 == response.status_code, response.status_code + assert response.data[0]["user"]["username"] == project.owner.username + + +def test_get_comment_versions_from_history_entry_without_comment(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create( + type=HistoryType.change, + key=key, + diff={}, + user={"pk": project.owner.id}) + + client.login(project.owner) + url = reverse("userstory-history-comment-versions", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + response = client.get(url, content_type="application/json") + assert 200 == response.status_code, response.status_code + assert response.data == None