From 09eced41a01dd36cff9d2f5b067518c962c6c9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 9 May 2014 11:05:09 +0200 Subject: [PATCH] Update notifications to use the new history module --- taiga/base/api.py | 98 --------------- taiga/base/decorators.py | 27 ---- taiga/base/notifications/api.py | 51 -------- taiga/base/serializers.py | 64 ---------- taiga/front/__init__.py | 1 + taiga/projects/admin.py | 2 +- taiga/projects/issues/admin.py | 4 +- taiga/projects/issues/api.py | 16 +-- taiga/projects/issues/models.py | 5 - taiga/projects/issues/serializers.py | 2 +- .../update_issue_notification-body-html.jinja | 20 +-- .../update_issue_notification-body-text.jinja | 4 +- taiga/projects/milestones/admin.py | 3 +- taiga/projects/milestones/api.py | 2 +- taiga/projects/milestones/models.py | 7 +- taiga/projects/milestones/serializers.py | 1 - ...ate_milestone_notification-body-html.jinja | 20 +-- ...ate_milestone_notification-body-text.jinja | 4 +- taiga/projects/mixins/notifications.py | 116 ++++++++++++++++++ .../projects/mixins/notifications/__init__.py | 0 .../mixins/notifications/serializers.py | 42 ------- taiga/projects/models.py | 3 - .../notifications/__init__.py | 0 .../notifications/models.py | 75 +---------- .../notifications/services.py | 0 taiga/projects/tasks/admin.py | 2 +- taiga/projects/tasks/api.py | 29 +---- taiga/projects/tasks/models.py | 8 +- taiga/projects/tasks/services.py | 41 +++++++ .../update_task_notification-body-html.jinja | 20 +-- .../update_task_notification-body-text.jinja | 4 +- taiga/projects/tasks/tests/tests_api.py | 20 --- ...pdate_project_notification-body-html.jinja | 20 +-- ...pdate_project_notification-body-text.jinja | 4 +- taiga/projects/userstories/admin.py | 4 +- taiga/projects/userstories/api.py | 30 +++-- taiga/projects/userstories/models.py | 7 +- taiga/projects/userstories/services.py | 8 +- ...ate_userstory_notification-body-html.jinja | 20 +-- ...ate_userstory_notification-body-text.jinja | 4 +- taiga/projects/userstories/tests/tests_api.py | 5 - taiga/projects/wiki/admin.py | 3 +- taiga/projects/wiki/api.py | 69 +++++------ taiga/projects/wiki/models.py | 5 - .../create_wiki_notification-body-html.jinja | 21 ++++ .../create_wiki_notification-body-text.jinja | 8 ++ .../create_wiki_notification-subject.jinja | 1 + .../destroy_wiki_notification-body-html.jinja | 13 ++ .../destroy_wiki_notification-body-text.jinja | 3 + .../destroy_wiki_notification-subject.jinja | 1 + .../update_wiki_notification-body-html.jinja | 28 +++++ .../update_wiki_notification-body-text.jinja | 15 +++ .../update_wiki_notification-subject.jinja | 1 + taiga/users/models.py | 2 +- 54 files changed, 345 insertions(+), 618 deletions(-) delete mode 100644 taiga/base/notifications/api.py create mode 100644 taiga/projects/mixins/notifications.py delete mode 100644 taiga/projects/mixins/notifications/__init__.py delete mode 100644 taiga/projects/mixins/notifications/serializers.py rename taiga/{base => projects}/notifications/__init__.py (100%) rename taiga/{base => projects}/notifications/models.py (57%) rename taiga/{base => projects}/notifications/services.py (100%) create mode 100644 taiga/projects/tasks/services.py create mode 100644 taiga/projects/wiki/templates/emails/create_wiki_notification-body-html.jinja create mode 100644 taiga/projects/wiki/templates/emails/create_wiki_notification-body-text.jinja create mode 100644 taiga/projects/wiki/templates/emails/create_wiki_notification-subject.jinja create mode 100644 taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-html.jinja create mode 100644 taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-text.jinja create mode 100644 taiga/projects/wiki/templates/emails/destroy_wiki_notification-subject.jinja create mode 100644 taiga/projects/wiki/templates/emails/update_wiki_notification-body-html.jinja create mode 100644 taiga/projects/wiki/templates/emails/update_wiki_notification-body-text.jinja create mode 100644 taiga/projects/wiki/templates/emails/update_wiki_notification-subject.jinja diff --git a/taiga/base/api.py b/taiga/base/api.py index 87f1f7ba..572af4cc 100644 --- a/taiga/base/api.py +++ b/taiga/base/api.py @@ -15,23 +15,15 @@ # along with this program. If not, see . -import reversion - from django.db import transaction from rest_framework import viewsets from rest_framework import status from rest_framework import mixins -from rest_framework import decorators as rf_decorators from rest_framework.response import Response -from reversion.revisions import revision_context_manager -from reversion.models import Version - - from . import pagination from . import serializers -from . import decorators # Transactional version of rest framework mixins. @@ -118,98 +110,9 @@ class DetailAndListSerializersMixin(object): return super().get_serializer_class() -class ReversionMixin(object): - historical_model = Version - historical_serializer_class = serializers.VersionSerializer - historical_paginate_by = 5 - - def get_historical_queryset(self): - return reversion.get_unique_for_object(self.get_object()) - - def get_historical_serializer_class(self): - serializer_class = self.historical_serializer_class - if serializer_class is not None: - return serializer_class - - assert self.historical_model is not None, \ - "'%s' should either include a 'serializer_class' attribute, " \ - "or use the 'model' attribute as a shortcut for " \ - "automatically generating a serializer class." \ - % self.__class__.__name__ - - class DefaultSerializer(self.model_serializer_class): - class Meta: - model = self.historical_model - return DefaultSerializer - - def get_historical_serializer(self, instance=None, data=None, files=None, - many=False, partial=False): - serializer_class = self.get_historical_serializer_class() - return serializer_class(instance, data=data, files=files, - many=many, partial=partial) - - def get_historical_pagination_serializer(self, page): - return self.get_historical_serializer(page.object_list, many=True) - - @rf_decorators.link() - @decorators.change_instance_attr("paginate_by", historical_paginate_by) - def historical(self, request, *args, **kwargs): - obj = self.get_object() - - self.historical_object_list = self.get_historical_queryset() - - # Switch between paginated or standard style responses - page = self.paginate_queryset(self.historical_object_list) - if page is not None: - serializer = self.get_historical_pagination_serializer(page) - else: - serializer = self.get_historical_serializer(self.historical_object_list, - many=True) - - return Response(serializer.data) - - @rf_decorators.action() - def restore(self, request, *args, **kwargs): - vpk = request.QUERY_PARAMS.get("version", None) - if not vpk: - return Response(status=status.HTTP_404_NOT_FOUND) - - try: - version = reversion.get_for_object(self.get_object()).get(pk=vpk) - version.revision.revert(delete=True) - - serializer = self.get_serializer(self.get_object()) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_202_ACCEPTED, - headers=headers) - except Version.DoesNotExist: - return Response(status=status.HTTP_400_BAD_REQUEST) - - def dispatch(self, request, *args, **kwargs): - revision_context_manager.start() - - try: - response = super().dispatch(request, *args, **kwargs) - except Exception as e: - revision_context_manager.invalidate() - revision_context_manager.end() - raise - - if self.request.user.is_authenticated(): - revision_context_manager.set_user(self.request.user) - - if response.status_code > 206: - revision_context_manager.invalidate() - - revision_context_manager.end() - return response - - - # Own subclasses of django rest framework viewsets class ModelCrudViewSet(DetailAndListSerializersMixin, - ReversionMixin, PreconditionMixin, pagination.HeadersPaginationMixin, pagination.ConditionalPaginationMixin, @@ -223,7 +126,6 @@ class ModelCrudViewSet(DetailAndListSerializersMixin, class ModelListViewSet(DetailAndListSerializersMixin, - ReversionMixin, PreconditionMixin, pagination.HeadersPaginationMixin, pagination.ConditionalPaginationMixin, diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py index e143efca..20f27a15 100644 --- a/taiga/base/decorators.py +++ b/taiga/base/decorators.py @@ -14,35 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from functools import wraps import warnings -def change_instance_attr(name, new_value): - """ - Change the attribute value temporarily for a new one. If it raise an AttributeError (if the - instance hasm't the attribute) the attribute will not be changed. - """ - def change_instance_attr(fn): - @wraps(fn) - def wrapper(instance, *args, **kwargs): - try: - old_value = instance.__getattribute__(name) - changed = True - except AttributeError: - changed = False - - if changed: - instance.__setattr__(name, new_value) - - ret = fn(instance, *args, **kwargs) - - if changed: - instance.__setattr__(name, old_value) - - return ret - return wrapper - return change_instance_attr - ## Rest Framework 2.4 backport some decorators. diff --git a/taiga/base/notifications/api.py b/taiga/base/notifications/api.py deleted file mode 100644 index 3342bbaa..00000000 --- a/taiga/base/notifications/api.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from . import services - - -class NotificationSenderMixin(object): - create_notification_template = None - update_notification_template = None - destroy_notification_template = None - notification_service = services.NotificationService() - - def _post_save_notification_sender(self, obj, created=False): - users = obj.get_watchers_to_notify(self.request.user) - comment = self.request.DATA.get("comment", None) - context = {'changer': self.request.user, "comment": comment, 'object': obj} - - if created: - self.notification_service.send_notification_email(self.create_notification_template, - users=users, context=context) - else: - context["changed_fields"] = obj.get_changed_fields_list(self.request.DATA) - self.notification_service.send_notification_email(self.update_notification_template, - users=users, context=context) - - def post_save(self, obj, created=False): - super().post_save(obj, created) - self._post_save_notification_sender(obj, created) - - def destroy(self, request, *args, **kwargs): - obj = self.get_object() - users = obj.get_watchers_to_notify(self.request.user) - - context = {'changer': self.request.user, 'object': obj} - self.notification_service.send_notification_email(self.destroy_notification_template, - users=users, context=context) - - return super().destroy(request, *args, **kwargs) diff --git a/taiga/base/serializers.py b/taiga/base/serializers.py index 0ed59b4d..8e6d72b7 100644 --- a/taiga/base/serializers.py +++ b/taiga/base/serializers.py @@ -16,9 +16,6 @@ from rest_framework import serializers -from reversion.models import Version -import reversion - from taiga.domains.base import get_active_domain from taiga.domains.models import Domain @@ -57,67 +54,6 @@ class AutoDomainField(serializers.WritableField): domain = get_active_domain() return domain -class VersionSerializer(serializers.ModelSerializer): - created_date = serializers.SerializerMethodField("get_created_date") - content_type = serializers.SerializerMethodField("get_content_type") - object_id = serializers.SerializerMethodField("get_object_id") - user = serializers.SerializerMethodField("get_user") - comment = serializers.SerializerMethodField("get_comment") - fields = serializers.SerializerMethodField("get_object_fields") - changed_fields = serializers.SerializerMethodField("get_changed_fields") - - class Meta: - model = Version - fields = ("id", "created_date", "content_type", "object_id", "user", "comment", - "fields", "changed_fields") - read_only = fields - - def get_created_date(self, obj): - return obj.revision.date_created - - def get_content_type(self, obj): - return obj.content_type.model - - def get_object_id(self, obj): - return obj.object_id_int - - def get_object_fields(self, obj): - return obj.field_dict - - def get_user(self, obj): - return obj.revision.user.id if obj.revision.user else None - - def get_comment(self, obj): - return obj.revision.comment - - def get_object_old_fields(self, obj): - versions = reversion.get_unique_for_object(obj.object) - try: - return versions[versions.index(obj) + 1].field_dict - except IndexError: - return {} - - def get_changed_fields(self, obj): - new_fields = self.get_object_fields(obj) - old_fields = self.get_object_old_fields(obj) - - changed_fields = {} - for key in new_fields.keys() | old_fields.keys(): - if key == "modified_date": - continue - - if old_fields.get(key, "") == new_fields.get(key, ""): - continue - - changed_fields[key] = { - "name": obj.object.__class__._meta.get_field_by_name( - key)[0].verbose_name, - "old": old_fields.get(key, None), - "new": new_fields.get(key, None), - } - - return changed_fields - class NeighborsSerializerMixin: diff --git a/taiga/front/__init__.py b/taiga/front/__init__.py index 85bcc02f..267fb543 100644 --- a/taiga/front/__init__.py +++ b/taiga/front/__init__.py @@ -27,6 +27,7 @@ URLS = { "project-admin": "/#/project/{0}/admin", "change-password": "/#/change-password/{0}", "invitation": "/#/invitation/{0}", + "wiki": "/#/project/{0}/wiki/{1}" } diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 4d501bda..dda77a44 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -47,7 +47,7 @@ class MembershipInline(admin.TabularInline): extra = 0 -class ProjectAdmin(reversion.VersionAdmin): +class ProjectAdmin(admin.ModelAdmin): list_display = ["name", "owner", "created_date", "total_milestones", "total_story_points", "domain"] list_display_links = list_display diff --git a/taiga/projects/issues/admin.py b/taiga/projects/issues/admin.py index b0750268..562fbe09 100644 --- a/taiga/projects/issues/admin.py +++ b/taiga/projects/issues/admin.py @@ -20,10 +20,8 @@ from taiga.projects.admin import AttachmentInline from . import models -import reversion - -class IssueAdmin(reversion.VersionAdmin): +class IssueAdmin(admin.ModelAdmin): list_display = ["project", "milestone", "ref", "subject",] list_display_links = ["ref", "subject",] inlines = [AttachmentInline] diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 0ed0de15..ffea1077 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import reversion -from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ from django.db.models import Q @@ -27,11 +25,12 @@ from rest_framework import filters from taiga.base import filters from taiga.base import exceptions as exc from taiga.base.decorators import list_route -from taiga.base.api import ModelCrudViewSet, NeighborsApiMixin -from taiga.base.notifications.api import NotificationSenderMixin from taiga.projects.permissions import AttachmentPermission from taiga.projects.serializers import AttachmentSerializer from taiga.projects.models import Attachment +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import NeighborsApiMixin +from taiga.projects.mixins.notifications import NotificationSenderMixin from . import models from . import permissions @@ -142,15 +141,6 @@ class IssueViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudViewSet) if obj.type and obj.type.project != obj.project: raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) - - def post_save(self, obj, created=False): - with reversion.create_revision(): - if "comment" in self.request.DATA: - # Update the comment in the last version - reversion.set_comment(self.request.DATA["comment"]) - super().post_save(obj, created) - - class IssueAttachmentViewSet(ModelCrudViewSet): model = Attachment serializer_class = AttachmentSerializer diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index c6d406fe..e79e4499 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -28,7 +28,6 @@ from taiga.base.utils.slug import ref_uniquely from taiga.base.notifications.models import WatchedMixin from taiga.projects.mixins.blocked.models import BlockedMixin -import reversion class Issue(NeighborsMixin, WatchedMixin, BlockedMixin): @@ -276,10 +275,6 @@ class Issue(NeighborsMixin, WatchedMixin, BlockedMixin): } -# Reversion registration (usufull for base.notification and for meke a historical) -reversion.register(Issue) - - # Model related signals handlers @receiver(models.signals.pre_save, sender=Issue, dispatch_uid="issue_finished_date_handler") def issue_finished_date_handler(sender, instance, **kwargs): diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index ffd748bb..ed29c340 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -18,7 +18,7 @@ from rest_framework import serializers from taiga.base.serializers import PickleField, NeighborsSerializerMixin from taiga.projects.serializers import AttachmentSerializer -from taiga.projects.mixins.notifications.serializers import WatcherValidationSerializerMixin +from taiga.projects.mixins.notifications import WatcherValidationSerializerMixin from . import models diff --git a/taiga/projects/issues/templates/emails/update_issue_notification-body-html.jinja b/taiga/projects/issues/templates/emails/update_issue_notification-body-html.jinja index 096b8300..96d47378 100644 --- a/taiga/projects/issues/templates/emails/update_issue_notification-body-html.jinja +++ b/taiga/projects/issues/templates/emails/update_issue_notification-body-html.jinja @@ -14,24 +14,8 @@

Comment {{ comment|linebreaksbr }}

{% endif %} {% if changed_fields %} -

Updated fields: -

- {% for field in changed_fields %} -
- {{ field.verbose_name}} -
- {% if field.new_value != None or field.new_value != "" %} -
- to: {{ field.new_value|linebreaksbr }} -
- {% endif %} - {% if field.old_value != None or field.old_value != "" %} -
- from: {{ field.old_value|linebreaksbr }} -
- {% endif %} - {% endfor %} -
+

Updated fields:

+ {% include "emails/includes/fields_diff-html.jinja" %} {% endif %} diff --git a/taiga/projects/issues/templates/emails/update_issue_notification-body-text.jinja b/taiga/projects/issues/templates/emails/update_issue_notification-body-text.jinja index 5f68badb..a3dd43ba 100644 --- a/taiga/projects/issues/templates/emails/update_issue_notification-body-text.jinja +++ b/taiga/projects/issues/templates/emails/update_issue_notification-body-text.jinja @@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }} {% endif %} {% if changed_fields %} - Updated fields: - {% for field in changed_fields %} - * {{ field.verbose_name}}: from '{{ field.old_value}}' to '{{ field.new_value }}'. - {% endfor %} + {% include "emails/includes/fields_diff-text.jinja" %} {% endif %} ** More info at {{ final_url_name }} ({{ final_url }}) ** diff --git a/taiga/projects/milestones/admin.py b/taiga/projects/milestones/admin.py index 3d2f096e..b741a1b2 100644 --- a/taiga/projects/milestones/admin.py +++ b/taiga/projects/milestones/admin.py @@ -15,7 +15,6 @@ # along with this program. If not, see . from django.contrib import admin -import reversion from . import models @@ -25,7 +24,7 @@ class MilestoneInline(admin.TabularInline): extra = 0 -class MilestoneAdmin(reversion.VersionAdmin): +class MilestoneAdmin(admin.ModelAdmin): list_display = ["name", "project", "owner", "closed", "estimated_start", "estimated_finish"] list_display_links = list_display diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 4a407ef4..b9dcb599 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -24,7 +24,7 @@ from taiga.base import filters from taiga.base import exceptions as exc from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet -from taiga.base.notifications.api import NotificationSenderMixin +from taiga.projects.mixins.notifications import NotificationSenderMixin from . import serializers from . import models diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index 9c98135f..64651ada 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -20,11 +20,10 @@ from django.utils.translation import ugettext_lazy as _ from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.dicts import dict_sum -from taiga.base.notifications.models import WatchedMixin +from taiga.projects.notifications.models import WatchedMixin from taiga.projects.userstories.models import UserStory -import reversion import itertools import datetime @@ -146,7 +145,3 @@ class Milestone(WatchedMixin, models.Model): finish_date__lt=date + datetime.timedelta(days=1) ) if us.is_closed ]) - - -# Reversion registration (usufull for base.notification and for meke a historical) -reversion.register(Milestone) diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 8ff7547f..c82d90b9 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -15,7 +15,6 @@ # along with this program. If not, see . import json -import reversion from rest_framework import serializers diff --git a/taiga/projects/milestones/templates/emails/update_milestone_notification-body-html.jinja b/taiga/projects/milestones/templates/emails/update_milestone_notification-body-html.jinja index e5e80926..ea456b34 100644 --- a/taiga/projects/milestones/templates/emails/update_milestone_notification-body-html.jinja +++ b/taiga/projects/milestones/templates/emails/update_milestone_notification-body-html.jinja @@ -14,24 +14,8 @@

Comment {{ comment|linebreaksbr }}

{% endif %} {% if changed_fields %} -

Updated fields: -

- {% for field in changed_fields %} -
- {{ field.verbose_name}} -
- {% if field.new_value != None or field.new_value != "" %} -
- to: {{ field.new_value|linebreaksbr }} -
- {% endif %} - {% if field.old_value != None or field.old_value != "" %} -
- from: {{ field.old_value|linebreaksbr }} -
- {% endif %} - {% endfor %} -
+

Updated fields:

+ {% include "emails/includes/fields_diff-html.jinja" %} {% endif %} diff --git a/taiga/projects/milestones/templates/emails/update_milestone_notification-body-text.jinja b/taiga/projects/milestones/templates/emails/update_milestone_notification-body-text.jinja index 5f41acbf..c2f667d5 100644 --- a/taiga/projects/milestones/templates/emails/update_milestone_notification-body-text.jinja +++ b/taiga/projects/milestones/templates/emails/update_milestone_notification-body-text.jinja @@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }} {% endif %} {% if changed_fields %} - Updated fields: - {% for field in changed_fields %} - * {{ field.verbose_name}}: from '{{ field.old_value}}' to '{{ field.new_value }}'. - {% endfor %} + {% include "emails/includes/fields_diff-text.jinja" %} {% endif %} ** More info at {{ final_url_name }} ({{ final_url }}) ** diff --git a/taiga/projects/mixins/notifications.py b/taiga/projects/mixins/notifications.py new file mode 100644 index 00000000..ef588cec --- /dev/null +++ b/taiga/projects/mixins/notifications.py @@ -0,0 +1,116 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +####### +# API # +####### + +from taiga.projects.notifications import services +from taiga.projects.history.services import take_snapshot +from taiga.projects.history.models import HistoryType + + +class NotificationSenderMixin(object): + create_notification_template = None + update_notification_template = None + destroy_notification_template = None + notification_service = services.NotificationService() + + def _get_object_for_snapshot(self, obj): + return obj + + def _post_save_notification_sender(self, obj, history): + users = obj.get_watchers_to_notify(history.owner) + context = { + "object": obj, + "changer": history.owner, + "comment": history.comment, + "changed_fields": history.values_diff + } + + if history.type == HistoryType.create: + self.notification_service.send_notification_email(self.create_notification_template, + users=users, context=context) + else: + self.notification_service.send_notification_email(self.update_notification_template, + users=users, context=context) + + def post_save(self, obj, created=False): + super().post_save(obj, created) + + user = self.request.user + comment = self.request.DATA.get("comment", "") + + obj = self._get_object_for_snapshot(obj) + history = take_snapshot(obj, comment=comment, user=user) + + if history: + self._post_save_notification_sender(obj, history) + + def pre_destroy(self, obj): + obj = self._get_object_for_snapshot(obj) + users = obj.get_watchers_to_notify(self.request.user) + + context = { + "object": obj, + "changer": self.request.user + } + self.notification_service.send_notification_email(self.destroy_notification_template, + users=users, context=context) + def post_destroy(self, obj): + pass + + def destroy(self, request, *args, **kwargs): + obj = self.get_object() + + self.pre_destroy(obj) + result = super().destroy(request, *args, **kwargs) + self.post_destroy(obj) + + return result + + +################ +# SERIEALIZERS # +################ + +from django.db.models.loading import get_model + +from rest_framework import serializers + + +class WatcherValidationSerializerMixin(object): + def validate_watchers(self, attrs, source): + values = set(attrs.get(source, [])) + if values: + project = None + if "project" in attrs and attrs["project"]: + if self.object and attrs["project"] == self.object.project.id: + project = self.object.project + else: + project_model = get_model("projects", "Project") + try: + project = project_model.objects.get(project__id=attrs["project"]) + except project_model.DoesNotExist: + pass + elif self.object: + project = self.object.project + + if len(values) != get_model("projects", "Membership").objects.filter(project=project, + user__in=values).count(): + raise serializers.ValidationError("Error, some watcher user is not a member of the project") + return attrs diff --git a/taiga/projects/mixins/notifications/__init__.py b/taiga/projects/mixins/notifications/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/taiga/projects/mixins/notifications/serializers.py b/taiga/projects/mixins/notifications/serializers.py deleted file mode 100644 index c3a264e5..00000000 --- a/taiga/projects/mixins/notifications/serializers.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from django.db.models.loading import get_model - -from rest_framework import serializers - - -class WatcherValidationSerializerMixin(object): - def validate_watchers(self, attrs, source): - values = set(attrs.get(source, [])) - if values: - project = None - if "project" in attrs and attrs["project"]: - if self.object and attrs["project"] == self.object.project.id: - project = self.object.project - else: - project_model = get_model("projects", "Project") - try: - project = project_model.objects.get(project__id=attrs["project"]) - except project_model.DoesNotExist: - pass - elif self.object: - project = self.object.project - - if len(values) != get_model("projects", "Membership").objects.filter(project=project, - user__in=values).count(): - raise serializers.ValidationError("Error, some watcher user is not a member of the project") - return attrs diff --git a/taiga/projects/models.py b/taiga/projects/models.py index c7992bdd..89bd3585 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -749,9 +749,6 @@ class ProjectTemplate(models.Model): return project -# Reversion registration (usufull for base.notification and for meke a historical) -reversion.register(Project) -reversion.register(Attachment) # On membership object is created/changed, update # role-points relation. diff --git a/taiga/base/notifications/__init__.py b/taiga/projects/notifications/__init__.py similarity index 100% rename from taiga/base/notifications/__init__.py rename to taiga/projects/notifications/__init__.py diff --git a/taiga/base/notifications/models.py b/taiga/projects/notifications/models.py similarity index 57% rename from taiga/base/notifications/models.py rename to taiga/projects/notifications/models.py index 14a3d414..f5fba223 100644 --- a/taiga/base/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -15,10 +15,8 @@ # along with this program. If not, see . from django.db import models -from django.db.models.fields import FieldDoesNotExist from django.utils.translation import ugettext_lazy as _ -import reversion class WatcherMixin(models.Model): @@ -63,38 +61,8 @@ class WatcherMixin(models.Model): def allow_notify_project(self, project): return self.notify_level == "all_owned_projects" -class WatchedMixin(models.Model): - notifiable_fields = [] - - class Meta: - abstract = True - - @property - def last_version(self): - version_list = reversion.get_for_object(self) - return version_list and version_list[0] or None - - def get_changed_fields_list(self, data_dict): - def _key_by_notifiable_field(item): - try: - return self.notifiable_fields.index(item["name"]) - except ValueError: - return 100000 # Emulate the maximum value - - if self.notifiable_fields: - changed_data = {k:v for k, v in data_dict.items() - if k in self.notifiable_fields} - else: - changed_data = data_dict - - fields_list = [] - for field_name, data_value in changed_data.items(): - field_dict = self._get_changed_field(field_name, data_value) - if field_dict["old_value"] != field_dict["new_value"]: - fields_list.append(field_dict) - - return sorted(fields_list, key=_key_by_notifiable_field) +class WatchedMixin(object): def get_watchers_to_notify(self, changer): watchers_to_notify = set() watchers_by_role = self._get_watchers_by_role() @@ -127,47 +95,6 @@ class WatchedMixin(models.Model): return watchers_to_notify - def _get_changed_field_verbose_name(self, field_name): - try: - return self._meta.get_field(field_name).verbose_name - except FieldDoesNotExist: - return field_name - - def _get_changed_field_old_value(self, field_name, data_value): - value = (self.last_version.field_dict.get(field_name, data_value) - if self.last_version else None) - field = self.__class__._meta.get_field_by_name(field_name)[0] or None - - if value and field: - # Get the old value from a ForeignKey - if type(field) is models.fields.related.ForeignKey: - try: - value = field.related.parent_model.objects.get(pk=value) - except field.related.parent_model.DoesNotExist: - pass - - display_method = getattr(self,"get_notifiable_{field_name}_display".format( - field_name=field_name) ,None) - return display_method(value) if display_method else value - - def _get_changed_field_new_value(self, field_name, data_value): - value = getattr(self, field_name, data_value) - display_method = getattr(self,"get_notifiable_{field_name}_display".format( - field_name=field_name) ,None) - return display_method(value) if display_method else value - - def _get_changed_field(self, field_name, data_value): - verbose_name = self._get_changed_field_verbose_name(field_name) - old_value = self._get_changed_field_old_value(field_name, None) - new_value = self._get_changed_field_new_value(field_name, data_value) - - return { - "name": field_name, - "verbose_name": verbose_name, - "old_value": old_value, - "new_value": new_value, - } - def _get_watchers_by_role(self): """ Return the actual instances of watchers of this object, classified by role. diff --git a/taiga/base/notifications/services.py b/taiga/projects/notifications/services.py similarity index 100% rename from taiga/base/notifications/services.py rename to taiga/projects/notifications/services.py diff --git a/taiga/projects/tasks/admin.py b/taiga/projects/tasks/admin.py index c0e7fc08..5fb79345 100644 --- a/taiga/projects/tasks/admin.py +++ b/taiga/projects/tasks/admin.py @@ -21,7 +21,7 @@ from taiga.projects.admin import AttachmentInline from . import models -class TaskAdmin(reversion.VersionAdmin): +class TaskAdmin(admin.ModelAdmin): list_display = ["project", "milestone", "user_story", "ref", "subject",] list_display_links = ["ref", "subject",] list_filter = ["project"] diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 91364061..752ac9c5 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -27,19 +27,15 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.permissions import has_project_perm from taiga.base.api import ModelCrudViewSet -from taiga.base.notifications.api import NotificationSenderMixin from taiga.projects.permissions import AttachmentPermission from taiga.projects.serializers import AttachmentSerializer from taiga.projects.models import Attachment, Project +from taiga.projects.mixins.notifications import NotificationSenderMixin from taiga.projects.userstories.models import UserStory from . import models from . import permissions from . import serializers - -import reversion - - class TaskAttachmentViewSet(ModelCrudViewSet): model = Attachment serializer_class = AttachmentSerializer @@ -66,6 +62,7 @@ class TaskAttachmentViewSet(ModelCrudViewSet): obj.project.memberships.filter(user=self.request.user).count() == 0): raise exc.PermissionDenied(_("You don't have permissions for add " "attachments to this task.")) +from . import services class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet): @@ -102,13 +99,6 @@ class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet): if obj.status and obj.status.project != obj.project: raise exc.PermissionDenied(_("You don't have permissions for add/modify this task.")) - def post_save(self, obj, created=False): - with reversion.create_revision(): - if "comment" in self.request.DATA: - # Update the comment in the last version - reversion.set_comment(self.request.DATA["comment"]) - super().post_save(obj, created) - @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): bulk_tasks = request.DATA.get('bulkTasks', None) @@ -124,21 +114,14 @@ class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet): raise exc.BadRequest(_('usId parameter is mandatory')) project = get_object_or_404(Project, id=project_id) - us = get_object_or_404(UserStory, id=us_id) + user_story = get_object_or_404(UserStory, id=us_id) if request.user != project.owner and not has_project_perm(request.user, project, 'add_task'): raise exc.PermissionDenied(_("You don't have permisions to create tasks.")) - items = filter(lambda s: len(s) > 0, - map(lambda s: s.strip(), bulk_tasks.split("\n"))) - - tasks = [] - for item in items: - obj = models.Task.objects.create(subject=item, project=project, - user_story=us, owner=request.user, - status=project.default_task_status) - tasks.append(obj) - self._post_save_notification_sender(obj, True) + service = services.TasksService() + tasks = service.bulk_insert(project, request.user, user_story, bulk_tasks, + callback_on_success=self.post_save) tasks_serialized = self.serializer_class(tasks, many=True) return Response(data=tasks_serialized.data) diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index adeb015a..b6e3566f 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -24,7 +24,7 @@ from django.utils.translation import ugettext_lazy as _ from picklefield.fields import PickledObjectField from taiga.base.utils.slug import ref_uniquely -from taiga.base.notifications.models import WatchedMixin +from taiga.projects.notifications.models import WatchedMixin from taiga.projects.userstories.models import UserStory from taiga.projects.milestones.models import Milestone from taiga.projects.mixins.blocked.models import BlockedMixin @@ -34,7 +34,7 @@ import reversion class Task(WatchedMixin, BlockedMixin): user_story = models.ForeignKey("userstories.UserStory", null=True, blank=True, - related_name="tasks", verbose_name=_("user story")) + related_name="tasks", verbose_name=_("user story")) ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, verbose_name=_("ref")) owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None, @@ -109,10 +109,6 @@ class Task(WatchedMixin, BlockedMixin): } -# Reversion registration (usufull for base.notification and for meke a historical) -reversion.register(Task) - - # Model related signals handlers @receiver(models.signals.pre_save, sender=Task, dispatch_uid="task_ref_handler") def task_ref_handler(sender, instance, **kwargs): diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py new file mode 100644 index 00000000..75f51fe0 --- /dev/null +++ b/taiga/projects/tasks/services.py @@ -0,0 +1,41 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.db import transaction +from django.db import connection + +from . import models + + +class TasksService(object): + @transaction.atomic + def bulk_insert(self, project, user, user_story, data, callback_on_success=None): + tasks = [] + + items = filter(lambda s: len(s) > 0, + map(lambda s: s.strip(), data.split("\n"))) + + for item in items: + obj = models.Task.objects.create(subject=item, project=project, + user_story=user_story, owner=user, + status=project.default_task_status) + tasks.append(obj) + + if callback_on_success: + callback_on_success(obj, True) + + return tasks + diff --git a/taiga/projects/tasks/templates/emails/update_task_notification-body-html.jinja b/taiga/projects/tasks/templates/emails/update_task_notification-body-html.jinja index 6a0d9bc6..c6e281a4 100644 --- a/taiga/projects/tasks/templates/emails/update_task_notification-body-html.jinja +++ b/taiga/projects/tasks/templates/emails/update_task_notification-body-html.jinja @@ -14,24 +14,8 @@

Comment {{ comment|linebreaksbr }}

{% endif %} {% if changed_fields %} -

Updated fields: -

- {% for field in changed_fields %} -
- {{ field.verbose_name}} -
- {% if field.new_value != None or field.new_value != "" %} -
- to: {{ field.new_value|linebreaksbr }} -
- {% endif %} - {% if field.old_value != None or field.old_value != "" %} -
- from: {{ field.old_value|linebreaksbr }} -
- {% endif %} - {% endfor %} -
+

Updated fields:

+ {% include "emails/includes/fields_diff-html.jinja" %} {% endif %} diff --git a/taiga/projects/tasks/templates/emails/update_task_notification-body-text.jinja b/taiga/projects/tasks/templates/emails/update_task_notification-body-text.jinja index c8181cfa..771d1d94 100644 --- a/taiga/projects/tasks/templates/emails/update_task_notification-body-text.jinja +++ b/taiga/projects/tasks/templates/emails/update_task_notification-body-text.jinja @@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }} {% endif %} {% if changed_fields %} - Updated fields: - {% for field in changed_fields %} - * {{ field.verbose_name}}: from '{{ field.old_value}}' to '{{ field.new_value }}'. - {% endfor %} + {% include "emails/includes/fields_diff-text.jinja" %} {% endif %} ** More info at {{ final_url_name }} ({{ final_url }}) ** diff --git a/taiga/projects/tasks/tests/tests_api.py b/taiga/projects/tasks/tests/tests_api.py index 3ee56c6e..da80ae04 100644 --- a/taiga/projects/tasks/tests/tests_api.py +++ b/taiga/projects/tasks/tests/tests_api.py @@ -20,8 +20,6 @@ from django import test from django.core import mail from django.core.urlresolvers import reverse -import reversion - from taiga.users.tests import create_user from taiga.projects.tests import create_project, add_membership from taiga.projects.milestones.tests import create_milestone @@ -111,24 +109,6 @@ class TasksTestCase(test.TestCase): response = self.client.get(reverse("tasks-detail", args=(self.task1.id,))) self.assertEqual(response.status_code, 401) - def test_view_task_by_project_owner(self): - response = self.client.login(username=self.user1.username, - password=self.user1.username) - self.assertTrue(response) - - # Change task for generate history/diff. - with reversion.create_revision(): - self.task1.tags = ["LL"] - self.task1.save() - - with reversion.create_revision(): - self.task1.tags = ["LLKK"] - self.task1.save() - - response = self.client.get(reverse("tasks-detail", args=(self.task1.id,))) - self.assertEqual(response.status_code, 200) - self.client.logout() - def test_view_task_by_owner(self): response = self.client.login(username=self.user2.username, password=self.user2.username) diff --git a/taiga/projects/templates/emails/update_project_notification-body-html.jinja b/taiga/projects/templates/emails/update_project_notification-body-html.jinja index c4012623..39fe4f7e 100644 --- a/taiga/projects/templates/emails/update_project_notification-body-html.jinja +++ b/taiga/projects/templates/emails/update_project_notification-body-html.jinja @@ -13,24 +13,8 @@

Comment {{ comment|linebreaksbr }}

{% endif %} {% if changed_fields %} -

Updated fields: -

- {% for field in changed_fields %} -
- {{ field.verbose_name}} -
- {% if field.new_value != None or field.new_value != "" %} -
- to: {{ field.new_value|linebreaksbr }} -
- {% endif %} - {% if field.old_value != None or field.old_value != "" %} -
- from: {{ field.old_value|linebreaksbr }} -
- {% endif %} - {% endfor %} -
+

Updated fields:

+ {% include "emails/includes/fields_diff-html.jinja" %} {% endif %} diff --git a/taiga/projects/templates/emails/update_project_notification-body-text.jinja b/taiga/projects/templates/emails/update_project_notification-body-text.jinja index cbe39067..dfe93b9c 100644 --- a/taiga/projects/templates/emails/update_project_notification-body-text.jinja +++ b/taiga/projects/templates/emails/update_project_notification-body-text.jinja @@ -8,8 +8,8 @@ Comment: {{ comment|linebreaksbr }} {% endif %} {% if changed_fields %} - Updated fields: - {% for field in changed_fields %} - * {{ field.verbose_name}}: from '{{ field.old_value}}' to '{{ field.new_value }}'. + {% for field_name, values in changed_fields.items() %} + * {{ verbose_name(object, field_name) }}: from '{{ values.0 }}' to '{{ values.1 }}'. {% endfor %} {% endif %} diff --git a/taiga/projects/userstories/admin.py b/taiga/projects/userstories/admin.py index 99385355..3e47aafa 100644 --- a/taiga/projects/userstories/admin.py +++ b/taiga/projects/userstories/admin.py @@ -15,14 +15,12 @@ # along with this program. If not, see . from django.contrib import admin -import reversion from taiga.projects.admin import AttachmentInline from . import models - class RolePointsInline(admin.TabularInline): model = models.RolePoints sortable_field_name = 'role' @@ -39,7 +37,7 @@ class RolePointsAdmin(admin.ModelAdmin): readonly_fields = ["user_story", "role", "points"] -class UserStoryAdmin(reversion.VersionAdmin): +class UserStoryAdmin(admin.ModelAdmin): list_display = ["project", "milestone", "ref", "subject",] list_display_links = ["ref", "subject",] list_filter = ["project"] diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 7ebb0ff1..4392edf0 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import reversion - from django.db import transaction from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType @@ -27,13 +25,16 @@ from rest_framework import status from taiga.base import filters from taiga.base import exceptions as exc -from taiga.base.decorators import list_route, action +from taiga.base.decorators import list_route +from taiga.base.decorators import action from taiga.base.permissions import has_project_perm -from taiga.base.api import ModelCrudViewSet, NeighborsApiMixin -from taiga.base.notifications.api import NotificationSenderMixin from taiga.projects.permissions import AttachmentPermission from taiga.projects.serializers import AttachmentSerializer from taiga.projects.models import Attachment, Project +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import NeighborsApiMixin +from taiga.projects.mixins.notifications import NotificationSenderMixin +from taiga.projects.history.services import take_snapshot from . import models from . import permissions @@ -103,16 +104,19 @@ class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudView raise exc.PermissionDenied(_("You don't have permisions to create user stories.")) service = services.UserStoriesService() - service.bulk_insert(project, request.user, bulk_stories, - callback_on_success=self._post_save_notification_sender) + user_stories = service.bulk_insert(project, request.user, bulk_stories, + callback_on_success=self.post_save) - return Response(data=None, status=status.HTTP_204_NO_CONTENT) + user_stories_serialized = self.serializer_class(user_stories, many=True) + return Response(data=user_stories_serialized.data) @list_route(methods=["POST"]) def bulk_update_order(self, request, **kwargs): # bulkStories should be: # [[1,1],[23, 2], ...] + # TODO: Generate the histoy snaptshot when change the uss order in the backlog. + # Implement order with linked lists \o/. bulk_stories = request.DATA.get("bulkStories", None) if bulk_stories is None: @@ -138,10 +142,12 @@ class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudView # Added comment to the origin (issue) if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue: - with reversion.create_revision(): - reversion.set_comment(_("Generated the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - {subject}\")").format( - ref=self.object.ref, subject=self.object.subject)) - self.object.generated_from_issue.save() + self.object.generated_from_issue.save() + + comment = _("Generate the user story [US #{ref} - " + "{subject}](:us:{ref} \"US #{ref} - {subject}\")") + comment = comment.format(ref=self.object.ref, subject=self.object.subject) + take_snapshot(self.object.generated_from_issue, comment=comment, user=self.request.user) return response diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 659e5029..3d3b8287 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -21,12 +21,11 @@ from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from picklefield.fields import PickledObjectField -import reversion from taiga.base.models import NeighborsMixin from taiga.base.utils.slug import ref_uniquely -from taiga.base.notifications.models import WatchedMixin from taiga.projects.mixins.blocked.models import BlockedMixin +from taiga.projects.notifications.models import WatchedMixin class RolePoints(models.Model): @@ -177,10 +176,6 @@ class UserStory(NeighborsMixin, WatchedMixin, BlockedMixin, models.Model): } -# Reversion registration (usufull for base.notification and for meke a historical) -reversion.register(UserStory) - - # Model related signals handlers @receiver(models.signals.pre_save, sender=UserStory, dispatch_uid="user_story_ref_handler") def us_ref_handler(sender, instance, **kwargs): diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 28137e9a..de1693da 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -18,23 +18,29 @@ from django.db import transaction from django.db import connection from . import models -import reversion class UserStoriesService(object): @transaction.atomic def bulk_insert(self, project, user, data, callback_on_success=None): + user_stories = [] + items = filter(lambda s: len(s) > 0, map(lambda s: s.strip(), data.split("\n"))) for item in items: obj = models.UserStory.objects.create(subject=item, project=project, owner=user, status=project.default_us_status) + user_stories.append(obj) + if callback_on_success: callback_on_success(obj, True) + return user_stories + @transaction.atomic def bulk_update_order(self, project, user, data): + # TODO: Create a history snapshot of all updated USs cursor = connection.cursor() sql = """ diff --git a/taiga/projects/userstories/templates/emails/update_userstory_notification-body-html.jinja b/taiga/projects/userstories/templates/emails/update_userstory_notification-body-html.jinja index 0ba75ab6..913525ae 100644 --- a/taiga/projects/userstories/templates/emails/update_userstory_notification-body-html.jinja +++ b/taiga/projects/userstories/templates/emails/update_userstory_notification-body-html.jinja @@ -14,24 +14,8 @@

Comment {{ comment|linebreaksbr }}

{% endif %} {% if changed_fields %} -

Updated fields: -

- {% for field in changed_fields %} -
- {{ field.verbose_name}} -
- {% if field.new_value != None or field.new_value != "" %} -
- to: {{ field.new_value|linebreaksbr }} -
- {% endif %} - {% if field.old_value != None or field.old_value != "" %} -
- from: {{ field.old_value|linebreaksbr }} -
- {% endif %} - {% endfor %} -
+

Updated fields:

+ {% include "emails/includes/fields_diff-html.jinja" %} {% endif %} diff --git a/taiga/projects/userstories/templates/emails/update_userstory_notification-body-text.jinja b/taiga/projects/userstories/templates/emails/update_userstory_notification-body-text.jinja index 83a02d53..e796e9bc 100644 --- a/taiga/projects/userstories/templates/emails/update_userstory_notification-body-text.jinja +++ b/taiga/projects/userstories/templates/emails/update_userstory_notification-body-text.jinja @@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }} {% endif %} {% if changed_fields %} - Updated fields: - {% for field in changed_fields %} - * {{ field.verbose_name}}: from '{{ field.old_value}}' to '{{ field.new_value }}'. - {% endfor %} + {% include "emails/includes/fields_diff-text.jinja" %} {% endif %} ** More info at {{ final_url_name }} ({{ final_url }}) ** diff --git a/taiga/projects/userstories/tests/tests_api.py b/taiga/projects/userstories/tests/tests_api.py index fd3d56e4..8ec8d810 100644 --- a/taiga/projects/userstories/tests/tests_api.py +++ b/taiga/projects/userstories/tests/tests_api.py @@ -27,7 +27,6 @@ from taiga.projects.issues.tests import create_issue from . import create_userstory import json -import reversion class UserStoriesTestCase(test.TestCase): @@ -689,8 +688,6 @@ class UserStoriesTestCase(test.TestCase): self.assertEqual(len(mail.outbox), 2) self.assertEqual(response.data["origin_issue"]["subject"], issue.subject) - issue_historical = reversion.get_unique_for_object(issue) - self.assertTrue(data["subject"] in issue_historical[0].revision.comment) self.client.logout() @@ -721,8 +718,6 @@ class UserStoriesTestCase(test.TestCase): self.assertEqual(len(mail.outbox), 2) self.assertEqual(response.data["origin_issue"]["subject"], issue.subject) - issue_historical = reversion.get_unique_for_object(issue) - self.assertTrue(data["subject"] in issue_historical[0].revision.comment) self.client.logout() diff --git a/taiga/projects/wiki/admin.py b/taiga/projects/wiki/admin.py index 6fc1e2cf..053cc8a7 100644 --- a/taiga/projects/wiki/admin.py +++ b/taiga/projects/wiki/admin.py @@ -16,8 +16,7 @@ from django.contrib import admin -from taiga.projects.wiki.models import WikiPage -from taiga.projects.admin import AttachmentInline +from taiga.projects.attachments.admin import AttachmentInline from . import models diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index 0d751497..2daba09d 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -14,60 +14,55 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ - from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework import status from taiga.base import filters from taiga.base import exceptions as exc -from taiga.base.api import ModelCrudViewSet, ModelListViewSet -from taiga.base.notifications.api import NotificationSenderMixin -from taiga.projects.permissions import AttachmentPermission -from taiga.projects.serializers import AttachmentSerializer -from taiga.projects.models import Attachment +from taiga.base.api import ModelCrudViewSet +from taiga.base.decorators import list_route +from taiga.projects.mixins.notifications import NotificationSenderMixin +from taiga.projects.attachments.api import BaseAttachmentViewSet +from taiga.projects.models import Project +from taiga.mdrender.service import render as mdrender from . import models from . import permissions from . import serializers -class WikiAttachmentViewSet(ModelCrudViewSet): - model = Attachment - serializer_class = AttachmentSerializer - permission_classes = (IsAuthenticated, AttachmentPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) - filter_fields = ["project", "object_id"] - - def get_queryset(self): - ct = ContentType.objects.get_for_model(models.WikiPage) - qs = super().get_queryset() - qs = qs.filter(content_type=ct) - return qs.distinct() - - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PermissionDenied(_("You don't have permissions for add " - "attachments to this wiki page.")) - - def pre_save(self, obj): - if not obj.id: - obj.content_type = ContentType.objects.get_for_model(models.WikiPage) - obj.owner = self.request.user - - super().pre_save(obj) - - -class WikiViewSet(ModelCrudViewSet): +class WikiViewSet(NotificationSenderMixin, ModelCrudViewSet): model = models.WikiPage serializer_class = serializers.WikiPageSerializer permission_classes = (IsAuthenticated,) filter_backends = (filters.IsProjectMemberFilterBackend,) filter_fields = ["project", "slug"] + create_notification_template = "create_wiki_notification" + update_notification_template = "update_wiki_notification" + destroy_notification_template = "destroy_wiki_notification" + + @list_route(methods=["POST"]) + def render(self, request, **kwargs): + content = request.DATA.get("content", None) + project_id = request.DATA.get("project_id", None) + + if not content: + return Response({"content": "No content parameter"}, status=status.HTTP_400_BAD_REQUEST) + if not project_id: + return Response({"project_id": "No project_id parameter"}, status=status.HTTP_400_BAD_REQUEST) + + try: + project = Project.objects.get(pk=project_id) + except Project.DoesNotExist: + return Response({"project_id": "Not valid project id"}, status=status.HTTP_400_BAD_REQUEST) + + data = mdrender(project, content) + + return Response({"data": data}) + def pre_conditions_on_save(self, obj): super().pre_conditions_on_save(obj) diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index e193783c..e521f13d 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -19,8 +19,6 @@ from django.contrib.contenttypes import generic from django.conf import settings from django.utils.translation import ugettext_lazy as _ -import reversion - class WikiPage(models.Model): project = models.ForeignKey("projects.Project", null=False, blank=False, @@ -51,6 +49,3 @@ class WikiPage(models.Model): def __str__(self): return "project {0} - {1}".format(self.project_id, self.slug) - - -reversion.register(WikiPage) diff --git a/taiga/projects/wiki/templates/emails/create_wiki_notification-body-html.jinja b/taiga/projects/wiki/templates/emails/create_wiki_notification-body-html.jinja new file mode 100644 index 00000000..5eaf9f83 --- /dev/null +++ b/taiga/projects/wiki/templates/emails/create_wiki_notification-body-html.jinja @@ -0,0 +1,21 @@ +{% extends "emails/base.jinja" %} + +{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %} +{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %} + +{% block body %} + + + + +
+

Project: {{ object.project.name }}

+

Wiki Page: {{ object.slug }}

+

Created by {{ changer.get_full_name() }}.

+
+{% endblock %} +{% block footer %} +

+ More info at: {{ final_url_name }} +

+{% endblock %} diff --git a/taiga/projects/wiki/templates/emails/create_wiki_notification-body-text.jinja b/taiga/projects/wiki/templates/emails/create_wiki_notification-body-text.jinja new file mode 100644 index 00000000..b41f4b8c --- /dev/null +++ b/taiga/projects/wiki/templates/emails/create_wiki_notification-body-text.jinja @@ -0,0 +1,8 @@ +{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %} +{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %} + +- Project: {{ object.project.name }} +- Wiki Page: {{ object.slug }} +- Created by {{ changer.get_full_name() }} + +** More info at {{ final_url_name }} ({{ final_url }}) ** diff --git a/taiga/projects/wiki/templates/emails/create_wiki_notification-subject.jinja b/taiga/projects/wiki/templates/emails/create_wiki_notification-subject.jinja new file mode 100644 index 00000000..a32efed4 --- /dev/null +++ b/taiga/projects/wiki/templates/emails/create_wiki_notification-subject.jinja @@ -0,0 +1 @@ +[{{ object.project.name|safe }}] Created the Wiki Page "{{ object.slug }}" diff --git a/taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-html.jinja b/taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-html.jinja new file mode 100644 index 00000000..6f9bc2ba --- /dev/null +++ b/taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-html.jinja @@ -0,0 +1,13 @@ +{% extends "emails/base.jinja" %} + +{% block body %} + + + + +
+

{{ object.project.name }}

+

Wiki Page: {{ object.slug }}

+

Deleted by {{ changer.get_full_name() }}

+
+{% endblock %} diff --git a/taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-text.jinja b/taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-text.jinja new file mode 100644 index 00000000..e69b4559 --- /dev/null +++ b/taiga/projects/wiki/templates/emails/destroy_wiki_notification-body-text.jinja @@ -0,0 +1,3 @@ +- Project: {{ object.project.name }} +- Wiki Page: {{ object.slug }} +- Deleted by {{ changer.get_full_name() }} diff --git a/taiga/projects/wiki/templates/emails/destroy_wiki_notification-subject.jinja b/taiga/projects/wiki/templates/emails/destroy_wiki_notification-subject.jinja new file mode 100644 index 00000000..7fd60924 --- /dev/null +++ b/taiga/projects/wiki/templates/emails/destroy_wiki_notification-subject.jinja @@ -0,0 +1 @@ +[{{ object.project.name|safe }}] Deleted the Wiki Page "{{ object.slug }}" diff --git a/taiga/projects/wiki/templates/emails/update_wiki_notification-body-html.jinja b/taiga/projects/wiki/templates/emails/update_wiki_notification-body-html.jinja new file mode 100644 index 00000000..89e3880d --- /dev/null +++ b/taiga/projects/wiki/templates/emails/update_wiki_notification-body-html.jinja @@ -0,0 +1,28 @@ +{% extends "emails/base.jinja" %} + +{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %} +{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %} + +{% block body %} + + + + +
+

Project: {{ object.project.name }}

+

Wiki Page: {{ object.slug }}

+

Updated by {{ changer.get_full_name() }}.

+ {% if comment %} +

Comment {{ comment|linebreaksbr }}

+ {% endif %} + {% if changed_fields %} +

Updated fields:

+ {% include "emails/includes/fields_diff-html.jinja" %} + {% endif %} +
+{% endblock %} +{% block footer %} +

+ More info at: {{ final_url_name }} +

+{% endblock %} diff --git a/taiga/projects/wiki/templates/emails/update_wiki_notification-body-text.jinja b/taiga/projects/wiki/templates/emails/update_wiki_notification-body-text.jinja new file mode 100644 index 00000000..b358d18f --- /dev/null +++ b/taiga/projects/wiki/templates/emails/update_wiki_notification-body-text.jinja @@ -0,0 +1,15 @@ +{% set final_url = resolve_front_url("wiki", object.project.slug, object.slug) %} +{% set final_url_name = "Taiga - View Wiki Page '{0}'".format(object.slug) %} + +- Project: {{ object.project.name }} +- Wiki Page: {{ object.slug }} +- Updated by {{ changer.get_full_name() }} +{% if comment %} +Comment: {{ comment|linebreaksbr }} +{% endif %} +{% if changed_fields %} +- Updated fields: + {% include "emails/includes/fields_diff-text.jinja" %} +{% endif %} + +** More info at {{ final_url_name }} ({{ final_url }}) ** diff --git a/taiga/projects/wiki/templates/emails/update_wiki_notification-subject.jinja b/taiga/projects/wiki/templates/emails/update_wiki_notification-subject.jinja new file mode 100644 index 00000000..18413c2d --- /dev/null +++ b/taiga/projects/wiki/templates/emails/update_wiki_notification-subject.jinja @@ -0,0 +1 @@ +[{{ object.project.name|safe }}] Updated the Wiki Page "{{ object.slug }}" diff --git a/taiga/users/models.py b/taiga/users/models.py index e2915e49..15ef2261 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import UserManager, AbstractUser from taiga.base.utils.slug import slugify_uniquely -from taiga.base.notifications.models import WatcherMixin +from taiga.projects.notifications.models import WatcherMixin import random