From 7b7548b47dab968d4dcd94e7c7c8b65bd30edf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 10 Feb 2016 12:47:41 +0100 Subject: [PATCH] Update django to 1.9 and the rest of requirements --- requirements-devel.txt | 4 +- requirements.txt | 33 ++-- settings/common.py | 5 +- settings/local.py.example | 2 +- taiga/base/api/fields.py | 8 +- taiga/base/api/serializers.py | 28 ++-- taiga/base/api/settings.py | 3 +- taiga/base/api/utils/encoders.py | 3 - taiga/base/api/views.py | 5 +- taiga/base/apps.py | 6 +- taiga/base/management/commands/test_emails.py | 4 +- taiga/base/response.py | 5 +- taiga/base/signals/cleanup_files.py | 97 +++++++++++ taiga/base/signals/thumbnails.py | 2 +- taiga/events/apps.py | 3 +- taiga/feedback/apps.py | 3 +- taiga/mdrender/__init__.py | 18 --- taiga/mdrender/extensions/wikilinks.py | 152 +++++++++--------- taiga/mdrender/service.py | 1 + taiga/projects/apps.py | 6 +- taiga/projects/attachments/admin.py | 4 +- taiga/projects/attachments/models.py | 4 +- taiga/projects/issues/apps.py | 9 +- taiga/projects/issues/models.py | 4 +- taiga/projects/likes/models.py | 4 +- taiga/projects/notifications/models.py | 5 +- taiga/projects/notifications/services.py | 4 +- taiga/projects/references/models.py | 2 +- taiga/projects/tasks/apps.py | 7 +- taiga/projects/tasks/models.py | 4 +- taiga/projects/userstories/apps.py | 89 +++++----- taiga/projects/userstories/models.py | 6 +- taiga/projects/votes/models.py | 6 +- taiga/projects/wiki/models.py | 4 +- taiga/timeline/apps.py | 9 +- taiga/timeline/models.py | 2 +- taiga/webhooks/apps.py | 9 +- tests/unit/test_serializer_mixins.py | 23 +-- 38 files changed, 338 insertions(+), 245 deletions(-) create mode 100644 taiga/base/signals/cleanup_files.py diff --git a/requirements-devel.txt b/requirements-devel.txt index da4f0eb9..e01aa38a 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,8 +1,8 @@ -r requirements.txt -factory_boy==2.6.0 +factory_boy==2.6.1 py==1.4.31 -pytest==2.8.5 +pytest==2.8.7 pytest-django==2.9.1 pytest-pythonpath==0.7 diff --git a/requirements.txt b/requirements.txt index 993c3787..181e863f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,37 +1,36 @@ -Django==1.8.6 +Django==1.9.2 #djangorestframework==2.3.13 # It's not necessary since Taiga 1.7 django-picklefield==0.3.2 -django-sampledatahelper==0.3.0 -gunicorn==19.3.0 +django-sampledatahelper==0.4.0 +gunicorn==19.4.5 psycopg2==2.6.1 -Pillow==2.9.0 +Pillow==3.1.1 pytz==2015.7 six==1.10.0 -amqp==1.4.7 -djmail==0.11 +amqp==1.4.9 +djmail==0.12.0.post1 django-pgjson==0.3.1 djorm-pgarray==1.2 -django-jinja==2.1.1 +django-jinja==2.1.2 jinja2==2.8 pygments==2.0.2 -django-sites==0.8 +django-sites==0.9 Markdown==2.6.5 fn==0.4.3 diff-match-patch==20121119 -requests==2.8.1 +requests==2.9.1 django-sr==0.0.4 -easy-thumbnails==2.2.1 -celery==3.1.19 +easy-thumbnails==2.3 +celery==3.1.20 redis==2.10.5 -Unidecode==0.04.18 -raven==5.9.2 +Unidecode==0.04.19 +raven==5.10.2 bleach==1.4.2 -django-ipware==1.1.2 -premailer==2.9.6 +django-ipware==1.1.3 +premailer==2.9.7 cssutils==1.0.1 # Compatible with python 3.5 -django-transactional-cleanup==0.1.15 lxml==3.5.0 git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea -pyjwkest==1.0.9 +pyjwkest==1.1.5 python-dateutil==2.4.2 netaddr==0.7.18 diff --git a/settings/common.py b/settings/common.py index 7096f6e6..e207f057 100644 --- a/settings/common.py +++ b/settings/common.py @@ -30,7 +30,7 @@ DEBUG = False DATABASES = { "default": { - "ENGINE": "transaction_hooks.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", "NAME": "taiga", } } @@ -320,7 +320,6 @@ INSTALLED_APPS = [ "sr", "easy_thumbnails", "raven.contrib.django.raven_compat", - "django_transactional_cleanup", ] WSGI_APPLICATION = "taiga.wsgi.application" @@ -347,7 +346,7 @@ LOGGING = { "handlers": { "null": { "level":"DEBUG", - "class":"django.utils.log.NullHandler", + "class":"logging.NullHandler", }, "console":{ "level":"DEBUG", diff --git a/settings/local.py.example b/settings/local.py.example index 28f5abb9..d0052032 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -24,7 +24,7 @@ from .development import * DATABASES = { 'default': { - 'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'taiga', 'USER': 'taiga', 'PASSWORD': 'changeme', diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index 50fe58f1..365e4070 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -64,17 +64,17 @@ from django.utils.encoding import is_protected_type from django.utils.functional import Promise from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ -from django.utils.datastructures import SortedDict from . import ISO_8601 from .settings import api_settings +from collections import OrderedDict +from decimal import Decimal, DecimalException import copy import datetime import inspect import re import warnings -from decimal import Decimal, DecimalException def is_non_str_iterable(obj): @@ -255,7 +255,7 @@ class Field(object): return [self.to_native(item) for item in value] elif isinstance(value, dict): # Make sure we preserve field ordering, if it exists - ret = SortedDict() + ret = OrderedDict() for key, val in value.items(): ret[key] = self.to_native(val) return ret @@ -270,7 +270,7 @@ class Field(object): return {} def metadata(self): - metadata = SortedDict() + metadata = OrderedDict() metadata["type"] = self.type_label metadata["required"] = getattr(self, "required", False) optional_attrs = ["read_only", "label", "help_text", diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 8216ddf4..55ae824f 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -59,11 +59,11 @@ from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils import six -from django.utils.datastructures import SortedDict from django.utils.translation import ugettext as _ from .settings import api_settings +from collections import OrderedDict import copy import datetime import inspect @@ -148,7 +148,7 @@ class DictWithMetadata(dict): return dict(self) -class SortedDictWithMetadata(SortedDict): +class OrderedDictWithMetadata(OrderedDict): """ A sorted dict-like object, that can have additional properties attached. """ @@ -158,7 +158,7 @@ class SortedDictWithMetadata(SortedDict): Overriden to remove the metadata from the dict, since it shouldn't be pickle and may in some instances be unpickleable. """ - return SortedDict(self).__dict__ + return OrderedDict(self).__dict__ def _is_protected_type(obj): @@ -194,7 +194,7 @@ def _get_declared_fields(bases, attrs): if hasattr(base, "base_fields"): fields = list(base.base_fields.items()) + fields - return SortedDict(fields) + return OrderedDict(fields) class SerializerMetaclass(type): @@ -222,7 +222,7 @@ class BaseSerializer(WritableField): pass _options_class = SerializerOptions - _dict_class = SortedDictWithMetadata + _dict_class = OrderedDictWithMetadata def __init__(self, instance=None, data=None, files=None, context=None, partial=False, many=None, @@ -268,7 +268,7 @@ class BaseSerializer(WritableField): This will be the set of any explicitly declared fields, plus the set of fields returned by get_default_fields(). """ - ret = SortedDict() + ret = OrderedDict() # Get the explicitly declared fields base_fields = copy.deepcopy(self.base_fields) @@ -284,7 +284,7 @@ class BaseSerializer(WritableField): # If "fields" is specified, use those fields, in that order. if self.opts.fields: assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple" - new = SortedDict() + new = OrderedDict() for key in self.opts.fields: new[key] = ret[key] ret = new @@ -458,7 +458,10 @@ class BaseSerializer(WritableField): many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type)) if many: - return [self.to_native(item) for item in value] + try: + return [self.to_native(item) for item in value] + except TypeError: + pass # LazyObject is iterable so we need to catch this return self.to_native(value) def field_from_native(self, data, files, field_name, into): @@ -610,7 +613,10 @@ class BaseSerializer(WritableField): DeprecationWarning, stacklevel=2) if many: - self._data = [self.to_native(item) for item in obj] + try: + self._data = [self.to_native(item) for item in obj] + except TypeError: + self._data = self.to_native(obj) # LazyObject is iterable so we need to catch this else: self._data = self.to_native(obj) @@ -645,7 +651,7 @@ class BaseSerializer(WritableField): Useful for things like responding to OPTIONS requests, or generating API schemas for auto-documentation. """ - return SortedDict( + return OrderedDict( [(field_name, field.metadata()) for field_name, field in six.iteritems(self.fields)] ) @@ -740,7 +746,7 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer))) assert cls is not None, \ "Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__ opts = cls._meta.concrete_model._meta - ret = SortedDict() + ret = OrderedDict() nested = bool(self.opts.depth) # Deal with adding the primary key field diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py index bada39b7..9b894be6 100644 --- a/taiga/base/api/settings.py +++ b/taiga/base/api/settings.py @@ -62,9 +62,10 @@ back to the defaults. from __future__ import unicode_literals from django.conf import settings -from django.utils import importlib from django.utils import six +import importlib + from . import ISO_8601 diff --git a/taiga/base/api/utils/encoders.py b/taiga/base/api/utils/encoders.py index cf792697..0f878914 100644 --- a/taiga/base/api/utils/encoders.py +++ b/taiga/base/api/utils/encoders.py @@ -45,13 +45,10 @@ Helper classes for parsers. """ from django.db.models.query import QuerySet -from django.utils.datastructures import SortedDict from django.utils.functional import Promise from django.utils import timezone from django.utils.encoding import force_text -from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata - import datetime import decimal import types diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py index 68f58e27..791523d4 100644 --- a/taiga/base/api/views.py +++ b/taiga/base/api/views.py @@ -43,6 +43,8 @@ import json +from collections import OrderedDict + from django.conf import settings from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponse @@ -50,7 +52,6 @@ from django.http.response import HttpResponseBase from django.views.decorators.csrf import csrf_exempt from django.views.defaults import server_error from django.views.generic import View -from django.utils.datastructures import SortedDict from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ @@ -462,7 +463,7 @@ class APIView(View): # By default we can't provide any form-like information, however the # generic views override this implementation and add additional # information for POST and PUT methods, based on the serializer. - ret = SortedDict() + ret = OrderedDict() ret['name'] = self.get_view_name() ret['description'] = self.get_view_description() ret['renders'] = [renderer.media_type for renderer in self.renderer_classes] diff --git a/taiga/base/apps.py b/taiga/base/apps.py index 5a35fa30..b56aaafb 100644 --- a/taiga/base/apps.py +++ b/taiga/base/apps.py @@ -17,12 +17,14 @@ from django.apps import AppConfig -from .signals.thumbnails import connect_thumbnail_signals - class BaseAppConfig(AppConfig): name = "taiga.base" verbose_name = "Base App Config" def ready(self): + from .signals.thumbnails import connect_thumbnail_signals + from .signals.cleanup_files import connect_cleanup_files_signals + connect_thumbnail_signals() + connect_cleanup_files_signals() diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py index 76d9bc2f..a3e99688 100644 --- a/taiga/base/management/commands/test_emails.py +++ b/taiga/base/management/commands/test_emails.py @@ -19,7 +19,7 @@ import datetime from optparse import make_option -from django.db.models.loading import get_model +from django.apps import apps from django.core.management.base import BaseCommand from django.utils import timezone @@ -163,7 +163,7 @@ class Command(BaseCommand): } for notification_email in notification_emails: - model = get_model(*notification_email[0].split(".")) + model = apps.get_model(*notification_email[0].split(".")) snapshot = { "subject": "Tests subject", "ref": 123123, diff --git a/taiga/base/response.py b/taiga/base/response.py index 3698ca7a..5b84123a 100644 --- a/taiga/base/response.py +++ b/taiga/base/response.py @@ -43,9 +43,10 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """The various HTTP responses for use in returning proper HTTP codes.""" +from http.client import responses + from django import http -from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse from django.utils import six @@ -114,7 +115,7 @@ class Response(SimpleTemplateResponse): """ # TODO: Deprecate and use a template tag instead # TODO: Status code text for RFC 6585 status codes - return STATUS_CODE_TEXT.get(self.status_code, '') + return responses.get(self.status_code, '') def __getstate__(self): """ diff --git a/taiga/base/signals/cleanup_files.py b/taiga/base/signals/cleanup_files.py new file mode 100644 index 00000000..0efab210 --- /dev/null +++ b/taiga/base/signals/cleanup_files.py @@ -0,0 +1,97 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.apps import apps +from django.db import models, connection +from django.db.utils import DEFAULT_DB_ALIAS, ConnectionHandler +from django.db.models.signals import pre_save, post_delete + +import logging +logger = logging.getLogger(__name__) + + +from django.dispatch import Signal + +cleanup_pre_delete = Signal(providing_args=["file"]) +cleanup_post_delete = Signal(providing_args=["file"]) + + + +def _find_models_with_filefield(): + result = [] + for model in apps.get_models(): + for field in model._meta.fields: + if isinstance(field, models.FileField): + result.append(model) + break + return result + + +def _delete_file(file_obj): + def delete_from_storage(): + try: + cleanup_pre_delete.send(sender=None, file=file_obj) + storage.delete(file_obj.name) + cleanup_post_delete.send(sender=None, file=file_obj) + except Exception: + logger.exception("Unexpected exception while attempting " + "to delete old file '%s'".format(file_obj.name)) + + storage = file_obj.storage + if storage and storage.exists(file_obj.name): + connection.on_commit(delete_from_storage) + + +def _get_file_fields(instance): + return filter( + lambda field: isinstance(field, models.FileField), + instance._meta.fields, + ) + + +def remove_files_on_change(sender, instance, **kwargs): + if not instance.pk: + return + + try: + old_instance = sender.objects.get(pk=instance.pk) + except instance.DoesNotExist: + return + + for field in _get_file_fields(instance): + old_file = getattr(old_instance, field.name) + new_file = getattr(instance, field.name) + + if old_file and old_file != new_file: + _delete_file(old_file) + + +def remove_files_on_delete(sender, instance, **kwargs): + for field in _get_file_fields(instance): + file_to_delete = getattr(instance, field.name) + + if file_to_delete: + _delete_file(file_to_delete) + + +def connect_cleanup_files_signals(): + connections = ConnectionHandler() + backend = connections[DEFAULT_DB_ALIAS] + + for model in _find_models_with_filefield(): + pre_save.connect(remove_files_on_change, sender=model) + post_delete.connect(remove_files_on_delete, sender=model) diff --git a/taiga/base/signals/thumbnails.py b/taiga/base/signals/thumbnails.py index 5709a59b..53bc1bca 100644 --- a/taiga/base/signals/thumbnails.py +++ b/taiga/base/signals/thumbnails.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django_transactional_cleanup.signals import cleanup_post_delete +from .cleanup_files import cleanup_post_delete from easy_thumbnails.files import get_thumbnailer diff --git a/taiga/events/apps.py b/taiga/events/apps.py index 13da3d83..6b4c6a59 100644 --- a/taiga/events/apps.py +++ b/taiga/events/apps.py @@ -19,15 +19,16 @@ import sys from django.apps import AppConfig from django.db.models import signals -from . import signal_handlers as handlers def connect_events_signals(): + from . import signal_handlers as handlers signals.post_save.connect(handlers.on_save_any_model, dispatch_uid="events_change") signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete") def disconnect_events_signals(): + from . import signal_handlers as handlers signals.post_save.disconnect(dispatch_uid="events_change") signals.post_delete.disconnect(dispatch_uid="events_delete") diff --git a/taiga/feedback/apps.py b/taiga/feedback/apps.py index 505b924b..b48a3cfb 100644 --- a/taiga/feedback/apps.py +++ b/taiga/feedback/apps.py @@ -20,8 +20,6 @@ from django.apps import apps from django.conf import settings from django.conf.urls import include, url -from .routers import router - class FeedbackAppConfig(AppConfig): name = "taiga.feedback" @@ -30,4 +28,5 @@ class FeedbackAppConfig(AppConfig): def ready(self): if settings.FEEDBACK_ENABLED: from taiga.urls import urlpatterns + from .routers import router urlpatterns.append(url(r'^api/v1/', include(router.urls))) diff --git a/taiga/mdrender/__init__.py b/taiga/mdrender/__init__.py index 11dbddfa..e69de29b 100644 --- a/taiga/mdrender/__init__.py +++ b/taiga/mdrender/__init__.py @@ -1,18 +0,0 @@ -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from .service import * diff --git a/taiga/mdrender/extensions/wikilinks.py b/taiga/mdrender/extensions/wikilinks.py index d3ac7030..61c36d5c 100644 --- a/taiga/mdrender/extensions/wikilinks.py +++ b/taiga/mdrender/extensions/wikilinks.py @@ -7,85 +7,85 @@ # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -# +# # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -from markdown import Extension -from markdown.inlinepatterns import Pattern -from markdown.treeprocessors import Treeprocessor - -from markdown.util import etree - -from taiga.front.templatetags.functions import resolve -from taiga.base.utils.slug import slugify - -import re - - -class WikiLinkExtension(Extension): - def __init__(self, project, *args, **kwargs): - self.project = project - return super().__init__(*args, **kwargs) - - def extendMarkdown(self, md, md_globals): - WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]" - md.inlinePatterns.add("wikilinks", - WikiLinksPattern(md, WIKILINK_RE, self.project), - " . from django.contrib import admin -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.admin import GenericTabularInline from . import models @@ -38,7 +38,7 @@ class AttachmentAdmin(admin.ModelAdmin): return super().formfield_for_foreignkey(db_field, request, **kwargs) -class AttachmentInline(generic.GenericTabularInline): +class AttachmentInline(GenericTabularInline): model = models.Attachment fields = ("attached_file", "owner") extra = 0 diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index 9b01cfca..fa4b337e 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -24,7 +24,7 @@ from unidecode import unidecode from django.db import models from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.translation import ugettext_lazy as _ @@ -57,7 +57,7 @@ class Attachment(models.Model): verbose_name=_("content type")) object_id = models.PositiveIntegerField(null=False, blank=False, verbose_name=_("object id")) - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), default=timezone.now) diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index 7c9352f7..26d58b18 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -19,12 +19,11 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from taiga.projects import signals as generic_handlers -from taiga.projects.custom_attributes import signals as custom_attributes_handlers -from . import signals as handlers - def connect_issues_signals(): + from taiga.projects import signals as generic_handlers + from . import signals as handlers + # Finished date signals.pre_save.connect(handlers.set_finished_date_when_edit_issue, sender=apps.get_model("issues", "Issue"), @@ -43,6 +42,8 @@ def connect_issues_signals(): def connect_issues_custom_attributes_signals(): + from taiga.projects.custom_attributes import signals as custom_attributes_handlers + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue, sender=apps.get_model("issues", "Issue"), dispatch_uid="create_custom_attribute_value_when_create_issue") diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 3faac676..df11f671 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils import timezone from django.dispatch import receiver @@ -63,7 +63,7 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None, related_name="issues_assigned_to_me", verbose_name=_("assigned to")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") external_reference = TextArrayField(default=None, verbose_name=_("external reference")) _importing = None diff --git a/taiga/projects/likes/models.py b/taiga/projects/likes/models.py index d5c4119f..078f48e3 100644 --- a/taiga/projects/likes/models.py +++ b/taiga/projects/likes/models.py @@ -17,7 +17,7 @@ # along with this program. If not, see . from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _ class Like(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, related_name="likes", verbose_name=_("user")) created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py index ef56ace7..8eb0db27 100644 --- a/taiga/projects/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -16,7 +16,8 @@ # along with this program. If not, see . from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey + from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -80,7 +81,7 @@ class HistoryChangeNotification(models.Model): class Watched(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, related_name="watched", verbose_name=_("user")) created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index b7223fba..86f5cb27 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -102,13 +102,13 @@ def analize_object_for_watchers(obj:object, comment:str, user:object): if not hasattr(obj, "add_watcher"): return - from taiga import mdrender as mdr texts = (getattr(obj, "description", ""), getattr(obj, "content", ""), comment,) - _, data = mdr.render_and_extract(obj.get_project(), "\n".join(texts)) + from taiga.mdrender.service import render_and_extract + _, data = render_and_extract(obj.get_project(), "\n".join(texts)) if data["mentions"]: for user in data["mentions"]: diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py index f8b196d9..bf3b072c 100644 --- a/taiga/projects/references/models.py +++ b/taiga/projects/references/models.py @@ -18,7 +18,7 @@ from django.db import models from django.utils import timezone from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.generic import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 3284ef1d..ad0209a0 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -19,11 +19,10 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from taiga.projects import signals as generic_handlers -from taiga.projects.custom_attributes import signals as custom_attributes_handlers -from . import signals as handlers def connect_tasks_signals(): + from taiga.projects import signals as generic_handlers + from . import signals as handlers # Finished date signals.pre_save.connect(handlers.set_finished_date_when_edit_task, sender=apps.get_model("tasks", "Task"), @@ -40,6 +39,7 @@ def connect_tasks_signals(): dispatch_uid="update_project_tags_when_delete_tagglabe_item_task") def connect_tasks_close_or_open_us_and_milestone_signals(): + from . import signals as handlers # Cached prev object version signals.pre_save.connect(handlers.cached_prev_task, sender=apps.get_model("tasks", "Task"), @@ -53,6 +53,7 @@ def connect_tasks_close_or_open_us_and_milestone_signals(): dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") def connect_tasks_custom_attributes_signals(): + from taiga.projects.custom_attributes import signals as custom_attributes_handlers signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task, sender=apps.get_model("tasks", "Task"), dispatch_uid="create_custom_attribute_value_when_create_task") diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 0340f8d6..a1e945f1 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -62,7 +62,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None, related_name="tasks_assigned_to_me", verbose_name=_("assigned to")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") is_iocaine = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is iocaine")) external_reference = TextArrayField(default=None, verbose_name=_("external reference")) diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index c86f6a99..3586ee57 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -19,51 +19,50 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from taiga.projects import signals as generic_handlers -from taiga.projects.custom_attributes import signals as custom_attributes_handlers -from . import signals as handlers - def connect_userstories_signals(): - # Cached prev object version - signals.pre_save.connect(handlers.cached_prev_us, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="cached_prev_us") + from taiga.projects import signals as generic_handlers + from . import signals as handlers + # Cached prev object version + signals.pre_save.connect(handlers.cached_prev_us, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="cached_prev_us") - # Role Points - signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_role_points_when_create_or_edit_us") + # Role Points + signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="update_role_points_when_create_or_edit_us") - # Tasks - signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_milestone_of_tasks_when_edit_us") + # Tasks + signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="update_milestone_of_tasks_when_edit_us") - # Open/Close US and Milestone - signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_us, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") - signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_us, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="try_to_close_milestone_when_delete_us") + # Open/Close US and Milestone + signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_us, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") + signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_us, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="try_to_close_milestone_when_delete_us") - # Tags - signals.pre_save.connect(generic_handlers.tags_normalization, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="tags_normalization_user_story") - signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") - signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") + # Tags + signals.pre_save.connect(generic_handlers.tags_normalization, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="tags_normalization_user_story") + signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") + signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") def connect_userstories_custom_attributes_signals(): - signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="create_custom_attribute_value_when_create_user_story") + from taiga.projects.custom_attributes import signals as custom_attributes_handlers + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="create_custom_attribute_value_when_create_user_story") def connect_all_userstories_signals(): @@ -72,18 +71,18 @@ def connect_all_userstories_signals(): def disconnect_userstories_signals(): - signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") - signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us") - signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") - signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") + signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") + signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us") + signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") + signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") def disconnect_userstories_custom_attributes_signals(): - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story") def disconnect_all_userstories_signals(): diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 6c561c07..4a046524 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -69,7 +69,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod related_name="user_stories", verbose_name=_("status"), on_delete=models.SET_NULL) is_closed = models.BooleanField(default=False) - points = models.ManyToManyField("projects.Points", null=False, blank=False, + points = models.ManyToManyField("projects.Points", blank=False, related_name="userstories", through="RolePoints", verbose_name=_("points")) @@ -97,7 +97,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod verbose_name=_("is client requirement")) team_requirement = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is team requirement")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") generated_from_issue = models.ForeignKey("issues.Issue", null=True, blank=True, on_delete=models.SET_NULL, related_name="generated_user_stories", diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py index 85152cbb..03c8976f 100644 --- a/taiga/projects/votes/models.py +++ b/taiga/projects/votes/models.py @@ -17,7 +17,7 @@ # along with this program. If not, see . from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _ class Votes(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count")) class Meta: @@ -46,7 +46,7 @@ class Votes(models.Model): class Vote(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, related_name="votes", verbose_name=_("user")) created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index abbdf44d..c3e20e4e 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -41,7 +41,7 @@ class WikiPage(OCCModelMixin, WatchedModelMixin, models.Model): default=timezone.now) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") _importing = None class Meta: diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index 898e3aa9..3cdfbc8c 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -19,16 +19,17 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from . import signals as handlers -from taiga.projects.history.models import HistoryEntry - class TimelineAppConfig(AppConfig): name = "taiga.timeline" verbose_name = "Timeline" def ready(self): - signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="timeline") + from . import signals as handlers + + signals.post_save.connect(handlers.on_new_history_entry, + sender=apps.get_model("history", "HistoryEntry"), + dispatch_uid="timeline") signals.pre_save.connect(handlers.create_membership_push_to_timeline, sender=apps.get_model("projects", "Membership")) signals.post_delete.connect(handlers.delete_membership_push_to_timeline, diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index d13db447..73eef400 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -22,7 +22,7 @@ from django.utils import timezone from django.core.exceptions import ValidationError from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.generic import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey from taiga.projects.models import Project diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py index f6dda872..90890262 100644 --- a/taiga/webhooks/apps.py +++ b/taiga/webhooks/apps.py @@ -15,15 +15,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps from django.apps import AppConfig from django.db.models import signals -from . import signal_handlers as handlers -from taiga.projects.history.models import HistoryEntry - def connect_webhooks_signals(): - signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="webhooks") + from . import signal_handlers as handlers + signals.post_save.connect(handlers.on_new_history_entry, + sender=apps.get_model("history", "HistoryEntry"), + dispatch_uid="webhooks") def disconnect_webhooks_signals(): diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index e9f94e0c..3e656caa 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -10,35 +10,36 @@ pytestmark = pytest.mark.django_db(transaction=True) import factory -class TestingProjectModel(models.Model): +class AuxProjectModel(models.Model): pass -class TestingModelWithNameAttribute(models.Model): +class AuxModelWithNameAttribute(models.Model): name = models.CharField(max_length=255, null=False, blank=False) - project = models.ForeignKey(TestingProjectModel, null=False, blank=False) + project = models.ForeignKey(AuxProjectModel, null=False, blank=False) -class TestingSerializer(ValidateDuplicatedNameInProjectMixin): +class AuxSerializer(ValidateDuplicatedNameInProjectMixin): class Meta: - model = TestingModelWithNameAttribute + model = AuxModelWithNameAttribute + def test_duplicated_name_validation(): - project = TestingProjectModel.objects.create() - instance_1 = TestingModelWithNameAttribute.objects.create(name="1", project=project) - instance_2 = TestingModelWithNameAttribute.objects.create(name="2", project=project) + project = AuxProjectModel.objects.create() + instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project) + instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project) # No duplicated_name - serializer = TestingSerializer(data={"name": "3", "project": project.id}) + serializer = AuxSerializer(data={"name": "3", "project": project.id}) assert serializer.is_valid() # Create duplicated_name - serializer = TestingSerializer(data={"name": "1", "project": project.id}) + serializer = AuxSerializer(data={"name": "1", "project": project.id}) assert not serializer.is_valid() # Update name to existing one - serializer = TestingSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) + serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) assert not serializer.is_valid()