Update notifications to use the new history module
parent
a8bdb364ee
commit
09eced41a0
|
@ -15,23 +15,15 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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,
|
||||
|
|
|
@ -14,35 +14,8 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ URLS = {
|
|||
"project-admin": "/#/project/{0}/admin",
|
||||
"change-password": "/#/change-password/{0}",
|
||||
"invitation": "/#/invitation/{0}",
|
||||
"wiki": "/#/project/{0}/wiki/{1}"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -14,24 +14,8 @@
|
|||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:
|
||||
<dl>
|
||||
{% for field in changed_fields %}
|
||||
<dt style="background: #669933; padding: 5px 15px; color: #fff">
|
||||
<b>{{ field.verbose_name}}</b>
|
||||
</dt>
|
||||
{% if field.new_value != None or field.new_value != "" %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>to:</b> <i>{{ field.new_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if field.old_value != None or field.old_value != "" %}
|
||||
<dd style="padding: 5px 15px; color: #bbb">
|
||||
<b>from:</b> <i>{{ field.old_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
|||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% for field in changed_fields %}
|
||||
* {{ field.verbose_name}}</b>: 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 }}) **
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import reversion
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
|
|
@ -14,24 +14,8 @@
|
|||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:
|
||||
<dl>
|
||||
{% for field in changed_fields %}
|
||||
<dt style="background: #669933; padding: 5px 15px; color: #fff">
|
||||
<b>{{ field.verbose_name}}</b>
|
||||
</dt>
|
||||
{% if field.new_value != None or field.new_value != "" %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>to:</b> <i>{{ field.new_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if field.old_value != None or field.old_value != "" %}
|
||||
<dd style="padding: 5px 15px; color: #bbb">
|
||||
<b>from:</b> <i>{{ field.old_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
|||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% for field in changed_fields %}
|
||||
* {{ field.verbose_name}}</b>: 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 }}) **
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#######
|
||||
# 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
|
|
@ -1,42 +0,0 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
|
@ -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.
|
||||
|
|
|
@ -15,10 +15,8 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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.
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
|
@ -14,24 +14,8 @@
|
|||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:
|
||||
<dl>
|
||||
{% for field in changed_fields %}
|
||||
<dt style="background: #669933; padding: 5px 15px; color: #fff">
|
||||
<b>{{ field.verbose_name}}</b>
|
||||
</dt>
|
||||
{% if field.new_value != None or field.new_value != "" %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>to:</b> <i>{{ field.new_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if field.old_value != None or field.old_value != "" %}
|
||||
<dd style="padding: 5px 15px; color: #bbb">
|
||||
<b>from:</b> <i>{{ field.old_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
|||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% for field in changed_fields %}
|
||||
* {{ field.verbose_name}}</b>: 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 }}) **
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -13,24 +13,8 @@
|
|||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:
|
||||
<dl>
|
||||
{% for field in changed_fields %}
|
||||
<dt style="background: #669933; padding: 5px 15px; color: #fff">
|
||||
<b>{{ field.verbose_name}}</b>
|
||||
</dt>
|
||||
{% if field.new_value != None or field.new_value != "" %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>to:</b> <i>{{ field.new_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if field.old_value != None or field.old_value != "" %}
|
||||
<dd style="padding: 5px 15px; color: #bbb">
|
||||
<b>from:</b> <i>{{ field.old_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -8,8 +8,8 @@ Comment: {{ comment|linebreaksbr }}
|
|||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% for field in changed_fields %}
|
||||
* {{ field.verbose_name}}</b>: from '{{ field.old_value}}' to '{{ field.new_value }}'.
|
||||
{% for field_name, values in changed_fields.items() %}
|
||||
* {{ verbose_name(object, field_name) }}</b>: from '{{ values.0 }}' to '{{ values.1 }}'.
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -15,14 +15,12 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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"]
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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,11 +142,13 @@ 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()
|
||||
|
||||
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
|
||||
|
||||
def pre_save(self, obj):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = """
|
||||
|
|
|
@ -14,24 +14,8 @@
|
|||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:
|
||||
<dl>
|
||||
{% for field in changed_fields %}
|
||||
<dt style="background: #669933; padding: 5px 15px; color: #fff">
|
||||
<b>{{ field.verbose_name}}</b>
|
||||
</dt>
|
||||
{% if field.new_value != None or field.new_value != "" %}
|
||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
||||
<b>to:</b> <i>{{ field.new_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if field.old_value != None or field.old_value != "" %}
|
||||
<dd style="padding: 5px 15px; color: #bbb">
|
||||
<b>from:</b> <i>{{ field.old_value|linebreaksbr }}</i>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
|||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
- Updated fields:
|
||||
{% for field in changed_fields %}
|
||||
* {{ field.verbose_name}}</b>: 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 }}) **
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -14,60 +14,55 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Wiki Page: {{ object.slug }}</h2>
|
||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -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 }}) **
|
|
@ -0,0 +1 @@
|
|||
[{{ object.project.name|safe }}] Created the Wiki Page "{{ object.slug }}"
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "emails/base.jinja" %}
|
||||
|
||||
{% block body %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>{{ object.project.name }}</h1>
|
||||
<h2>Wiki Page: {{ object.slug }}</h2>
|
||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -0,0 +1,3 @@
|
|||
- Project: {{ object.project.name }}
|
||||
- Wiki Page: {{ object.slug }}
|
||||
- Deleted by {{ changer.get_full_name() }}
|
|
@ -0,0 +1 @@
|
|||
[{{ object.project.name|safe }}] Deleted the Wiki Page "{{ object.slug }}"
|
|
@ -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 %}
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||
<tr>
|
||||
<td>
|
||||
<h1>Project: {{ object.project.name }}</h1>
|
||||
<h2>Wiki Page: {{ object.slug }}</h2>
|
||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
||||
{% if comment %}
|
||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||
{% endif %}
|
||||
{% if changed_fields %}
|
||||
<p>Updated fields:</p>
|
||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -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 }}) **
|
|
@ -0,0 +1 @@
|
|||
[{{ object.project.name|safe }}] Updated the Wiki Page "{{ object.slug }}"
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue