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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework import decorators as rf_decorators
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from reversion.revisions import revision_context_manager
|
|
||||||
from reversion.models import Version
|
|
||||||
|
|
||||||
|
|
||||||
from . import pagination
|
from . import pagination
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import decorators
|
|
||||||
|
|
||||||
|
|
||||||
# Transactional version of rest framework mixins.
|
# Transactional version of rest framework mixins.
|
||||||
|
@ -118,98 +110,9 @@ class DetailAndListSerializersMixin(object):
|
||||||
return super().get_serializer_class()
|
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
|
# Own subclasses of django rest framework viewsets
|
||||||
|
|
||||||
class ModelCrudViewSet(DetailAndListSerializersMixin,
|
class ModelCrudViewSet(DetailAndListSerializersMixin,
|
||||||
ReversionMixin,
|
|
||||||
PreconditionMixin,
|
PreconditionMixin,
|
||||||
pagination.HeadersPaginationMixin,
|
pagination.HeadersPaginationMixin,
|
||||||
pagination.ConditionalPaginationMixin,
|
pagination.ConditionalPaginationMixin,
|
||||||
|
@ -223,7 +126,6 @@ class ModelCrudViewSet(DetailAndListSerializersMixin,
|
||||||
|
|
||||||
|
|
||||||
class ModelListViewSet(DetailAndListSerializersMixin,
|
class ModelListViewSet(DetailAndListSerializersMixin,
|
||||||
ReversionMixin,
|
|
||||||
PreconditionMixin,
|
PreconditionMixin,
|
||||||
pagination.HeadersPaginationMixin,
|
pagination.HeadersPaginationMixin,
|
||||||
pagination.ConditionalPaginationMixin,
|
pagination.ConditionalPaginationMixin,
|
||||||
|
|
|
@ -14,35 +14,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
import warnings
|
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.
|
## 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 rest_framework import serializers
|
||||||
|
|
||||||
from reversion.models import Version
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
from taiga.domains.base import get_active_domain
|
from taiga.domains.base import get_active_domain
|
||||||
from taiga.domains.models import Domain
|
from taiga.domains.models import Domain
|
||||||
|
|
||||||
|
@ -57,67 +54,6 @@ class AutoDomainField(serializers.WritableField):
|
||||||
domain = get_active_domain()
|
domain = get_active_domain()
|
||||||
return 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:
|
class NeighborsSerializerMixin:
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ URLS = {
|
||||||
"project-admin": "/#/project/{0}/admin",
|
"project-admin": "/#/project/{0}/admin",
|
||||||
"change-password": "/#/change-password/{0}",
|
"change-password": "/#/change-password/{0}",
|
||||||
"invitation": "/#/invitation/{0}",
|
"invitation": "/#/invitation/{0}",
|
||||||
|
"wiki": "/#/project/{0}/wiki/{1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ class MembershipInline(admin.TabularInline):
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProjectAdmin(reversion.VersionAdmin):
|
class ProjectAdmin(admin.ModelAdmin):
|
||||||
list_display = ["name", "owner", "created_date", "total_milestones",
|
list_display = ["name", "owner", "created_date", "total_milestones",
|
||||||
"total_story_points", "domain"]
|
"total_story_points", "domain"]
|
||||||
list_display_links = list_display
|
list_display_links = list_display
|
||||||
|
|
|
@ -20,10 +20,8 @@ from taiga.projects.admin import AttachmentInline
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
class IssueAdmin(admin.ModelAdmin):
|
||||||
class IssueAdmin(reversion.VersionAdmin):
|
|
||||||
list_display = ["project", "milestone", "ref", "subject",]
|
list_display = ["project", "milestone", "ref", "subject",]
|
||||||
list_display_links = ["ref", "subject",]
|
list_display_links = ["ref", "subject",]
|
||||||
inlines = [AttachmentInline]
|
inlines = [AttachmentInline]
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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.utils.translation import ugettext_lazy as _
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
@ -27,11 +25,12 @@ from rest_framework import filters
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import list_route
|
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.permissions import AttachmentPermission
|
||||||
from taiga.projects.serializers import AttachmentSerializer
|
from taiga.projects.serializers import AttachmentSerializer
|
||||||
from taiga.projects.models import Attachment
|
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 models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
@ -142,15 +141,6 @@ class IssueViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudViewSet)
|
||||||
|
|
||||||
if obj.type and obj.type.project != obj.project:
|
if obj.type and obj.type.project != obj.project:
|
||||||
raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue."))
|
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):
|
class IssueAttachmentViewSet(ModelCrudViewSet):
|
||||||
model = Attachment
|
model = Attachment
|
||||||
serializer_class = AttachmentSerializer
|
serializer_class = AttachmentSerializer
|
||||||
|
|
|
@ -28,7 +28,6 @@ from taiga.base.utils.slug import ref_uniquely
|
||||||
from taiga.base.notifications.models import WatchedMixin
|
from taiga.base.notifications.models import WatchedMixin
|
||||||
from taiga.projects.mixins.blocked.models import BlockedMixin
|
from taiga.projects.mixins.blocked.models import BlockedMixin
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
|
||||||
class Issue(NeighborsMixin, WatchedMixin, BlockedMixin):
|
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
|
# Model related signals handlers
|
||||||
@receiver(models.signals.pre_save, sender=Issue, dispatch_uid="issue_finished_date_handler")
|
@receiver(models.signals.pre_save, sender=Issue, dispatch_uid="issue_finished_date_handler")
|
||||||
def issue_finished_date_handler(sender, instance, **kwargs):
|
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.base.serializers import PickleField, NeighborsSerializerMixin
|
||||||
from taiga.projects.serializers import AttachmentSerializer
|
from taiga.projects.serializers import AttachmentSerializer
|
||||||
from taiga.projects.mixins.notifications.serializers import WatcherValidationSerializerMixin
|
from taiga.projects.mixins.notifications import WatcherValidationSerializerMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -14,24 +14,8 @@
|
||||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
<p>Updated fields:
|
<p>Updated fields:</p>
|
||||||
<dl>
|
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
- Updated fields:
|
- Updated fields:
|
||||||
{% for field in changed_fields %}
|
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||||
* {{ field.verbose_name}}</b>: from '{{ field.old_value}}' to '{{ field.new_value }}'.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
import reversion
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ class MilestoneInline(admin.TabularInline):
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class MilestoneAdmin(reversion.VersionAdmin):
|
class MilestoneAdmin(admin.ModelAdmin):
|
||||||
list_display = ["name", "project", "owner", "closed", "estimated_start",
|
list_display = ["name", "project", "owner", "closed", "estimated_start",
|
||||||
"estimated_finish"]
|
"estimated_finish"]
|
||||||
list_display_links = list_display
|
list_display_links = list_display
|
||||||
|
|
|
@ -24,7 +24,7 @@ from taiga.base import filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.api import ModelCrudViewSet
|
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 serializers
|
||||||
from . import models
|
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.slug import slugify_uniquely
|
||||||
from taiga.base.utils.dicts import dict_sum
|
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
|
from taiga.projects.userstories.models import UserStory
|
||||||
|
|
||||||
import reversion
|
|
||||||
import itertools
|
import itertools
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
@ -146,7 +145,3 @@ class Milestone(WatchedMixin, models.Model):
|
||||||
finish_date__lt=date + datetime.timedelta(days=1)
|
finish_date__lt=date + datetime.timedelta(days=1)
|
||||||
) if us.is_closed
|
) 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import reversion
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
|
@ -14,24 +14,8 @@
|
||||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
<p>Updated fields:
|
<p>Updated fields:</p>
|
||||||
<dl>
|
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
- Updated fields:
|
- Updated fields:
|
||||||
{% for field in changed_fields %}
|
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||||
* {{ field.verbose_name}}</b>: from '{{ field.old_value}}' to '{{ field.new_value }}'.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
** 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
|
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
|
# On membership object is created/changed, update
|
||||||
# role-points relation.
|
# role-points relation.
|
||||||
|
|
|
@ -15,10 +15,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
|
||||||
class WatcherMixin(models.Model):
|
class WatcherMixin(models.Model):
|
||||||
|
@ -63,38 +61,8 @@ class WatcherMixin(models.Model):
|
||||||
def allow_notify_project(self, project):
|
def allow_notify_project(self, project):
|
||||||
return self.notify_level == "all_owned_projects"
|
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):
|
def get_watchers_to_notify(self, changer):
|
||||||
watchers_to_notify = set()
|
watchers_to_notify = set()
|
||||||
watchers_by_role = self._get_watchers_by_role()
|
watchers_by_role = self._get_watchers_by_role()
|
||||||
|
@ -127,47 +95,6 @@ class WatchedMixin(models.Model):
|
||||||
|
|
||||||
return watchers_to_notify
|
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):
|
def _get_watchers_by_role(self):
|
||||||
"""
|
"""
|
||||||
Return the actual instances of watchers of this object, classified by role.
|
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
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class TaskAdmin(reversion.VersionAdmin):
|
class TaskAdmin(admin.ModelAdmin):
|
||||||
list_display = ["project", "milestone", "user_story", "ref", "subject",]
|
list_display = ["project", "milestone", "user_story", "ref", "subject",]
|
||||||
list_display_links = ["ref", "subject",]
|
list_display_links = ["ref", "subject",]
|
||||||
list_filter = ["project"]
|
list_filter = ["project"]
|
||||||
|
|
|
@ -27,19 +27,15 @@ from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.permissions import has_project_perm
|
from taiga.base.permissions import has_project_perm
|
||||||
from taiga.base.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.notifications.api import NotificationSenderMixin
|
|
||||||
from taiga.projects.permissions import AttachmentPermission
|
from taiga.projects.permissions import AttachmentPermission
|
||||||
from taiga.projects.serializers import AttachmentSerializer
|
from taiga.projects.serializers import AttachmentSerializer
|
||||||
from taiga.projects.models import Attachment, Project
|
from taiga.projects.models import Attachment, Project
|
||||||
|
from taiga.projects.mixins.notifications import NotificationSenderMixin
|
||||||
from taiga.projects.userstories.models import UserStory
|
from taiga.projects.userstories.models import UserStory
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
|
||||||
class TaskAttachmentViewSet(ModelCrudViewSet):
|
class TaskAttachmentViewSet(ModelCrudViewSet):
|
||||||
model = Attachment
|
model = Attachment
|
||||||
serializer_class = AttachmentSerializer
|
serializer_class = AttachmentSerializer
|
||||||
|
@ -66,6 +62,7 @@ class TaskAttachmentViewSet(ModelCrudViewSet):
|
||||||
obj.project.memberships.filter(user=self.request.user).count() == 0):
|
obj.project.memberships.filter(user=self.request.user).count() == 0):
|
||||||
raise exc.PermissionDenied(_("You don't have permissions for add "
|
raise exc.PermissionDenied(_("You don't have permissions for add "
|
||||||
"attachments to this task."))
|
"attachments to this task."))
|
||||||
|
from . import services
|
||||||
|
|
||||||
|
|
||||||
class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
|
@ -102,13 +99,6 @@ class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
if obj.status and obj.status.project != obj.project:
|
if obj.status and obj.status.project != obj.project:
|
||||||
raise exc.PermissionDenied(_("You don't have permissions for add/modify this task."))
|
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"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
bulk_tasks = request.DATA.get('bulkTasks', None)
|
bulk_tasks = request.DATA.get('bulkTasks', None)
|
||||||
|
@ -124,21 +114,14 @@ class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
raise exc.BadRequest(_('usId parameter is mandatory'))
|
raise exc.BadRequest(_('usId parameter is mandatory'))
|
||||||
|
|
||||||
project = get_object_or_404(Project, id=project_id)
|
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'):
|
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."))
|
raise exc.PermissionDenied(_("You don't have permisions to create tasks."))
|
||||||
|
|
||||||
items = filter(lambda s: len(s) > 0,
|
service = services.TasksService()
|
||||||
map(lambda s: s.strip(), bulk_tasks.split("\n")))
|
tasks = service.bulk_insert(project, request.user, user_story, bulk_tasks,
|
||||||
|
callback_on_success=self.post_save)
|
||||||
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)
|
|
||||||
|
|
||||||
tasks_serialized = self.serializer_class(tasks, many=True)
|
tasks_serialized = self.serializer_class(tasks, many=True)
|
||||||
return Response(data=tasks_serialized.data)
|
return Response(data=tasks_serialized.data)
|
||||||
|
|
|
@ -24,7 +24,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from picklefield.fields import PickledObjectField
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
from taiga.base.utils.slug import ref_uniquely
|
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.userstories.models import UserStory
|
||||||
from taiga.projects.milestones.models import Milestone
|
from taiga.projects.milestones.models import Milestone
|
||||||
from taiga.projects.mixins.blocked.models import BlockedMixin
|
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
|
# Model related signals handlers
|
||||||
@receiver(models.signals.pre_save, sender=Task, dispatch_uid="task_ref_handler")
|
@receiver(models.signals.pre_save, sender=Task, dispatch_uid="task_ref_handler")
|
||||||
def task_ref_handler(sender, instance, **kwargs):
|
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>
|
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
<p>Updated fields:
|
<p>Updated fields:</p>
|
||||||
<dl>
|
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
- Updated fields:
|
- Updated fields:
|
||||||
{% for field in changed_fields %}
|
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||||
* {{ field.verbose_name}}</b>: from '{{ field.old_value}}' to '{{ field.new_value }}'.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||||
|
|
|
@ -20,8 +20,6 @@ from django import test
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
from taiga.users.tests import create_user
|
from taiga.users.tests import create_user
|
||||||
from taiga.projects.tests import create_project, add_membership
|
from taiga.projects.tests import create_project, add_membership
|
||||||
from taiga.projects.milestones.tests import create_milestone
|
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,)))
|
response = self.client.get(reverse("tasks-detail", args=(self.task1.id,)))
|
||||||
self.assertEqual(response.status_code, 401)
|
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):
|
def test_view_task_by_owner(self):
|
||||||
response = self.client.login(username=self.user2.username,
|
response = self.client.login(username=self.user2.username,
|
||||||
password=self.user2.username)
|
password=self.user2.username)
|
||||||
|
|
|
@ -13,24 +13,8 @@
|
||||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
<p>Updated fields:
|
<p>Updated fields:</p>
|
||||||
<dl>
|
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -8,8 +8,8 @@ Comment: {{ comment|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
- Updated fields:
|
- Updated fields:
|
||||||
{% for field in changed_fields %}
|
{% for field_name, values in changed_fields.items() %}
|
||||||
* {{ field.verbose_name}}</b>: from '{{ field.old_value}}' to '{{ field.new_value }}'.
|
* {{ verbose_name(object, field_name) }}</b>: from '{{ values.0 }}' to '{{ values.1 }}'.
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,12 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
import reversion
|
|
||||||
|
|
||||||
from taiga.projects.admin import AttachmentInline
|
from taiga.projects.admin import AttachmentInline
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RolePointsInline(admin.TabularInline):
|
class RolePointsInline(admin.TabularInline):
|
||||||
model = models.RolePoints
|
model = models.RolePoints
|
||||||
sortable_field_name = 'role'
|
sortable_field_name = 'role'
|
||||||
|
@ -39,7 +37,7 @@ class RolePointsAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ["user_story", "role", "points"]
|
readonly_fields = ["user_story", "role", "points"]
|
||||||
|
|
||||||
|
|
||||||
class UserStoryAdmin(reversion.VersionAdmin):
|
class UserStoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ["project", "milestone", "ref", "subject",]
|
list_display = ["project", "milestone", "ref", "subject",]
|
||||||
list_display_links = ["ref", "subject",]
|
list_display_links = ["ref", "subject",]
|
||||||
list_filter = ["project"]
|
list_filter = ["project"]
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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 filters
|
||||||
from taiga.base import exceptions as exc
|
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.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.permissions import AttachmentPermission
|
||||||
from taiga.projects.serializers import AttachmentSerializer
|
from taiga.projects.serializers import AttachmentSerializer
|
||||||
from taiga.projects.models import Attachment, Project
|
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 models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
@ -103,16 +104,19 @@ class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudView
|
||||||
raise exc.PermissionDenied(_("You don't have permisions to create user stories."))
|
raise exc.PermissionDenied(_("You don't have permisions to create user stories."))
|
||||||
|
|
||||||
service = services.UserStoriesService()
|
service = services.UserStoriesService()
|
||||||
service.bulk_insert(project, request.user, bulk_stories,
|
user_stories = service.bulk_insert(project, request.user, bulk_stories,
|
||||||
callback_on_success=self._post_save_notification_sender)
|
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"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_update_order(self, request, **kwargs):
|
def bulk_update_order(self, request, **kwargs):
|
||||||
# bulkStories should be:
|
# bulkStories should be:
|
||||||
# [[1,1],[23, 2], ...]
|
# [[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)
|
bulk_stories = request.DATA.get("bulkStories", None)
|
||||||
|
|
||||||
if bulk_stories is None:
|
if bulk_stories is None:
|
||||||
|
@ -138,11 +142,13 @@ class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudView
|
||||||
|
|
||||||
# Added comment to the origin (issue)
|
# Added comment to the origin (issue)
|
||||||
if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_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
|
return response
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
|
|
|
@ -21,12 +21,11 @@ from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from picklefield.fields import PickledObjectField
|
from picklefield.fields import PickledObjectField
|
||||||
import reversion
|
|
||||||
|
|
||||||
from taiga.base.models import NeighborsMixin
|
from taiga.base.models import NeighborsMixin
|
||||||
from taiga.base.utils.slug import ref_uniquely
|
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.mixins.blocked.models import BlockedMixin
|
||||||
|
from taiga.projects.notifications.models import WatchedMixin
|
||||||
|
|
||||||
|
|
||||||
class RolePoints(models.Model):
|
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
|
# Model related signals handlers
|
||||||
@receiver(models.signals.pre_save, sender=UserStory, dispatch_uid="user_story_ref_handler")
|
@receiver(models.signals.pre_save, sender=UserStory, dispatch_uid="user_story_ref_handler")
|
||||||
def us_ref_handler(sender, instance, **kwargs):
|
def us_ref_handler(sender, instance, **kwargs):
|
||||||
|
|
|
@ -18,23 +18,29 @@ from django.db import transaction
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
|
||||||
class UserStoriesService(object):
|
class UserStoriesService(object):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def bulk_insert(self, project, user, data, callback_on_success=None):
|
def bulk_insert(self, project, user, data, callback_on_success=None):
|
||||||
|
user_stories = []
|
||||||
|
|
||||||
items = filter(lambda s: len(s) > 0,
|
items = filter(lambda s: len(s) > 0,
|
||||||
map(lambda s: s.strip(), data.split("\n")))
|
map(lambda s: s.strip(), data.split("\n")))
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
obj = models.UserStory.objects.create(subject=item, project=project, owner=user,
|
obj = models.UserStory.objects.create(subject=item, project=project, owner=user,
|
||||||
status=project.default_us_status)
|
status=project.default_us_status)
|
||||||
|
user_stories.append(obj)
|
||||||
|
|
||||||
if callback_on_success:
|
if callback_on_success:
|
||||||
callback_on_success(obj, True)
|
callback_on_success(obj, True)
|
||||||
|
|
||||||
|
return user_stories
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def bulk_update_order(self, project, user, data):
|
def bulk_update_order(self, project, user, data):
|
||||||
|
# TODO: Create a history snapshot of all updated USs
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
|
|
|
@ -14,24 +14,8 @@
|
||||||
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
<p>Comment <b>{{ comment|linebreaksbr }}</b></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
<p>Updated fields:
|
<p>Updated fields:</p>
|
||||||
<dl>
|
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,9 +9,7 @@ Comment: {{ comment|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if changed_fields %}
|
{% if changed_fields %}
|
||||||
- Updated fields:
|
- Updated fields:
|
||||||
{% for field in changed_fields %}
|
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||||
* {{ field.verbose_name}}</b>: from '{{ field.old_value}}' to '{{ field.new_value }}'.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
** More info at {{ final_url_name }} ({{ final_url }}) **
|
||||||
|
|
|
@ -27,7 +27,6 @@ from taiga.projects.issues.tests import create_issue
|
||||||
from . import create_userstory
|
from . import create_userstory
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
|
||||||
class UserStoriesTestCase(test.TestCase):
|
class UserStoriesTestCase(test.TestCase):
|
||||||
|
@ -689,8 +688,6 @@ class UserStoriesTestCase(test.TestCase):
|
||||||
self.assertEqual(len(mail.outbox), 2)
|
self.assertEqual(len(mail.outbox), 2)
|
||||||
|
|
||||||
self.assertEqual(response.data["origin_issue"]["subject"], issue.subject)
|
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()
|
self.client.logout()
|
||||||
|
|
||||||
|
@ -721,8 +718,6 @@ class UserStoriesTestCase(test.TestCase):
|
||||||
self.assertEqual(len(mail.outbox), 2)
|
self.assertEqual(len(mail.outbox), 2)
|
||||||
|
|
||||||
self.assertEqual(response.data["origin_issue"]["subject"], issue.subject)
|
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()
|
self.client.logout()
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,7 @@
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from taiga.projects.wiki.models import WikiPage
|
from taiga.projects.attachments.admin import AttachmentInline
|
||||||
from taiga.projects.admin import AttachmentInline
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -14,60 +14,55 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
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 filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.notifications.api import NotificationSenderMixin
|
from taiga.base.decorators import list_route
|
||||||
from taiga.projects.permissions import AttachmentPermission
|
from taiga.projects.mixins.notifications import NotificationSenderMixin
|
||||||
from taiga.projects.serializers import AttachmentSerializer
|
from taiga.projects.attachments.api import BaseAttachmentViewSet
|
||||||
from taiga.projects.models import Attachment
|
from taiga.projects.models import Project
|
||||||
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class WikiAttachmentViewSet(ModelCrudViewSet):
|
class WikiViewSet(NotificationSenderMixin, 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):
|
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
serializer_class = serializers.WikiPageSerializer
|
serializer_class = serializers.WikiPageSerializer
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
filter_fields = ["project", "slug"]
|
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):
|
def pre_conditions_on_save(self, obj):
|
||||||
super().pre_conditions_on_save(obj)
|
super().pre_conditions_on_save(obj)
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,6 @@ from django.contrib.contenttypes import generic
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
|
|
||||||
class WikiPage(models.Model):
|
class WikiPage(models.Model):
|
||||||
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
@ -51,6 +49,3 @@ class WikiPage(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "project {0} - {1}".format(self.project_id, self.slug)
|
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 django.contrib.auth.models import UserManager, AbstractUser
|
||||||
|
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
from taiga.base.notifications.models import WatcherMixin
|
from taiga.projects.notifications.models import WatcherMixin
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue