Update django to 1.9 and the rest of requirements

remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-02-10 12:47:41 +01:00
parent 010fcfa635
commit 7b7548b47d
38 changed files with 338 additions and 245 deletions

View File

@ -1,8 +1,8 @@
-r requirements.txt -r requirements.txt
factory_boy==2.6.0 factory_boy==2.6.1
py==1.4.31 py==1.4.31
pytest==2.8.5 pytest==2.8.7
pytest-django==2.9.1 pytest-django==2.9.1
pytest-pythonpath==0.7 pytest-pythonpath==0.7

View File

@ -1,37 +1,36 @@
Django==1.8.6 Django==1.9.2
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7 #djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
django-picklefield==0.3.2 django-picklefield==0.3.2
django-sampledatahelper==0.3.0 django-sampledatahelper==0.4.0
gunicorn==19.3.0 gunicorn==19.4.5
psycopg2==2.6.1 psycopg2==2.6.1
Pillow==2.9.0 Pillow==3.1.1
pytz==2015.7 pytz==2015.7
six==1.10.0 six==1.10.0
amqp==1.4.7 amqp==1.4.9
djmail==0.11 djmail==0.12.0.post1
django-pgjson==0.3.1 django-pgjson==0.3.1
djorm-pgarray==1.2 djorm-pgarray==1.2
django-jinja==2.1.1 django-jinja==2.1.2
jinja2==2.8 jinja2==2.8
pygments==2.0.2 pygments==2.0.2
django-sites==0.8 django-sites==0.9
Markdown==2.6.5 Markdown==2.6.5
fn==0.4.3 fn==0.4.3
diff-match-patch==20121119 diff-match-patch==20121119
requests==2.8.1 requests==2.9.1
django-sr==0.0.4 django-sr==0.0.4
easy-thumbnails==2.2.1 easy-thumbnails==2.3
celery==3.1.19 celery==3.1.20
redis==2.10.5 redis==2.10.5
Unidecode==0.04.18 Unidecode==0.04.19
raven==5.9.2 raven==5.10.2
bleach==1.4.2 bleach==1.4.2
django-ipware==1.1.2 django-ipware==1.1.3
premailer==2.9.6 premailer==2.9.7
cssutils==1.0.1 # Compatible with python 3.5 cssutils==1.0.1 # Compatible with python 3.5
django-transactional-cleanup==0.1.15
lxml==3.5.0 lxml==3.5.0
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
pyjwkest==1.0.9 pyjwkest==1.1.5
python-dateutil==2.4.2 python-dateutil==2.4.2
netaddr==0.7.18 netaddr==0.7.18

View File

@ -30,7 +30,7 @@ DEBUG = False
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql",
"NAME": "taiga", "NAME": "taiga",
} }
} }
@ -320,7 +320,6 @@ INSTALLED_APPS = [
"sr", "sr",
"easy_thumbnails", "easy_thumbnails",
"raven.contrib.django.raven_compat", "raven.contrib.django.raven_compat",
"django_transactional_cleanup",
] ]
WSGI_APPLICATION = "taiga.wsgi.application" WSGI_APPLICATION = "taiga.wsgi.application"
@ -347,7 +346,7 @@ LOGGING = {
"handlers": { "handlers": {
"null": { "null": {
"level":"DEBUG", "level":"DEBUG",
"class":"django.utils.log.NullHandler", "class":"logging.NullHandler",
}, },
"console":{ "console":{
"level":"DEBUG", "level":"DEBUG",

View File

@ -24,7 +24,7 @@ from .development import *
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'taiga', 'NAME': 'taiga',
'USER': 'taiga', 'USER': 'taiga',
'PASSWORD': 'changeme', 'PASSWORD': 'changeme',

View File

@ -64,17 +64,17 @@ from django.utils.encoding import is_protected_type
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
from . import ISO_8601 from . import ISO_8601
from .settings import api_settings from .settings import api_settings
from collections import OrderedDict
from decimal import Decimal, DecimalException
import copy import copy
import datetime import datetime
import inspect import inspect
import re import re
import warnings import warnings
from decimal import Decimal, DecimalException
def is_non_str_iterable(obj): def is_non_str_iterable(obj):
@ -255,7 +255,7 @@ class Field(object):
return [self.to_native(item) for item in value] return [self.to_native(item) for item in value]
elif isinstance(value, dict): elif isinstance(value, dict):
# Make sure we preserve field ordering, if it exists # Make sure we preserve field ordering, if it exists
ret = SortedDict() ret = OrderedDict()
for key, val in value.items(): for key, val in value.items():
ret[key] = self.to_native(val) ret[key] = self.to_native(val)
return ret return ret
@ -270,7 +270,7 @@ class Field(object):
return {} return {}
def metadata(self): def metadata(self):
metadata = SortedDict() metadata = OrderedDict()
metadata["type"] = self.type_label metadata["type"] = self.type_label
metadata["required"] = getattr(self, "required", False) metadata["required"] = getattr(self, "required", False)
optional_attrs = ["read_only", "label", "help_text", optional_attrs = ["read_only", "label", "help_text",

View File

@ -59,11 +59,11 @@ from django.core.paginator import Page
from django.db import models from django.db import models
from django.forms import widgets from django.forms import widgets
from django.utils import six from django.utils import six
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .settings import api_settings from .settings import api_settings
from collections import OrderedDict
import copy import copy
import datetime import datetime
import inspect import inspect
@ -148,7 +148,7 @@ class DictWithMetadata(dict):
return dict(self) return dict(self)
class SortedDictWithMetadata(SortedDict): class OrderedDictWithMetadata(OrderedDict):
""" """
A sorted dict-like object, that can have additional properties attached. 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 Overriden to remove the metadata from the dict, since it shouldn't be
pickle and may in some instances be unpickleable. pickle and may in some instances be unpickleable.
""" """
return SortedDict(self).__dict__ return OrderedDict(self).__dict__
def _is_protected_type(obj): def _is_protected_type(obj):
@ -194,7 +194,7 @@ def _get_declared_fields(bases, attrs):
if hasattr(base, "base_fields"): if hasattr(base, "base_fields"):
fields = list(base.base_fields.items()) + fields fields = list(base.base_fields.items()) + fields
return SortedDict(fields) return OrderedDict(fields)
class SerializerMetaclass(type): class SerializerMetaclass(type):
@ -222,7 +222,7 @@ class BaseSerializer(WritableField):
pass pass
_options_class = SerializerOptions _options_class = SerializerOptions
_dict_class = SortedDictWithMetadata _dict_class = OrderedDictWithMetadata
def __init__(self, instance=None, data=None, files=None, def __init__(self, instance=None, data=None, files=None,
context=None, partial=False, many=None, context=None, partial=False, many=None,
@ -268,7 +268,7 @@ class BaseSerializer(WritableField):
This will be the set of any explicitly declared fields, This will be the set of any explicitly declared fields,
plus the set of fields returned by get_default_fields(). plus the set of fields returned by get_default_fields().
""" """
ret = SortedDict() ret = OrderedDict()
# Get the explicitly declared fields # Get the explicitly declared fields
base_fields = copy.deepcopy(self.base_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 "fields" is specified, use those fields, in that order.
if self.opts.fields: if self.opts.fields:
assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple" assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple"
new = SortedDict() new = OrderedDict()
for key in self.opts.fields: for key in self.opts.fields:
new[key] = ret[key] new[key] = ret[key]
ret = new ret = new
@ -458,7 +458,10 @@ class BaseSerializer(WritableField):
many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type)) many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type))
if many: 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) return self.to_native(value)
def field_from_native(self, data, files, field_name, into): def field_from_native(self, data, files, field_name, into):
@ -610,7 +613,10 @@ class BaseSerializer(WritableField):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
if many: 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: else:
self._data = self.to_native(obj) self._data = self.to_native(obj)
@ -645,7 +651,7 @@ class BaseSerializer(WritableField):
Useful for things like responding to OPTIONS requests, or generating Useful for things like responding to OPTIONS requests, or generating
API schemas for auto-documentation. API schemas for auto-documentation.
""" """
return SortedDict( return OrderedDict(
[(field_name, field.metadata()) [(field_name, field.metadata())
for field_name, field in six.iteritems(self.fields)] 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, \ assert cls is not None, \
"Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__ "Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__
opts = cls._meta.concrete_model._meta opts = cls._meta.concrete_model._meta
ret = SortedDict() ret = OrderedDict()
nested = bool(self.opts.depth) nested = bool(self.opts.depth)
# Deal with adding the primary key field # Deal with adding the primary key field

View File

@ -62,9 +62,10 @@ back to the defaults.
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.utils import importlib
from django.utils import six from django.utils import six
import importlib
from . import ISO_8601 from . import ISO_8601

View File

@ -45,13 +45,10 @@
Helper classes for parsers. Helper classes for parsers.
""" """
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime import datetime
import decimal import decimal
import types import types

View File

@ -43,6 +43,8 @@
import json import json
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse 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.decorators.csrf import csrf_exempt
from django.views.defaults import server_error from django.views.defaults import server_error
from django.views.generic import View from django.views.generic import View
from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _ 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 # By default we can't provide any form-like information, however the
# generic views override this implementation and add additional # generic views override this implementation and add additional
# information for POST and PUT methods, based on the serializer. # information for POST and PUT methods, based on the serializer.
ret = SortedDict() ret = OrderedDict()
ret['name'] = self.get_view_name() ret['name'] = self.get_view_name()
ret['description'] = self.get_view_description() ret['description'] = self.get_view_description()
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes] ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]

View File

@ -17,12 +17,14 @@
from django.apps import AppConfig from django.apps import AppConfig
from .signals.thumbnails import connect_thumbnail_signals
class BaseAppConfig(AppConfig): class BaseAppConfig(AppConfig):
name = "taiga.base" name = "taiga.base"
verbose_name = "Base App Config" verbose_name = "Base App Config"
def ready(self): def ready(self):
from .signals.thumbnails import connect_thumbnail_signals
from .signals.cleanup_files import connect_cleanup_files_signals
connect_thumbnail_signals() connect_thumbnail_signals()
connect_cleanup_files_signals()

View File

@ -19,7 +19,7 @@ import datetime
from optparse import make_option 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.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
@ -163,7 +163,7 @@ class Command(BaseCommand):
} }
for notification_email in notification_emails: for notification_email in notification_emails:
model = get_model(*notification_email[0].split(".")) model = apps.get_model(*notification_email[0].split("."))
snapshot = { snapshot = {
"subject": "Tests subject", "subject": "Tests subject",
"ref": 123123, "ref": 123123,

View File

@ -43,9 +43,10 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""The various HTTP responses for use in returning proper HTTP codes.""" """The various HTTP responses for use in returning proper HTTP codes."""
from http.client import responses
from django import http from django import http
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse from django.template.response import SimpleTemplateResponse
from django.utils import six from django.utils import six
@ -114,7 +115,7 @@ class Response(SimpleTemplateResponse):
""" """
# TODO: Deprecate and use a template tag instead # TODO: Deprecate and use a template tag instead
# TODO: Status code text for RFC 6585 status codes # 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): def __getstate__(self):
""" """

View File

@ -0,0 +1,97 @@
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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.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)

View File

@ -15,7 +15,7 @@
# 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_transactional_cleanup.signals import cleanup_post_delete from .cleanup_files import cleanup_post_delete
from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.files import get_thumbnailer

View File

@ -19,15 +19,16 @@ import sys
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models import signals from django.db.models import signals
from . import signal_handlers as handlers
def connect_events_signals(): 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_save.connect(handlers.on_save_any_model, dispatch_uid="events_change")
signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete") signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete")
def disconnect_events_signals(): def disconnect_events_signals():
from . import signal_handlers as handlers
signals.post_save.disconnect(dispatch_uid="events_change") signals.post_save.disconnect(dispatch_uid="events_change")
signals.post_delete.disconnect(dispatch_uid="events_delete") signals.post_delete.disconnect(dispatch_uid="events_delete")

View File

@ -20,8 +20,6 @@ from django.apps import apps
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from .routers import router
class FeedbackAppConfig(AppConfig): class FeedbackAppConfig(AppConfig):
name = "taiga.feedback" name = "taiga.feedback"
@ -30,4 +28,5 @@ class FeedbackAppConfig(AppConfig):
def ready(self): def ready(self):
if settings.FEEDBACK_ENABLED: if settings.FEEDBACK_ENABLED:
from taiga.urls import urlpatterns from taiga.urls import urlpatterns
from .routers import router
urlpatterns.append(url(r'^api/v1/', include(router.urls))) urlpatterns.append(url(r'^api/v1/', include(router.urls)))

View File

@ -1,18 +0,0 @@
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 .service import *

View File

@ -7,85 +7,85 @@
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the # published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. # License, or (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details. # GNU Affero General Public License for more details.
# #
# 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 markdown import Extension from markdown import Extension
from markdown.inlinepatterns import Pattern from markdown.inlinepatterns import Pattern
from markdown.treeprocessors import Treeprocessor from markdown.treeprocessors import Treeprocessor
from markdown.util import etree from markdown.util import etree
from taiga.front.templatetags.functions import resolve from taiga.front.templatetags.functions import resolve
from taiga.base.utils.slug import slugify from taiga.base.utils.slug import slugify
import re import re
class WikiLinkExtension(Extension): class WikiLinkExtension(Extension):
def __init__(self, project, *args, **kwargs): def __init__(self, project, *args, **kwargs):
self.project = project self.project = project
return super().__init__(*args, **kwargs) return super().__init__(*args, **kwargs)
def extendMarkdown(self, md, md_globals): def extendMarkdown(self, md, md_globals):
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]" WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
md.inlinePatterns.add("wikilinks", md.inlinePatterns.add("wikilinks",
WikiLinksPattern(md, WIKILINK_RE, self.project), WikiLinksPattern(md, WIKILINK_RE, self.project),
"<not_strong") "<not_strong")
md.treeprocessors.add("relative_to_absolute_links", md.treeprocessors.add("relative_to_absolute_links",
RelativeLinksTreeprocessor(md, self.project), RelativeLinksTreeprocessor(md, self.project),
"<prettify") "<prettify")
class WikiLinksPattern(Pattern): class WikiLinksPattern(Pattern):
def __init__(self, md, pattern, project): def __init__(self, md, pattern, project):
self.project = project self.project = project
self.md = md self.md = md
super().__init__(pattern) super().__init__(pattern)
def handleMatch(self, m): def handleMatch(self, m):
label = m.group(2).strip() label = m.group(2).strip()
url = resolve("wiki", self.project.slug, slugify(label)) url = resolve("wiki", self.project.slug, slugify(label))
if m.group(3): if m.group(3):
title = m.group(3).strip()[1:] title = m.group(3).strip()[1:]
else: else:
title = label title = label
a = etree.Element("a") a = etree.Element("a")
a.text = title a.text = title
a.set("href", url) a.set("href", url)
a.set("title", title) a.set("title", title)
a.set("class", "reference wiki") a.set("class", "reference wiki")
return a return a
SLUG_RE = re.compile(r"^[-a-zA-Z0-9_]+$") SLUG_RE = re.compile(r"^[-a-zA-Z0-9_]+$")
class RelativeLinksTreeprocessor(Treeprocessor): class RelativeLinksTreeprocessor(Treeprocessor):
def __init__(self, md, project): def __init__(self, md, project):
self.project = project self.project = project
super().__init__(md) super().__init__(md)
def run(self, root): def run(self, root):
links = root.getiterator("a") links = root.getiterator("a")
for a in links: for a in links:
href = a.get("href", "") href = a.get("href", "")
if SLUG_RE.search(href): if SLUG_RE.search(href):
# [wiki](wiki_page) -> <a href="FRONT_HOST/.../wiki/wiki_page" ... # [wiki](wiki_page) -> <a href="FRONT_HOST/.../wiki/wiki_page" ...
url = resolve("wiki", self.project.slug, href) url = resolve("wiki", self.project.slug, href)
a.set("href", url) a.set("href", url)
a.set("class", "reference wiki") a.set("class", "reference wiki")
elif href and href[0] == "/": elif href and href[0] == "/":
# [some link](/some/link) -> <a href="FRONT_HOST/some/link" ... # [some link](/some/link) -> <a href="FRONT_HOST/some/link" ...
url = "{}{}".format(resolve("home"), href[1:]) url = "{}{}".format(resolve("home"), href[1:])
a.set("href", url) a.set("href", url)

View File

@ -144,4 +144,5 @@ def get_diff_of_htmls(html1, html2):
diffutil.diff_cleanupSemantic(diffs) diffutil.diff_cleanupSemantic(diffs)
return diffutil.diff_pretty_html(diffs) return diffutil.diff_pretty_html(diffs)
__all__ = ["render", "get_diff_of_htmls", "render_and_extract"] __all__ = ["render", "get_diff_of_htmls", "render_and_extract"]

View File

@ -19,12 +19,11 @@ from django.apps import AppConfig
from django.apps import apps from django.apps import apps
from django.db.models import signals from django.db.models import signals
from . import signals as handlers
## Project Signals ## Project Signals
def connect_projects_signals(): def connect_projects_signals():
from . import signals as handlers
# On project object is created apply template. # On project object is created apply template.
signals.post_save.connect(handlers.project_post_save, signals.post_save.connect(handlers.project_post_save,
sender=apps.get_model("projects", "Project"), sender=apps.get_model("projects", "Project"),
@ -51,6 +50,7 @@ def disconnect_projects_signals():
## Memberships Signals ## Memberships Signals
def connect_memberships_signals(): def connect_memberships_signals():
from . import signals as handlers
# On membership object is deleted, update role-points relation. # On membership object is deleted, update role-points relation.
signals.pre_delete.connect(handlers.membership_post_delete, signals.pre_delete.connect(handlers.membership_post_delete,
sender=apps.get_model("projects", "Membership"), sender=apps.get_model("projects", "Membership"),
@ -71,6 +71,7 @@ def disconnect_memberships_signals():
## US Statuses Signals ## US Statuses Signals
def connect_us_status_signals(): def connect_us_status_signals():
from . import signals as handlers
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status, signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status,
sender=apps.get_model("projects", "UserStoryStatus"), sender=apps.get_model("projects", "UserStoryStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status") dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
@ -85,6 +86,7 @@ def disconnect_us_status_signals():
## Tasks Statuses Signals ## Tasks Statuses Signals
def connect_task_status_signals(): def connect_task_status_signals():
from . import signals as handlers
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status, signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status,
sender=apps.get_model("projects", "TaskStatus"), sender=apps.get_model("projects", "TaskStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status") dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")

View File

@ -16,7 +16,7 @@
# 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
from django.contrib.contenttypes import generic from django.contrib.contenttypes.admin import GenericTabularInline
from . import models from . import models
@ -38,7 +38,7 @@ class AttachmentAdmin(admin.ModelAdmin):
return super().formfield_for_foreignkey(db_field, request, **kwargs) return super().formfield_for_foreignkey(db_field, request, **kwargs)
class AttachmentInline(generic.GenericTabularInline): class AttachmentInline(GenericTabularInline):
model = models.Attachment model = models.Attachment
fields = ("attached_file", "owner") fields = ("attached_file", "owner")
extra = 0 extra = 0

View File

@ -24,7 +24,7 @@ from unidecode import unidecode
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType 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 import timezone
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -57,7 +57,7 @@ class Attachment(models.Model):
verbose_name=_("content type")) verbose_name=_("content type"))
object_id = models.PositiveIntegerField(null=False, blank=False, object_id = models.PositiveIntegerField(null=False, blank=False,
verbose_name=_("object id")) 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, created_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("created date"), verbose_name=_("created date"),
default=timezone.now) default=timezone.now)

View File

@ -19,12 +19,11 @@ from django.apps import AppConfig
from django.apps import apps from django.apps import apps
from django.db.models import signals 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(): def connect_issues_signals():
from taiga.projects import signals as generic_handlers
from . import signals as handlers
# Finished date # Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue, signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
sender=apps.get_model("issues", "Issue"), sender=apps.get_model("issues", "Issue"),
@ -43,6 +42,8 @@ def connect_issues_signals():
def connect_issues_custom_attributes_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, signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue,
sender=apps.get_model("issues", "Issue"), sender=apps.get_model("issues", "Issue"),
dispatch_uid="create_custom_attribute_value_when_create_issue") dispatch_uid="create_custom_attribute_value_when_create_issue")

View File

@ -16,7 +16,7 @@
# 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.contrib.contenttypes import generic from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.dispatch import receiver 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, assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
default=None, related_name="issues_assigned_to_me", default=None, related_name="issues_assigned_to_me",
verbose_name=_("assigned to")) verbose_name=_("assigned to"))
attachments = generic.GenericRelation("attachments.Attachment") attachments = GenericRelation("attachments.Attachment")
external_reference = TextArrayField(default=None, verbose_name=_("external reference")) external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
_importing = None _importing = None

View File

@ -17,7 +17,7 @@
# 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.conf import settings 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.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _
class Like(models.Model): class Like(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType") content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField() 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, user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="likes", verbose_name=_("user")) related_name="likes", verbose_name=_("user"))
created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,

View File

@ -16,7 +16,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.conf import settings 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.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -80,7 +81,7 @@ class HistoryChangeNotification(models.Model):
class Watched(models.Model): class Watched(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType") content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField() 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, user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False,
related_name="watched", verbose_name=_("user")) related_name="watched", verbose_name=_("user"))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,

View File

@ -102,13 +102,13 @@ def analize_object_for_watchers(obj:object, comment:str, user:object):
if not hasattr(obj, "add_watcher"): if not hasattr(obj, "add_watcher"):
return return
from taiga import mdrender as mdr
texts = (getattr(obj, "description", ""), texts = (getattr(obj, "description", ""),
getattr(obj, "content", ""), getattr(obj, "content", ""),
comment,) 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"]: if data["mentions"]:
for user in data["mentions"]: for user in data["mentions"]:

View File

@ -18,7 +18,7 @@
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.contrib.contenttypes.models import ContentType 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.userstories.models import UserStory
from taiga.projects.tasks.models import Task from taiga.projects.tasks.models import Task

View File

@ -19,11 +19,10 @@ from django.apps import AppConfig
from django.apps import apps from django.apps import apps
from django.db.models import signals 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(): def connect_tasks_signals():
from taiga.projects import signals as generic_handlers
from . import signals as handlers
# Finished date # Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_task, signals.pre_save.connect(handlers.set_finished_date_when_edit_task,
sender=apps.get_model("tasks", "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") dispatch_uid="update_project_tags_when_delete_tagglabe_item_task")
def connect_tasks_close_or_open_us_and_milestone_signals(): def connect_tasks_close_or_open_us_and_milestone_signals():
from . import signals as handlers
# Cached prev object version # Cached prev object version
signals.pre_save.connect(handlers.cached_prev_task, signals.pre_save.connect(handlers.cached_prev_task,
sender=apps.get_model("tasks", "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") dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
def connect_tasks_custom_attributes_signals(): 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, signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task,
sender=apps.get_model("tasks", "Task"), sender=apps.get_model("tasks", "Task"),
dispatch_uid="create_custom_attribute_value_when_create_task") dispatch_uid="create_custom_attribute_value_when_create_task")

View File

@ -16,7 +16,7 @@
# 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.contrib.contenttypes import generic from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ 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, assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
default=None, related_name="tasks_assigned_to_me", default=None, related_name="tasks_assigned_to_me",
verbose_name=_("assigned to")) verbose_name=_("assigned to"))
attachments = generic.GenericRelation("attachments.Attachment") attachments = GenericRelation("attachments.Attachment")
is_iocaine = models.BooleanField(default=False, null=False, blank=True, is_iocaine = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is iocaine")) verbose_name=_("is iocaine"))
external_reference = TextArrayField(default=None, verbose_name=_("external reference")) external_reference = TextArrayField(default=None, verbose_name=_("external reference"))

View File

@ -19,51 +19,50 @@ from django.apps import AppConfig
from django.apps import apps from django.apps import apps
from django.db.models import signals 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(): def connect_userstories_signals():
# Cached prev object version from taiga.projects import signals as generic_handlers
signals.pre_save.connect(handlers.cached_prev_us, from . import signals as handlers
sender=apps.get_model("userstories", "UserStory"), # Cached prev object version
dispatch_uid="cached_prev_us") signals.pre_save.connect(handlers.cached_prev_us,
sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="cached_prev_us")
# Role Points # Role Points
signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us, signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="update_role_points_when_create_or_edit_us") dispatch_uid="update_role_points_when_create_or_edit_us")
# Tasks # Tasks
signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us, signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="update_milestone_of_tasks_when_edit_us") dispatch_uid="update_milestone_of_tasks_when_edit_us")
# Open/Close US and Milestone # Open/Close US and Milestone
signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_us, signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_us,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") 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, signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_us,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="try_to_close_milestone_when_delete_us") dispatch_uid="try_to_close_milestone_when_delete_us")
# Tags # Tags
signals.pre_save.connect(generic_handlers.tags_normalization, signals.pre_save.connect(generic_handlers.tags_normalization,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="tags_normalization_user_story") dispatch_uid="tags_normalization_user_story")
signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") 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, signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
sender=apps.get_model("userstories", "UserStory"), sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
def connect_userstories_custom_attributes_signals(): def connect_userstories_custom_attributes_signals():
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story, from taiga.projects.custom_attributes import signals as custom_attributes_handlers
sender=apps.get_model("userstories", "UserStory"), signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story,
dispatch_uid="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(): def connect_all_userstories_signals():
@ -72,18 +71,18 @@ def connect_all_userstories_signals():
def disconnect_userstories_signals(): def disconnect_userstories_signals():
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us") 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_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="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_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.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.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_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.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(): 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(): def disconnect_all_userstories_signals():

View File

@ -16,7 +16,7 @@
# 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.contrib.contenttypes import generic from django.contrib.contenttypes.fields import GenericRelation
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 _
from django.utils import timezone from django.utils import timezone
@ -69,7 +69,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
related_name="user_stories", verbose_name=_("status"), related_name="user_stories", verbose_name=_("status"),
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
is_closed = models.BooleanField(default=False) 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", related_name="userstories", through="RolePoints",
verbose_name=_("points")) verbose_name=_("points"))
@ -97,7 +97,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
verbose_name=_("is client requirement")) verbose_name=_("is client requirement"))
team_requirement = models.BooleanField(default=False, null=False, blank=True, team_requirement = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is team requirement")) 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, generated_from_issue = models.ForeignKey("issues.Issue", null=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="generated_user_stories", related_name="generated_user_stories",

View File

@ -17,7 +17,7 @@
# 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.conf import settings 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.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _
class Votes(models.Model): class Votes(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType") content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField() 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")) count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count"))
class Meta: class Meta:
@ -46,7 +46,7 @@ class Votes(models.Model):
class Vote(models.Model): class Vote(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType") content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField() 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, user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="votes", verbose_name=_("user")) related_name="votes", verbose_name=_("user"))
created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True,

View File

@ -16,7 +16,7 @@
# 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.contrib.contenttypes import generic from django.contrib.contenttypes.fields import GenericRelation
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 _
from django.utils import timezone from django.utils import timezone
@ -41,7 +41,7 @@ class WikiPage(OCCModelMixin, WatchedModelMixin, models.Model):
default=timezone.now) default=timezone.now)
modified_date = models.DateTimeField(null=False, blank=False, modified_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("modified date")) verbose_name=_("modified date"))
attachments = generic.GenericRelation("attachments.Attachment") attachments = GenericRelation("attachments.Attachment")
_importing = None _importing = None
class Meta: class Meta:

View File

@ -19,16 +19,17 @@ from django.apps import AppConfig
from django.apps import apps from django.apps import apps
from django.db.models import signals from django.db.models import signals
from . import signals as handlers
from taiga.projects.history.models import HistoryEntry
class TimelineAppConfig(AppConfig): class TimelineAppConfig(AppConfig):
name = "taiga.timeline" name = "taiga.timeline"
verbose_name = "Timeline" verbose_name = "Timeline"
def ready(self): 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, signals.pre_save.connect(handlers.create_membership_push_to_timeline,
sender=apps.get_model("projects", "Membership")) sender=apps.get_model("projects", "Membership"))
signals.post_delete.connect(handlers.delete_membership_push_to_timeline, signals.post_delete.connect(handlers.delete_membership_push_to_timeline,

View File

@ -22,7 +22,7 @@ from django.utils import timezone
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType 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 from taiga.projects.models import Project

View File

@ -15,15 +15,16 @@
# 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.apps import apps
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models import signals from django.db.models import signals
from . import signal_handlers as handlers
from taiga.projects.history.models import HistoryEntry
def connect_webhooks_signals(): 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(): def disconnect_webhooks_signals():

View File

@ -10,35 +10,36 @@ pytestmark = pytest.mark.django_db(transaction=True)
import factory import factory
class TestingProjectModel(models.Model): class AuxProjectModel(models.Model):
pass pass
class TestingModelWithNameAttribute(models.Model): class AuxModelWithNameAttribute(models.Model):
name = models.CharField(max_length=255, null=False, blank=False) 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: class Meta:
model = TestingModelWithNameAttribute model = AuxModelWithNameAttribute
def test_duplicated_name_validation(): def test_duplicated_name_validation():
project = TestingProjectModel.objects.create() project = AuxProjectModel.objects.create()
instance_1 = TestingModelWithNameAttribute.objects.create(name="1", project=project) instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project)
instance_2 = TestingModelWithNameAttribute.objects.create(name="2", project=project) instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project)
# No duplicated_name # No duplicated_name
serializer = TestingSerializer(data={"name": "3", "project": project.id}) serializer = AuxSerializer(data={"name": "3", "project": project.id})
assert serializer.is_valid() assert serializer.is_valid()
# Create duplicated_name # Create duplicated_name
serializer = TestingSerializer(data={"name": "1", "project": project.id}) serializer = AuxSerializer(data={"name": "1", "project": project.id})
assert not serializer.is_valid() assert not serializer.is_valid()
# Update name to existing one # 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() assert not serializer.is_valid()