Update notifications to use the new history module

remotes/origin/enhancement/email-actions
David Barragán Merino 2014-05-09 11:05:09 +02:00
parent a8bdb364ee
commit 09eced41a0
54 changed files with 345 additions and 618 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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}"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]

View File

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

View File

@ -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
@ -34,7 +34,7 @@ import reversion
class Task(WatchedMixin, BlockedMixin): class Task(WatchedMixin, BlockedMixin):
user_story = models.ForeignKey("userstories.UserStory", null=True, blank=True, user_story = models.ForeignKey("userstories.UserStory", null=True, blank=True,
related_name="tasks", verbose_name=_("user story")) related_name="tasks", verbose_name=_("user story"))
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_("ref")) verbose_name=_("ref"))
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
@ -109,10 +109,6 @@ class Task(WatchedMixin, BlockedMixin):
} }
# Reversion registration (usufull for base.notification and for meke a historical)
reversion.register(Task)
# Model related signals handlers # 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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 %}

View File

@ -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"]

View File

@ -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,10 +142,12 @@ 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(): self.object.generated_from_issue.save()
reversion.set_comment(_("Generated the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - {subject}\")").format(
ref=self.object.ref, subject=self.object.subject)) comment = _("Generate the user story [US #{ref} - "
self.object.generated_from_issue.save() "{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

View File

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

View File

@ -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 = """

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 %}

View File

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

View File

@ -0,0 +1 @@
[{{ object.project.name|safe }}] Created the Wiki Page "{{ object.slug }}"

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
- Project: {{ object.project.name }}
- Wiki Page: {{ object.slug }}
- Deleted by {{ changer.get_full_name() }}

View File

@ -0,0 +1 @@
[{{ object.project.name|safe }}] Deleted the Wiki Page "{{ object.slug }}"

View File

@ -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 %}

View File

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

View File

@ -0,0 +1 @@
[{{ object.project.name|safe }}] Updated the Wiki Page "{{ object.slug }}"

View File

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