diff --git a/settings/common.py b/settings/common.py index b4307383..d3ac205e 100644 --- a/settings/common.py +++ b/settings/common.py @@ -177,16 +177,16 @@ INSTALLED_APPS = [ "taiga.users", "taiga.userstorage", "taiga.projects", + "taiga.projects.references", + "taiga.projects.history", + "taiga.projects.notifications", "taiga.projects.attachments", + "taiga.projects.votes", "taiga.projects.milestones", "taiga.projects.userstories", "taiga.projects.tasks", "taiga.projects.issues", - "taiga.projects.references", "taiga.projects.wiki", - "taiga.projects.history", - "taiga.projects.notifications", - "taiga.projects.votes", "taiga.searches", "taiga.timeline", "taiga.mdrender", diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py index 6bc2c9ba..d0331fdf 100644 --- a/taiga/auth/backends.py +++ b/taiga/auth/backends.py @@ -36,7 +36,7 @@ import base64 import re from django.core import signing -from django.db.models import get_model +from django.apps import apps from rest_framework.authentication import BaseAuthentication from taiga.base import exceptions as exc @@ -85,7 +85,7 @@ def get_user_for_token(token): except signing.BadSignature: raise exc.NotAuthenticated("Invalid token") - model_cls = get_model("users", "User") + model_cls = apps.get_model("users", "User") try: user = model_cls.objects.get(pk=data["user_id"]) diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 5e22b4bc..72a40af2 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -23,7 +23,7 @@ should be contained in a class". Because of that, it not uses clasess and uses simple functions. """ -from django.db.models.loading import get_model +from django.apps import apps from django.db.models import Q from django.db import transaction as tx from django.db import IntegrityError @@ -68,7 +68,7 @@ def is_user_already_registred(*, username:str, email:str, github_id:int=None) -> Checks if a specified user is already registred. """ - user_model = get_model("users", "User") + user_model = apps.get_model("users", "User") or_expr = Q(username=username) | Q(email=email) if github_id: @@ -86,7 +86,7 @@ def get_membership_by_token(token:str): If not matches with any membership NotFound exception is raised. """ - membership_model = get_model("projects", "Membership") + membership_model = apps.get_model("projects", "Membership") qs = membership_model.objects.filter(token=token) if len(qs) == 0: raise exc.NotFound("Token not matches any valid invitation.") @@ -108,7 +108,7 @@ def public_register(username:str, password:str, email:str, full_name:str): if is_user_already_registred(username=username, email=email): raise exc.IntegrityError("User is already registred.") - user_model = get_model("users", "User") + user_model = apps.get_model("users", "User") user = user_model(username=username, email=email, full_name=full_name) @@ -149,7 +149,7 @@ def private_register_for_new_user(token:str, username:str, email:str, if is_user_already_registred(username=username, email=email): raise exc.WrongArguments(_("Username or Email is already in use.")) - user_model = get_model("users", "User") + user_model = apps.get_model("users", "User") user = user_model(username=username, email=email, full_name=full_name) @@ -177,7 +177,7 @@ def github_register(username:str, email:str, full_name:str, github_id:int, bio:s :returns: User """ - user_model = get_model("users", "User") + user_model = apps.get_model("users", "User") user, created = user_model.objects.get_or_create(github_id=github_id, defaults={"username": username, "email": email, diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py index d00a089c..95f8028d 100644 --- a/taiga/base/api/permissions.py +++ b/taiga/base/api/permissions.py @@ -18,7 +18,7 @@ import abc from taiga.base.utils import sequence as sq from taiga.permissions.service import user_has_perm, is_project_owner -from django.db.models.loading import get_model +from django.apps import apps ###################################################################### @@ -181,7 +181,7 @@ class HasProjectParamAndPerm(PermissionComponent): super().__init__(*components) def check_permissions(self, request, view, obj=None): - Project = get_model('projects', 'Project') + Project = apps.get_model('projects', 'Project') project_id = request.QUERY_PARAMS.get("project", None) try: project = Project.objects.get(pk=project_id) diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 6b41f7ca..2d350340 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from functools import partial -from django.db.models.loading import get_model +from django.apps import apps from django.contrib.contenttypes.models import ContentType from taiga.base.utils.iterators import as_tuple from taiga.base.utils.iterators import as_dict @@ -41,7 +41,7 @@ def _get_generic_values(ids:tuple, *, typename=None, attr:str="name") -> tuple: @as_dict def _get_users_values(ids:set) -> dict: - user_model = get_model("users", "User") + user_model = apps.get_model("users", "User") ids = filter(lambda x: x is not None, ids) qs = user_model.objects.filter(pk__in=tuple(ids)) @@ -199,7 +199,7 @@ def milestone_freezer(milestone) -> dict: def userstory_freezer(us) -> dict: - rp_cls = get_model("userstories", "RolePoints") + rp_cls = apps.get_model("userstories", "RolePoints") rpqsd = rp_cls.objects.filter(user_story=us) points = {} diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 300213a4..505a6730 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -17,7 +17,7 @@ import uuid from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.db import models -from django.db.models.loading import get_model +from django.apps import apps from django.utils.functional import cached_property from django.conf import settings from django_pgjson.fields import JsonField @@ -79,7 +79,7 @@ class HistoryEntry(models.Model): @cached_property def owner(self): pk = self.user["pk"] - model = get_model("users", "User") + model = apps.get_model("users", "User") return model.objects.get(pk=pk) @cached_property diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 53e36a66..3401f42a 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -35,7 +35,7 @@ from functools import lru_cache from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.paginator import Paginator, InvalidPage -from django.db.models.loading import get_model +from django.apps import apps from django.db import transaction as tx from taiga.mdrender.service import render as mdrender @@ -207,7 +207,7 @@ def _rebuild_snapshot_from_diffs(keysnapshot, partials): def get_last_snapshot_for_key(key:str) -> FrozenObj: - entry_model = get_model("history", "HistoryEntry") + entry_model = apps.get_model("history", "HistoryEntry") # Search last snapshot qs = (entry_model.objects @@ -251,7 +251,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): new_fobj = freeze_model_instance(obj) old_fobj, need_real_snapshot = get_last_snapshot_for_key(key) - entry_model = get_model("history", "HistoryEntry") + entry_model = apps.get_model("history", "HistoryEntry") user_id = None if user is None else user.id user_name = "" if user is None else user.get_full_name() @@ -306,7 +306,7 @@ def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change Get one page of history for specified object. """ key = make_key_from_model_object(obj) - history_entry_model = get_model("history", "HistoryEntry") + history_entry_model = apps.get_model("history", "HistoryEntry") qs = history_entry_model.objects.filter(key=key, type__in=types) if not include_hidden: diff --git a/taiga/projects/issues/__init__.py b/taiga/projects/issues/__init__.py index e69de29b..aff90c37 100644 --- a/taiga/projects/issues/__init__.py +++ b/taiga/projects/issues/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +default_app_config = "taiga.projects.issues.apps.IssuesAppConfig" + diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py new file mode 100644 index 00000000..153c2e0b --- /dev/null +++ b/taiga/projects/issues/apps.py @@ -0,0 +1,40 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 AppConfig +from django.apps import apps +from django.db.models import signals + +from taiga.projects import signals as generic_handlers +from . import signals as handlers + + +class IssuesAppConfig(AppConfig): + name = "taiga.projects.issues" + verbose_name = "Issues" + + def ready(self): + # Finixhed date + signals.pre_save.connect(handlers.set_finished_date_when_edit_issue, + sender=apps.get_model("issues", "Issue")) + + # Tags + signals.pre_save.connect(generic_handlers.tags_normalization, + sender=apps.get_model("issues", "Issue")) + signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, + sender=apps.get_model("issues", "Issue")) + signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, + sender=apps.get_model("issues", "Issue")) diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index d08b44a3..e43bace3 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -21,11 +21,11 @@ from django.utils import timezone from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ -from taiga.base.tags import TaggedMixin -from taiga.base.utils.slug import ref_uniquely -from taiga.projects.notifications import WatchedModelMixin from taiga.projects.occ import OCCModelMixin +from taiga.projects.notifications import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin +from taiga.base.tags import TaggedMixin + from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags @@ -67,7 +67,6 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. verbose_name = "issue" verbose_name_plural = "issues" ordering = ["project", "-created_date"] - #unique_together = ("ref", "project") permissions = ( ("view_issue", "Can view issue"), ) @@ -96,29 +95,3 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. @property def is_closed(self): return self.status.is_closed - - -# Model related signals handlers -@receiver(models.signals.pre_save, sender=Issue, dispatch_uid="issue_finished_date_handler") -def issue_finished_date_handler(sender, instance, **kwargs): - if instance.status.is_closed and not instance.finished_date: - instance.finished_date = timezone.now() - elif not instance.status.is_closed and instance.finished_date: - instance.finished_date = None - - -@receiver(models.signals.pre_save, sender=Issue, dispatch_uid="issue-tags-normalization") -def issue_tags_normalization(sender, instance, **kwargs): - if isinstance(instance.tags, (list, tuple)): - instance.tags = list(map(lambda x: x.lower(), instance.tags)) - - -@receiver(models.signals.post_save, sender=Issue, dispatch_uid="issue_update_project_colors") -def issue_update_project_tags(sender, instance, **kwargs): - update_project_tags_colors_handler(instance) - - -@receiver(models.signals.post_delete, sender=Issue, dispatch_uid="issue_update_project_colors_on_delete") -def issue_update_project_tags_on_delete(sender, instance, **kwargs): - remove_unused_tags(instance.project) - instance.project.save() diff --git a/taiga/projects/issues/signals.py b/taiga/projects/issues/signals.py new file mode 100644 index 00000000..9389d410 --- /dev/null +++ b/taiga/projects/issues/signals.py @@ -0,0 +1,28 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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.utils import timezone + + +#################################### +# Signals for set finished date +#################################### + +def set_finished_date_when_edit_issue(sender, instance, **kwargs): + if instance.status.is_closed and not instance.finished_date: + instance.finished_date = timezone.now() + elif not instance.status.is_closed and instance.finished_date: + instance.finished_date = None diff --git a/taiga/projects/milestones/apps.py b/taiga/projects/milestones/apps.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/milestones/services.py b/taiga/projects/milestones/services.py new file mode 100644 index 00000000..cac04870 --- /dev/null +++ b/taiga/projects/milestones/services.py @@ -0,0 +1,37 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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.utils import timezone + +from . import models + + + +def calculate_milestone_is_closed(milestone): + return (all([task.status.is_closed for task in milestone.tasks.all()]) and + all([user_story.is_closed for user_story in milestone.user_stories.all()])) + + +def close_milestone(milestone): + if not milestone.closed: + milestone.closed = True + milestone.save(update_fields=["closed",]) + + +def open_milestone(milestone): + if milestone.closed: + milestone.closed = False + milestone.save(update_fields=["closed",]) diff --git a/taiga/projects/milestones/signals.py b/taiga/projects/milestones/signals.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/models.py b/taiga/projects/models.py index ea1f4e12..ed2adf4a 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -19,7 +19,7 @@ import itertools from django.core.exceptions import ValidationError from django.db import models from django.db.models import signals -from django.db.models.loading import get_model +from django.apps import apps from django.conf import settings from django.dispatch import receiver from django.contrib.auth import get_user_model @@ -211,8 +211,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): return user_model.objects.filter(id__in=list(members)) def update_role_points(self, user_stories=None): - RolePoints = get_model("userstories", "RolePoints") - Role = get_model("users", "Role") + RolePoints = apps.get_model("userstories", "RolePoints") + Role = apps.get_model("users", "Role") # Get all available roles on this project roles = self.get_roles().filter(computable=True) @@ -251,7 +251,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): return dict_sum(*flat_role_dicts) def _get_points_increment(self, client_requirement, team_requirement): - userstory_model = get_model("userstories", "UserStory") + userstory_model = apps.get_model("userstories", "UserStory") user_stories = userstory_model.objects.none() last_milestones = self.milestones.order_by('-estimated_finish') last_milestone = last_milestones[0] if last_milestones else None @@ -742,9 +742,9 @@ def membership_post_delete(sender, instance, using, **kwargs): # On membership object is deleted, update watchers of all objects relation. @receiver(signals.post_delete, sender=Membership, dispatch_uid='update_watchers_on_membership_post_delete') def update_watchers_on_membership_post_delete(sender, instance, using, **kwargs): - models = [get_model("userstories", "UserStory"), - get_model("tasks", "Task"), - get_model("issues", "Issue")] + models = [apps.get_model("userstories", "UserStory"), + apps.get_model("tasks", "Task"), + apps.get_model("issues", "Issue")] # `user_id` is used beacuse in some momments # instance.user can contain pointer to now diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 9ac7e45d..f5be6166 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -18,12 +18,9 @@ from functools import partial from operator import is_not from django.conf import settings -from django.db.models.loading import get_model from django.db import models from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers -from taiga.projects.history.models import HistoryType from taiga.projects.notifications import services @@ -144,18 +141,3 @@ class WatchedModelMixin(models.Model): self.get_owner(),) is_not_none = partial(is_not, None) return frozenset(filter(is_not_none, participants)) - - -# class WatcherValidationSerializerMixin(object): -# def validate_watchers(self, attrs, source): -# values = set(attrs.get(source, [])) -# if values: -# project = None -# if "project" in attrs and attrs["project"]: -# project = attrs["project"] -# elif self.object: -# project = self.object.project -# model_cls = get_model("projects", "Membership") -# if len(values) != model_cls.objects.filter(project=project, user__in=values).count(): -# raise serializers.ValidationError("Error, some watcher user is not a member of the project") -# return attrs diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 5ec435ca..1b8c86c0 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -16,7 +16,7 @@ from functools import partial -from django.db.models.loading import get_model +from django.apps import apps from django.db import IntegrityError from django.contrib.contenttypes.models import ContentType @@ -33,7 +33,7 @@ def notify_policy_exists(project, user) -> bool: Check if policy exists for specified project and user. """ - model_cls = get_model("notifications", "NotifyPolicy") + model_cls = apps.get_model("notifications", "NotifyPolicy") qs = model_cls.objects.filter(project=project, user=user) return qs.exists() @@ -43,7 +43,7 @@ def create_notify_policy(project, user, level=NotifyLevel.notwatch): """ Given a project and user, create notification policy for it. """ - model_cls = get_model("notifications", "NotifyPolicy") + model_cls = apps.get_model("notifications", "NotifyPolicy") try: return model_cls.objects.create(project=project, user=user, @@ -56,7 +56,7 @@ def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.notwatch """ Given a project and user, create notification policy for it. """ - model_cls = get_model("notifications", "NotifyPolicy") + model_cls = apps.get_model("notifications", "NotifyPolicy") try: result = model_cls.objects.get_or_create(project=project, user=user, @@ -70,7 +70,7 @@ def get_notify_policy(project, user): """ Get notification level for specified project and user. """ - model_cls = get_model("notifications", "NotifyPolicy") + model_cls = apps.get_model("notifications", "NotifyPolicy") instance, _ = model_cls.objects.get_or_create(project=project, user=user, defaults={"notify_level": NotifyLevel.notwatch}) return instance diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 0fd3bffd..ad8f7169 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db.models.loading import get_model +from django.apps import apps from django.shortcuts import get_object_or_404 from rest_framework.response import Response @@ -37,7 +37,7 @@ class ResolverViewSet(viewsets.ViewSet): data = serializer.data - project_model = get_model("projects", "Project") + project_model = apps.get_model("projects", "Project") project = get_object_or_404(project_model, slug=data["project"]) self.check_permissions(request, "list", project) diff --git a/taiga/projects/references/services.py b/taiga/projects/references/services.py index aa4179ab..c40ca311 100644 --- a/taiga/projects/references/services.py +++ b/taiga/projects/references/services.py @@ -14,11 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db.models.loading import get_model +from django.apps import apps def get_instance_by_ref(project_id, obj_ref): - model_cls = get_model("references", "Reference") + model_cls = apps.get_model("references", "Reference") try: instance = model_cls.objects.get(project_id=project_id, ref=obj_ref) except model_cls.DoesNotExist: diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py new file mode 100644 index 00000000..e61722cb --- /dev/null +++ b/taiga/projects/signals.py @@ -0,0 +1,37 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags + + +#################################### +# Signals over project items +#################################### + +## TAGS + +def tags_normalization(sender, instance, **kwargs): + if isinstance(instance.tags, (list, tuple)): + instance.tags = list(map(str.lower, instance.tags)) + + +def update_project_tags_when_create_or_edit_taggable_item(sender, instance, **kwargs): + update_project_tags_colors_handler(instance) + + +def update_project_tags_when_delete_taggable_item(sender, instance, **kwargs): + remove_unused_tags(instance.project) + instance.project.save() diff --git a/taiga/projects/tasks/__init__.py b/taiga/projects/tasks/__init__.py index e69de29b..0af24e1d 100644 --- a/taiga/projects/tasks/__init__.py +++ b/taiga/projects/tasks/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +default_app_config = "taiga.projects.tasks.apps.TasksAppConfig" diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py new file mode 100644 index 00000000..752560de --- /dev/null +++ b/taiga/projects/tasks/apps.py @@ -0,0 +1,46 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 AppConfig +from django.apps import apps +from django.db.models import signals + +from taiga.projects import signals as generic_handlers +from . import signals as handlers + + +class TasksAppConfig(AppConfig): + name = "taiga.projects.tasks" + verbose_name = "Tasks" + + def ready(self): + # Cached prev object version + signals.pre_save.connect(handlers.cached_prev_task, + sender=apps.get_model("tasks", "Task")) + + # Open/Close US and Milestone + signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_task, + sender=apps.get_model("tasks", "Task")) + signals.post_delete.connect(handlers.try_to_close_or_open_us_and_milestone_when_delete_task, + sender=apps.get_model("tasks", "Task")) + + # Tags + signals.pre_save.connect(generic_handlers.tags_normalization, + sender=apps.get_model("tasks", "Task")) + signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, + sender=apps.get_model("tasks", "Task")) + signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, + sender=apps.get_model("tasks", "Task")) diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index a3708466..9f71dd3b 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -18,18 +18,12 @@ from django.db import models from django.contrib.contenttypes import generic from django.conf import settings from django.utils import timezone -from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ -from taiga.base.tags import TaggedMixin -from taiga.base.utils.slug import ref_uniquely -from taiga.projects.notifications import WatchedModelMixin from taiga.projects.occ import OCCModelMixin -from taiga.projects.userstories.models import UserStory -from taiga.projects.userstories import services as us_service -from taiga.projects.milestones.models import Milestone +from taiga.projects.notifications import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin -from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags +from taiga.base.tags import TaggedMixin class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): @@ -90,71 +84,3 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M def __str__(self): return "({1}) {0}".format(self.ref, self.subject) - - -def milestone_has_open_userstories(milestone): - qs = milestone.user_stories.exclude(is_closed=True) - return qs.exists() - - -@receiver(models.signals.post_delete, sender=Task, dispatch_uid="tasks_milestone_close_handler_on_delete") -def tasks_milestone_close_handler_on_delete(sender, instance, **kwargs): - if instance.milestone_id and Milestone.objects.filter(id=instance.milestone_id): - if not milestone_has_open_userstories(instance.milestone): - instance.milestone.closed = True - instance.milestone.save(update_fields=["closed"]) - - -# Define the previous version of the task for use it on the post_save handler -@receiver(models.signals.pre_save, sender=Task, dispatch_uid="tasks_us_close_handler") -def tasks_us_close_handler(sender, instance, **kwargs): - instance.prev = None - if instance.id: - instance.prev = sender.objects.get(id=instance.id) - - -@receiver(models.signals.post_save, sender=Task, dispatch_uid="tasks_us_close_on_create_handler") -def tasks_us_close_on_create_handler(sender, instance, created, **kwargs): - if instance.user_story_id: - if us_service.calculate_userstory_is_closed(instance.user_story): - us_service.close_userstory(instance.user_story) - else: - us_service.open_userstory(instance.user_story) - - if instance.prev and instance.prev.user_story_id: - if us_service.calculate_userstory_is_closed(instance.prev.user_story): - us_service.close_userstory(instance.prev.user_story) - else: - us_service.open_userstory(instance.prev.user_story) - - -@receiver(models.signals.post_delete, sender=Task, dispatch_uid="tasks_us_close_handler_on_delete") -def tasks_us_close_handler_on_delete(sender, instance, **kwargs): - if instance.user_story_id: - if us_service.calculate_userstory_is_closed(instance.user_story): - us_service.close_userstory(instance.user_story) - else: - us_service.open_userstory(instance.user_story) - - -@receiver(models.signals.pre_save, sender=Task, dispatch_uid="tasks_milestone_close_handler") -def tasks_milestone_close_handler(sender, instance, **kwargs): - if instance.milestone_id: - if instance.status.is_closed and not instance.milestone.closed: - if not milestone_has_open_userstories(instance.milestone): - instance.milestone.closed = True - instance.milestone.save(update_fields=["closed"]) - elif not instance.status.is_closed and instance.milestone.closed: - instance.milestone.closed = False - instance.milestone.save(update_fields=["closed"]) - - -@receiver(models.signals.post_save, sender=Task, dispatch_uid="task_update_project_colors") -def task_update_project_tags(sender, instance, **kwargs): - update_project_tags_colors_handler(instance) - - -@receiver(models.signals.post_delete, sender=Task, dispatch_uid="task_update_project_colors_on_delete") -def task_update_project_tags_on_delete(sender, instance, **kwargs): - remove_unused_tags(instance.project) - instance.project.save() diff --git a/taiga/projects/tasks/signals.py b/taiga/projects/tasks/signals.py new file mode 100644 index 00000000..cbc633d1 --- /dev/null +++ b/taiga/projects/tasks/signals.py @@ -0,0 +1,90 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + + +#################################### +# Signals for cached prev task +#################################### + +# Define the previous version of the task for use it on the post_save handler +def cached_prev_task(sender, instance, **kwargs): + instance.prev = None + if instance.id: + instance.prev = sender.objects.get(id=instance.id) + + +#################################### +# Signals for close US and Milestone +#################################### + +def try_to_close_or_open_us_and_milestone_when_create_or_edit_task(sender, instance, created, **kwargs): + _try_to_close_or_open_us_when_create_or_edit_task(instance) + _try_to_close_or_open_milestone_when_create_or_edit_task(instance) + +def try_to_close_or_open_us_and_milestone_when_delete_task(sender, instance, **kwargs): + _try_to_close_or_open_us_when_delete_task(instance) + _try_to_close_milestone_when_delete_task(instance) + + +# US +def _try_to_close_or_open_us_when_create_or_edit_task(instance): + from taiga.projects.userstories import services as us_service + + if instance.user_story_id: + if us_service.calculate_userstory_is_closed(instance.user_story): + us_service.close_userstory(instance.user_story) + else: + us_service.open_userstory(instance.user_story) + + if instance.prev and instance.prev.user_story_id and instance.prev.user_story_id != instance.user_story_id: + if us_service.calculate_userstory_is_closed(instance.prev.user_story): + us_service.close_userstory(instance.prev.user_story) + else: + us_service.open_userstory(instance.prev.user_story) + + +def _try_to_close_or_open_us_when_delete_task(instance): + from taiga.projects.userstories import services as us_service + + if instance.user_story_id: + if us_service.calculate_userstory_is_closed(instance.user_story): + us_service.close_userstory(instance.user_story) + else: + us_service.open_userstory(instance.user_story) + + +# Milestone +def _try_to_close_or_open_milestone_when_create_or_edit_task(instance): + from taiga.projects.milestones import services as milestone_service + + if instance.milestone_id: + if milestone_service.calculate_milestone_is_closed(instance.milestone): + milestone_service.close_milestone(instance.milestone) + else: + milestone_service.open_milestone(instance.milestone) + + if instance.prev and instance.prev.milestone_id and instance.prev.milestone_id != instance.milestone_id: + if milestone_service.calculate_milestone_is_closed(instance.prev.milestone): + milestone_service.close_milestone(instance.prev.milestone) + else: + milestone_service.open_milestone(instance.prev.milestone) + + +def _try_to_close_milestone_when_delete_task(instance): + from taiga.projects.milestones import services as milestone_service + + if instance.milestone_id and milestone_service.calculate_milestone_is_closed(instance.milestone): + milestone_service.close_milestone(instance.milestone) diff --git a/taiga/projects/userstories/__init__.py b/taiga/projects/userstories/__init__.py index e69de29b..572f9d9a 100644 --- a/taiga/projects/userstories/__init__.py +++ b/taiga/projects/userstories/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +default_app_config = "taiga.projects.userstories.apps.UserStoriesAppConfig" diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py new file mode 100644 index 00000000..299f1cfc --- /dev/null +++ b/taiga/projects/userstories/apps.py @@ -0,0 +1,54 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 AppConfig +from django.apps import apps +from django.db.models import signals + +from taiga.projects import signals as generic_handlers +from . import signals as handlers + + +class UserStoriesAppConfig(AppConfig): + name = "taiga.projects.userstories" + verbose_name = "User Stories" + + def ready(self): + # Cached prev object version + signals.pre_save.connect(handlers.cached_prev_us, + sender=apps.get_model("userstories", "UserStory")) + + # Role Points + signals.post_save.connect(handlers.update_role_points_when_create_or_edit_us, + sender=apps.get_model("userstories", "UserStory")) + + # Tasks + signals.post_save.connect(handlers.update_milestone_of_tasks_when_edit_us, + sender=apps.get_model("userstories", "UserStory")) + + # 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")) + signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_us, + sender=apps.get_model("userstories", "UserStory")) + + # Tags + signals.pre_save.connect(generic_handlers.tags_normalization, + sender=apps.get_model("userstories", "UserStory")) + signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, + sender=apps.get_model("userstories", "UserStory")) + signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, + sender=apps.get_model("userstories", "UserStory")) diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index e46a1840..377aef52 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -17,16 +17,13 @@ from django.db import models from django.contrib.contenttypes import generic from django.conf import settings -from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from taiga.base.tags import TaggedMixin -from taiga.base.utils.slug import ref_uniquely -from taiga.projects.notifications import WatchedModelMixin from taiga.projects.occ import OCCModelMixin +from taiga.projects.notifications import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin -from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags +from taiga.base.tags import TaggedMixin class RolePoints(models.Model): @@ -138,45 +135,3 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod total += rp.points.value return total - - -@receiver(models.signals.post_save, sender=UserStory, - dispatch_uid="user_story_create_role_points_handler") -def us_create_role_points_handler(sender, instance, **kwargs): - if instance._importing: - return - instance.project.update_role_points(user_stories=[instance]) - - -@receiver(models.signals.post_save, sender=UserStory, - dispatch_uid="user_story_tasks_reassignation") -def us_task_reassignation(sender, instance, created, **kwargs): - if not created: - instance.tasks.update(milestone=instance.milestone) - - -@receiver(models.signals.pre_save, sender=UserStory, dispatch_uid="us-tags-normalization") -def us_tags_normalization(sender, instance, **kwargs): - if isinstance(instance.tags, (list, tuple)): - instance.tags = list(map(str.lower, instance.tags)) - -@receiver(models.signals.post_save, sender=UserStory, - dispatch_uid="user_story_on_status_change") -def us_close_open_on_status_change(sender, instance, **kwargs): - from taiga.projects.userstories import services as service - - if service.calculate_userstory_is_closed(instance): - service.close_userstory(instance) - else: - service.open_userstory(instance) - - -@receiver(models.signals.post_save, sender=UserStory, dispatch_uid="user_story_update_project_colors") -def us_update_project_tags(sender, instance, **kwargs): - update_project_tags_colors_handler(instance) - - -@receiver(models.signals.post_delete, sender=UserStory, dispatch_uid="user_story_update_project_colors_on_delete") -def us_update_project_tags_on_delete(sender, instance, **kwargs): - remove_unused_tags(instance.project) - instance.project.save() diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 9cb3ba33..ef2607e2 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -15,7 +15,7 @@ # along with this program. If not, see . import json -from django.db.models import get_model +from django.apps import apps from rest_framework import serializers from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin @@ -56,7 +56,7 @@ class UserStorySerializer(serializers.ModelSerializer): role_points = obj._related_data.pop("role_points", None) super().save_object(obj, **kwargs) - points_modelcls = get_model("projects", "Points") + points_modelcls = apps.get_model("projects", "Points") if role_points: for role_id, points_id in role_points.items(): diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py new file mode 100644 index 00000000..c1b491a8 --- /dev/null +++ b/taiga/projects/userstories/signals.py @@ -0,0 +1,91 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + + +#################################### +# Signals for cached prev US +#################################### + +# Define the previous version of the US for use it on the post_save handler +def cached_prev_us(sender, instance, **kwargs): + instance.prev = None + if instance.id: + instance.prev = sender.objects.get(id=instance.id) + + +#################################### +# Signals of role points +#################################### + +def update_role_points_when_create_or_edit_us(sender, instance, **kwargs): + if instance._importing: + return + instance.project.update_role_points(user_stories=[instance]) + + +#################################### +# Signals for update milestone of tasks +#################################### + +def update_milestone_of_tasks_when_edit_us(sender, instance, created, **kwargs): + if not created: + instance.tasks.update(milestone=instance.milestone) + + +#################################### +# Signals for close US and Milestone +#################################### + +def try_to_close_or_open_us_and_milestone_when_create_or_edit_us(sender, instance, created, **kwargs): + _try_to_close_or_open_us_when_create_or_edit_us(instance) + _try_to_close_or_open_milestone_when_create_or_edit_us(instance) + +def try_to_close_milestone_when_delete_us(sender, instance, **kwargs): + _try_to_close_milestone_when_delete_us(instance) + + +# US +def _try_to_close_or_open_us_when_create_or_edit_us(instance): + from . import services as us_service + + if us_service.calculate_userstory_is_closed(instance): + us_service.close_userstory(instance) + else: + us_service.open_userstory(instance) + + +# Milestone +def _try_to_close_or_open_milestone_when_create_or_edit_us(instance): + from taiga.projects.milestones import services as milestone_service + + if instance.milestone_id: + if milestone_service.calculate_milestone_is_closed(instance.milestone): + milestone_service.close_milestone(instance.milestone) + else: + milestone_service.open_milestone(instance.milestone) + + if instance.prev and instance.prev.milestone_id and instance.prev.milestone_id != instance.milestone_id: + if milestone_service.calculate_milestone_is_closed(instance.prev.milestone): + milestone_service.close_milestone(instance.prev.milestone) + else: + milestone_service.open_milestone(instance.prev.milestone) + + +def _try_to_close_milestone_when_delete_us(instance): + from taiga.projects.milestones import services as milestone_service + + if instance.milestone_id and milestone_service.calculate_milestone_is_closed(instance.milestone): + milestone_service.close_milestone(instance.milestone) diff --git a/taiga/projects/votes/services.py b/taiga/projects/votes/services.py index e09c96b3..ddc1deae 100644 --- a/taiga/projects/votes/services.py +++ b/taiga/projects/votes/services.py @@ -17,7 +17,7 @@ from django.db.models import F from django.db.transaction import atomic -from django.db.models.loading import get_model +from django.apps import apps from django.contrib.auth import get_user_model from .models import Votes, Vote @@ -32,7 +32,7 @@ def add_vote(obj, user): :param obj: Any Django model instance. :param user: User adding the vote. :class:`~taiga.users.models.User` instance. """ - obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj) + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) with atomic(): vote, created = Vote.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user) @@ -54,7 +54,7 @@ def remove_vote(obj, user): :param obj: Any Django model instance. :param user: User removing her vote. :class:`~taiga.users.models.User` instance. """ - obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj) + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) with atomic(): qs = Vote.objects.filter(content_type=obj_type, object_id=obj.id, user=user) if not qs.exists(): @@ -74,7 +74,7 @@ def get_voters(obj): :return: User queryset object representing the users that voted the object. """ - obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj) + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) return get_user_model().objects.filter(votes__content_type=obj_type, votes__object_id=obj.id) @@ -85,7 +85,7 @@ def get_votes(obj): :return: Number of votes or `0` if the object has no votes at all. """ - obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj) + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) try: return Votes.objects.get(content_type=obj_type, object_id=obj.id).count @@ -101,7 +101,7 @@ def get_voted(user_or_id, model): :return: Queryset of objects representing the votes of the user. """ - obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(model) + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) conditions = ('votes_vote.content_type_id = %s', '%s.id = votes_vote.object_id' % model._meta.db_table, 'votes_vote.user_id = %s') diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py index 740d645f..20e72b6d 100644 --- a/taiga/projects/votes/utils.py +++ b/taiga/projects/votes/utils.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.db.models.loading import get_model +from django.apps import apps def attach_votescount_to_queryset(queryset, as_field="votes_count"): @@ -33,7 +33,7 @@ def attach_votescount_to_queryset(queryset, as_field="votes_count"): :return: Queryset object with the additional `as_field` field. """ model = queryset.model - type = get_model("contenttypes", "ContentType").objects.get_for_model(model) + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) sql = ("SELECT coalesce(votes_votes.count, 0) FROM votes_votes " "WHERE votes_votes.content_type_id = {type_id} AND votes_votes.object_id = {tbl}.id") sql = sql.format(type_id=type.id, tbl=model._meta.db_table) diff --git a/taiga/searches/api.py b/taiga/searches/api.py index 7d5f36ea..4f585d91 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db.models.loading import get_model +from django.apps import apps from rest_framework.response import Response from rest_framework import viewsets @@ -31,7 +31,7 @@ from . import services class SearchViewSet(viewsets.ViewSet): def list(self, request, **kwargs): - project_model = get_model("projects", "Project") + project_model = apps.get_model("projects", "Project") text = request.QUERY_PARAMS.get('text', "") project_id = request.QUERY_PARAMS.get('project', None) @@ -55,7 +55,7 @@ class SearchViewSet(viewsets.ViewSet): return Response(result) def _get_project(self, project_id): - project_model = get_model("projects", "Project") + project_model = apps.get_model("projects", "Project") return project_model.objects.get(pk=project_id) def _search_user_stories(self, project, text): diff --git a/taiga/searches/services.py b/taiga/searches/services.py index e9b9d14d..e209b971 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db.models.loading import get_model +from django.apps import apps from django.conf import settings @@ -22,7 +22,7 @@ MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) def search_user_stories(project, text): - model_cls = get_model("userstories", "UserStory") + model_cls = apps.get_model("userstories", "UserStory") where_clause = ("to_tsvector(coalesce(userstories_userstory.subject) || ' ' || " "coalesce(userstories_userstory.description)) @@ plainto_tsquery(%s)") @@ -34,7 +34,7 @@ def search_user_stories(project, text): def search_tasks(project, text): - model_cls = get_model("tasks", "Task") + model_cls = apps.get_model("tasks", "Task") where_clause = ("to_tsvector(coalesce(tasks_task.subject, '') || ' ' || coalesce(tasks_task.description, '')) " "@@ plainto_tsquery(%s)") @@ -46,7 +46,7 @@ def search_tasks(project, text): def search_issues(project, text): - model_cls = get_model("issues", "Issue") + model_cls = apps.get_model("issues", "Issue") where_clause = ("to_tsvector(coalesce(issues_issue.subject) || ' ' || coalesce(issues_issue.description)) " "@@ plainto_tsquery(%s)") @@ -58,7 +58,7 @@ def search_issues(project, text): def search_wiki_pages(project, text): - model_cls = get_model("wiki", "WikiPage") + model_cls = apps.get_model("wiki", "WikiPage") where_clause = ("to_tsvector(coalesce(wiki_wikipage.slug) || ' ' || coalesce(wiki_wikipage.content)) " "@@ plainto_tsquery(%s)") diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 5cdf3549..d3427a29 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -14,9 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db.models.loading import get_model -from django.db.models import signals - from taiga.timeline.service import push_to_timeline # TODO: Add events to followers timeline when followers are implemented. diff --git a/taiga/users/api.py b/taiga/users/api.py index 92a2ddc3..0d3f0dff 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -16,7 +16,7 @@ import uuid -from django.db.models.loading import get_model +from django.apps import apps from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ @@ -48,7 +48,7 @@ class MembersFilterBackend(BaseFilterBackend): def filter_queryset(self, request, queryset, view): project_id = request.QUERY_PARAMS.get('project', None) if project_id: - Project = get_model('projects', 'Project') + Project = apps.get_model('projects', 'Project') project = get_object_or_404(Project, pk=project_id) if project.memberships.filter(user=request.user).exists() or project.owner == request.user: return queryset.filter(Q(memberships__project=project) | Q(id=project.owner.id)).distinct() @@ -201,7 +201,7 @@ class UsersViewSet(ModelCrudViewSet): user = self.get_object() self.check_permissions(request, 'starred', user) - stars = votes_service.get_voted(user.pk, model=get_model('projects', 'Project')) + stars = votes_service.get_voted(user.pk, model=apps.get_model('projects', 'Project')) stars_data = StarredSerializer(stars, many=True) return Response(stars_data.data) diff --git a/taiga/users/services.py b/taiga/users/services.py index df56c053..9366cee8 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -18,7 +18,7 @@ This model contains a domain logic for users application. """ -from django.db.models.loading import get_model +from django.apps import apps from django.db.models import Q from django.conf import settings @@ -40,7 +40,7 @@ def get_and_validate_user(*, username:str, password:str) -> bool: exception is raised. """ - user_model = get_model("users", "User") + user_model = apps.get_model("users", "User") qs = user_model.objects.filter(Q(username=username) | Q(email=username)) if len(qs) == 0: diff --git a/tests/factories.py b/tests/factories.py index 12c266ac..ffd368f7 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -19,7 +19,6 @@ import uuid import threading from datetime import date, timedelta -from django.db.models.loading import get_model from django.conf import settings import factory diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 02fbe5fd..18517a57 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -1,5 +1,5 @@ from django.core.urlresolvers import reverse -from django.db.models.loading import get_model +from django.apps import apps from taiga.base.utils import json from taiga.projects.serializers import ProjectDetailSerializer @@ -53,8 +53,8 @@ def data(): role__project=m.private_project2, role__permissions=[]) - ContentType = get_model("contenttypes", "ContentType") - Project = get_model("projects", "Project") + ContentType = apps.get_model("contenttypes", "ContentType") + Project = apps.get_model("projects", "Project") project_ct = ContentType.objects.get_for_model(Project) diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index b714f4a5..09d9eab6 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -18,7 +18,7 @@ import pytest from unittest.mock import patch, Mock -from django.db.models.loading import get_model +from django.apps import apps from django.core.urlresolvers import reverse from .. import factories @@ -88,7 +88,7 @@ def test_response_200_in_registration_with_github_account(client): def test_response_200_in_registration_with_github_account_in_a_project(client): - membership_model = get_model("projects", "Membership") + membership_model = apps.get_model("projects", "Membership") membership = factories.MembershipFactory(user=None) form = {"type": "github", "code": "xxxxxx", diff --git a/tests/integration/test_project_history.py b/tests/integration/test_project_history.py index 42d0476f..22b8cd7a 100644 --- a/tests/integration/test_project_history.py +++ b/tests/integration/test_project_history.py @@ -21,7 +21,6 @@ from unittest.mock import MagicMock from unittest.mock import patch from django.core.urlresolvers import reverse -from django.db.models.loading import get_model from .. import factories as f from taiga.projects.history import services diff --git a/tests/integration/test_project_notifications.py b/tests/integration/test_project_notifications.py index af572dba..09447dda 100644 --- a/tests/integration/test_project_notifications.py +++ b/tests/integration/test_project_notifications.py @@ -20,7 +20,7 @@ import pytest from unittest.mock import MagicMock, patch from django.core.urlresolvers import reverse -from django.db.models.loading import get_model +from django.apps import apps from .. import factories as f from taiga.projects.notifications import services @@ -59,7 +59,7 @@ def test_attach_notify_policy_to_project_queryset(): def test_create_retrieve_notify_policy(): project = f.ProjectFactory.create() - policy_model_cls = get_model("notifications", "NotifyPolicy") + policy_model_cls = apps.get_model("notifications", "NotifyPolicy") current_number = policy_model_cls.objects.all().count() assert current_number == 0 @@ -102,7 +102,7 @@ def test_users_to_notify(): member2 = f.MembershipFactory.create(project=project) member3 = f.MembershipFactory.create(project=project) - policy_model_cls = get_model("notifications", "NotifyPolicy") + policy_model_cls = apps.get_model("notifications", "NotifyPolicy") policy1 = policy_model_cls.objects.get(user=member1.user) policy2 = policy_model_cls.objects.get(user=member2.user)