diff --git a/taiga/base/api.py b/taiga/base/api.py index 5d3ad1b9..b3b0d694 100644 --- a/taiga/base/api.py +++ b/taiga/base/api.py @@ -63,13 +63,6 @@ class DestroyModelMixin(mixins.DestroyModelMixin): # Other mixins (what they are doing here?) -class NeighborsApiMixin(object): - def filter_queryset(self, queryset, force=False): - for backend in self.get_filter_backends(): - if force or self.action != "retrieve" or backend not in self.retrieve_exclude_filters: - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset - class PreconditionMixin(object): def pre_conditions_on_save(self, obj): diff --git a/taiga/base/models.py b/taiga/base/models.py index c5ca1027..90d40912 100644 --- a/taiga/base/models.py +++ b/taiga/base/models.py @@ -23,87 +23,3 @@ monkey.patch_api_view() monkey.patch_serializer() monkey.patch_import_module() monkey.patch_south_hacks() - - -class NeighborsMixin: - - def get_neighbors(self, queryset=None): - """Get the objects around this object. - - :param queryset: A queryset object to use as a starting point. Useful if you need to - pre-filter the neighbor candidates. - - :return: The tuple `(previous, next)`. - """ - if queryset is None: - queryset = type(self).objects.get_queryset() - queryset = queryset.order_by(*self._get_order_by(queryset)) - queryset = queryset.filter(~Q(id=self.id)) - - return self._get_previous_neighbor(queryset), self._get_next_neighbor(queryset) - - def _get_queryset_order_by(self, queryset): - return queryset.query.order_by - - def _get_order_by(self, queryset): - return self._get_queryset_order_by(queryset) or self._meta.ordering - - def _get_order_field_value(self, field): - field = field.lstrip("-") - obj = self - for attr in field.split("__"): - value = getattr(obj, attr, None) - if value is None: - break - obj = value - - return value - - def _transform_order_field_into_lookup(self, field, operator, operator_if_order_desc): - if field.startswith("-"): - field = field[1:] - operator = operator_if_order_desc - return field, operator - - def _format(self, value): - if hasattr(value, "format"): - value = value.format(obj=self) - return value - - def _or(self, conditions): - result = Q() - for condition in conditions: - result |= Q(**{key: self._format(condition[key]) for key in condition}) - return result - - def _get_neighbor_filters(self, queryset, operator, operator_if_order_desc): - conds = [] - for field in self._get_queryset_order_by(queryset): - value = self._get_order_field_value(field) - if value is None: - continue - lookup_field, operator = self._transform_order_field_into_lookup( - field, operator, operator_if_order_desc) - lookup = "{}__{}".format(lookup_field, operator) - conds.append({lookup: value}) - return conds - - def _get_prev_neighbor_filters(self, queryset): - return self._get_neighbor_filters(queryset, "lte", "gte") - - def _get_next_neighbor_filters(self, queryset): - return self._get_neighbor_filters(queryset, "gte", "lte") - - def _get_previous_neighbor(self, queryset): - queryset = queryset.filter(self._or(self._get_prev_neighbor_filters(queryset))) - try: - return queryset.reverse()[0] - except IndexError: - return None - - def _get_next_neighbor(self, queryset): - queryset = queryset.filter(self._or(self._get_next_neighbor_filters(queryset))) - try: - return queryset[0] - except IndexError: - return None diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py new file mode 100644 index 00000000..e0bcc07e --- /dev/null +++ b/taiga/base/neighbors.py @@ -0,0 +1,122 @@ +from functools import partial +from collections import namedtuple + +from django.db.models import Q + + +Neighbor = namedtuple("Neighbor", "left right") + + +def disjunction_filters(filters): + """From a list of queryset filters, it returns a disjunction (OR) Q object. + + :param filters: List of filters where each item is a dict where the keys are the lookups and + the values the values of those lookups. + + :return: :class:`django.db.models.Q` instance representing the disjunction of the filters. + """ + result = Q() + for filter in filters: + result |= Q(**{lookup: value for lookup, value in filter.items()}) + return result + + +def get_attribute(obj, attr): + """Finds `attr` in obj. + + :param obj: Object where to look for the attribute. + :param attr: Attribute name as a string. It can be a Django lookup field such as + `project__owner__name`, in which case it will look for `obj.project.owner.name`. + + :return: The attribute value. + :raises: `AttributeError` if some attribute doesn't exist. + """ + chunks = attr.lstrip("-").split("__") + attr, chunks = chunks[0], chunks[1:] + obj = value = getattr(obj, attr) + for path in chunks: + value = getattr(obj, path) + obj = value + + return value + + +def transform_field_into_lookup(name, value, operator="", operator_if_desc=""): + """From a field name and value, return a dict that may be used as a queryset filter. + + :param name: Field name as a string. + :param value: Field value. + :param operator: Operator to use in the lookup. + :param operator_if_desc: If the field is reversed (a "-" in front) use this operator + instead. + + :return: A dict that may be used as a queryset filter. + """ + if name.startswith("-"): + name = name[1:] + operator = operator_if_desc + lookup = "{}{}".format(name, operator) + return {lookup: value} + + +def get_neighbors(obj, results_set=None): + """Get the neighbors of a model instance. + + The neighbors are the objects that are at the left/right of `obj` in the results set. + + :param obj: The object you want to know its neighbors. + :param results_set: Find the neighbors applying the constraints of this set (a Django queryset + object). + + :return: Tuple `, `. Left and right neighbors can be `None`. + """ + if results_set is None: + results_set = type(obj).objects.get_queryset() + try: + left = _left_candidates(obj, results_set).reverse()[0] + except IndexError: + left = None + try: + right = _right_candidates(obj, results_set)[0] + except IndexError: + right = None + + return Neighbor(left, right) + + +def _get_candidates(obj, results_set, reverse=False): + ordering = (results_set.query.order_by or []) + list(obj._meta.ordering) + main_ordering, rest_ordering = ordering[0], ordering[1:] + try: + filters = obj.get_neighbors_additional_filters(results_set, ordering, reverse) + if filters is None: + raise AttributeError + except AttributeError: + filters = [order_field_as_filter(obj, main_ordering, reverse)] + filters += [ordering_fields_as_filter(obj, main_ordering, rest_ordering, reverse)] + + return (results_set + .filter(~Q(id=obj.id), disjunction_filters(filters)) + .distinct() + .order_by(*ordering)) +_left_candidates = partial(_get_candidates, reverse=True) +_right_candidates = partial(_get_candidates, reverse=False) + + +def order_field_as_filter(obj, order_field, reverse=None, operator=None): + value = get_attribute(obj, order_field) + if reverse is not None: + if operator is None: + operator = ("__gt", "__lt") + operator = (operator[1], operator[0]) if reverse else operator + else: + operator = () + return transform_field_into_lookup(order_field, value, *operator) + + +def ordering_fields_as_filter(obj, main_order_field, ordering_fields, reverse=False): + """Transform a list of ordering fields into a queryset filter.""" + filter = order_field_as_filter(obj, main_order_field) + for field in ordering_fields: + filter.update(order_field_as_filter(obj, field, reverse, ("__gte", "__lte"))) + return filter diff --git a/taiga/base/serializers.py b/taiga/base/serializers.py index 5b967140..c5c8caf7 100644 --- a/taiga/base/serializers.py +++ b/taiga/base/serializers.py @@ -20,6 +20,7 @@ from rest_framework import serializers from taiga.domains.base import get_active_domain from taiga.domains.models import Domain +from .neighbors import get_neighbors class PickleField(serializers.WritableField): @@ -72,9 +73,12 @@ class NeighborsSerializerMixin: def get_neighbors(self, obj): view, request = self.context.get("view", None), self.context.get("request", None) if view and request: - queryset = view.filter_queryset(view.get_queryset(), True) - previous, next = obj.get_neighbors(queryset) + queryset = view.filter_queryset(view.get_queryset()) + left, right = get_neighbors(obj, results_set=queryset) + else: + left = right = None - return {"previous": self.serialize_neighbor(previous), - "next": self.serialize_neighbor(next)} - return {"previous": None, "next": None} + return { + "previous": self.serialize_neighbor(left), + "next": self.serialize_neighbor(right) + } diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index e218af61..ce74a706 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -26,7 +26,7 @@ from taiga.base import filters from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet -from taiga.base.api import NeighborsApiMixin + from taiga.projects.mixins.notifications import NotificationSenderMixin from . import models @@ -87,6 +87,7 @@ class IssuesFilter(filters.FilterBackend): class IssuesOrdering(filters.FilterBackend): def filter_queryset(self, request, queryset, view): order_by = request.QUERY_PARAMS.get('order_by', None) + if order_by in ['owner', '-owner', 'assigned_to', '-assigned_to']: return queryset.order_by( '{}__first_name'.format(order_by), @@ -95,7 +96,7 @@ class IssuesOrdering(filters.FilterBackend): return queryset -class IssueViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudViewSet): +class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet): model = models.Issue queryset = models.Issue.objects.all().prefetch_related("attachments") serializer_class = serializers.IssueNeighborsSerializer diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index feea578e..b957ac79 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -23,13 +23,12 @@ from django.utils.translation import ugettext_lazy as _ from picklefield.fields import PickledObjectField -from taiga.base.models import NeighborsMixin from taiga.base.utils.slug import ref_uniquely from taiga.projects.notifications.models import WatchedMixin from taiga.projects.mixins.blocked import BlockedMixin -class Issue(NeighborsMixin, WatchedMixin, BlockedMixin, models.Model): +class Issue(WatchedMixin, BlockedMixin, models.Model): ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, verbose_name=_("ref")) owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None, @@ -77,164 +76,6 @@ class Issue(NeighborsMixin, WatchedMixin, BlockedMixin, models.Model): def __str__(self): return "({1}) {0}".format(self.ref, self.subject) - def _get_order_by(self, queryset): - ordering = self._get_queryset_order_by(queryset) - if ordering: - main_order = ordering[0] - need_extra_ordering = ("severity", "-severity", "owner__first_name", - "-owner__first_name", "status", "-status", "priority", - "-priority", "assigned_to__first_name", - "-assigned_to__first_name") - if main_order in need_extra_ordering: - ordering += self._meta.ordering - - return ordering - - def _get_prev_neighbor_filters(self, queryset): - conds = super()._get_prev_neighbor_filters(queryset) - try: - main_order = queryset.query.order_by[0] - except IndexError: - main_order = None - - if main_order == "severity": - conds = [{"severity__order__lt": self.severity.order}, - {"severity__order": self.severity.order, - "created_date__lt": self.created_date}] - elif main_order == "-severity": - conds = [{"severity__order__gt": self.severity.order}, - {"severity__order": self.severity.order, - "created_date__lt": self.created_date}] - elif main_order == "status": - conds = [{"status__order__lt": self.status.order}, - {"status__order": self.status.order, - "created_date__lt": self.created_date}] - elif main_order == "-status": - conds = [{"status__order__gt": self.status.order}, - {"status__order": self.status.order, - "created_date__lt": self.created_date}] - elif main_order == "priority": - conds = [{"priority__order__lt": self.priority.order}, - {"priority__order": self.priority.order, - "created_date__lt": self.created_date}] - elif main_order == "-priority": - conds = [{"priority__order__gt": self.priority.order}, - {"priority__order": self.priority.order, - "created_date__lt": self.created_date}] - elif main_order == "owner__first_name": - conds = [{"owner__first_name": self.owner.first_name, - "owner__last_name": self.owner.last_name, - "created_date__lt": self.created_date}, - {"owner__first_name": self.owner.first_name, - "owner__last_name__lt": self.owner.last_name}, - {"owner__first_name__lt": self.owner.first_name}] - elif main_order == "-owner__first_name": - conds = [{"owner__first_name": self.owner.first_name, - "owner__last_name": self.owner.last_name, - "created_date__lt": self.created_date}, - {"owner__first_name": self.owner.first_name, - "owner__last_name__gt": self.owner.last_name}, - {"owner__first_name__gt": self.owner.first_name}] - elif main_order == "assigned_to__first_name": - if self.assigned_to: - conds = [{"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name": self.assigned_to.last_name, - "created_date__lt": self.created_date}, - {"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name__lt": self.assigned_to.last_name}, - {"assigned_to__first_name__lt": self.assigned_to.first_name}] - else: - conds = [{"assigned_to__isnull": True, - "created_date__lt": self.created_date}, - {"assigned_to__isnull": False}] - elif main_order == "-assigned_to__first_name": - if self.assigned_to: - conds = [{"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name": self.assigned_to.last_name, - "created_date__lt": self.created_date}, - {"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name__gt": self.assigned_to.last_name}, - {"assigned_to__first_name__gt": self.assigned_to.first_name}, - {"assigned_to__isnull": True}] - else: - conds = [{"assigned_to__isnull": True, - "created_date__lt": self.created_date}, - {"assigned_to__isnull": False}] - - return conds - - def _get_next_neighbor_filters(self, queryset): - conds = super()._get_next_neighbor_filters(queryset) - ordering = queryset.query.order_by - try: - main_order = ordering[0] - except IndexError: - main_order = None - - if main_order == "severity": - conds = [{"severity__order__gt": self.severity.order}, - {"severity__order": self.severity.order, - "created_date__gt": self.created_date}] - elif main_order == "-severity": - conds = [{"severity__order__lt": self.severity.order}, - {"severity__order": self.severity.order, - "created_date__gt": self.created_date}] - elif main_order == "status": - conds = [{"status__order__gt": self.status.order}, - {"status__order": self.status.order, - "created_date__gt": self.created_date}] - elif main_order == "-status": - conds = [{"status__order__lt": self.status.order}, - {"status__order": self.status.order, - "created_date__gt": self.created_date}] - elif main_order == "priority": - conds = [{"priority__order__gt": self.priority.order}, - {"priority__order": self.priority.order, - "created_date__gt": self.created_date}] - elif main_order == "-priority": - conds = [{"priority__order__lt": self.priority.order}, - {"priority__order": self.priority.order, - "created_date__gt": self.created_date}] - elif main_order == "owner__first_name": - conds = [{"owner__first_name": self.owner.first_name, - "owner__last_name": self.owner.last_name, - "created_date__gt": self.created_date}, - {"owner__first_name": self.owner.first_name, - "owner__last_name__gt": self.owner.last_name}, - {"owner__first_name__gt": self.owner.first_name}] - elif main_order == "-owner__first_name": - conds = [{"owner__first_name": self.owner.first_name, - "owner__last_name": self.owner.last_name, - "created_date__gt": self.created_date}, - {"owner__first_name": self.owner.first_name, - "owner__last_name__lt": self.owner.last_name}, - {"owner__first_name__lt": self.owner.first_name}] - elif main_order == "assigned_to__first_name": - if self.assigned_to: - conds = [{"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name": self.assigned_to.last_name, - "created_date__gt": self.created_date}, - {"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name__gt": self.assigned_to.last_name}, - {"assigned_to__first_name__gt": self.assigned_to.first_name}, - {"assigned_to__isnull": True}] - else: - conds = [{"assigned_to__isnull": True, - "created_date__gt": self.created_date}] - elif main_order == "-assigned_to__first_name" and self.assigned_to: - conds = [{"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name": self.assigned_to.last_name, - "created_date__gt": self.created_date}, - {"assigned_to__first_name": self.assigned_to.first_name, - "assigned_to__last_name__lt": self.assigned_to.last_name}, - {"assigned_to__first_name__lt": self.assigned_to.first_name}] - else: - conds = [{"assigned_to__isnull": True, - "created_date__gt": self.created_date}, - {"assigned_to__isnull": False}] - - return conds - @property def is_closed(self): return self.status.is_closed diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 41a8f8bb..d92790c4 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -28,7 +28,7 @@ from taiga.base.decorators import list_route from taiga.base.decorators import action from taiga.base.permissions import has_project_perm from taiga.base.api import ModelCrudViewSet -from taiga.base.api import NeighborsApiMixin + from taiga.projects.mixins.notifications import NotificationSenderMixin from taiga.projects.models import Project from taiga.projects.history.services import take_snapshot @@ -39,7 +39,7 @@ from . import serializers from . import services -class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudViewSet): +class UserStoryViewSet(NotificationSenderMixin, ModelCrudViewSet): model = models.UserStory serializer_class = serializers.UserStoryNeighborsSerializer list_serializer_class = serializers.UserStorySerializer diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index f404ddba..e11b88ab 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -22,7 +22,6 @@ from django.utils.translation import ugettext_lazy as _ from picklefield.fields import PickledObjectField -from taiga.base.models import NeighborsMixin from taiga.base.utils.slug import ref_uniquely from taiga.projects.notifications.models import WatchedMixin from taiga.projects.mixins.blocked import BlockedMixin @@ -52,7 +51,8 @@ class RolePoints(models.Model): return "{}: {}".format(self.role.name, self.points.name) -class UserStory(NeighborsMixin, WatchedMixin, BlockedMixin, models.Model): +class UserStory(WatchedMixin, BlockedMixin, models.Model): + ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, verbose_name=_("ref")) milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True, @@ -112,16 +112,6 @@ class UserStory(NeighborsMixin, WatchedMixin, BlockedMixin, models.Model): def __repr__(self): return "" % (self.id) - def _get_prev_neighbor_filters(self, queryset): - conds = [{"order__lt": "{obj.order}"}, - {"order__lte": "{obj.order}", "ref__lt": "{obj.ref}"}] - return conds - - def _get_next_neighbor_filters(self, queryset): - conds = [{"order__gt": "{obj.order}"}, - {"order__gte": "{obj.order}", "ref__gt": "{obj.ref}"}] - return conds - def get_role_points(self): return self.role_points diff --git a/tests/factories.py b/tests/factories.py index cc39221f..73455e7a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -5,6 +5,9 @@ from django.conf import settings import taiga.domains.models import taiga.projects.models +import taiga.projects.userstories.models +import taiga.projects.issues.models +import taiga.projects.milestones.models import taiga.users.models import taiga.userstorage.models @@ -76,3 +79,61 @@ class StorageEntryFactory(factory.DjangoModelFactory): owner = factory.SubFactory("tests.factories.UserFactory") key = factory.Sequence(lambda n: "key-{}".format(n)) value = factory.Sequence(lambda n: "value {}".format(n)) + + +class UserStoryFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.userstories.models.UserStory + + ref = factory.Sequence(lambda n: n) + project = factory.SubFactory("tests.factories.ProjectFactory") + owner = factory.SubFactory("tests.factories.UserFactory") + subject = factory.Sequence(lambda n: "User Story {}".format(n)) + + +class MilestoneFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.milestones.models.Milestone + + name = factory.Sequence(lambda n: "Milestone {}".format(n)) + owner = factory.SubFactory("tests.factories.UserFactory") + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class IssueFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.issues.models.Issue + + subject = factory.Sequence(lambda n: "Issue {}".format(n)) + owner = factory.SubFactory("tests.factories.UserFactory") + project = factory.SubFactory("tests.factories.ProjectFactory") + status = factory.SubFactory("tests.factories.IssueStatusFactory") + severity = factory.SubFactory("tests.factories.SeverityFactory") + priority = factory.SubFactory("tests.factories.PriorityFactory") + type = factory.SubFactory("tests.factories.IssueTypeFactory") + milestone = factory.SubFactory("tests.factories.MilestoneFactory") + + +class IssueStatusFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.models.IssueStatus + + name = factory.Sequence(lambda n: "Issue Status {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class SeverityFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.models.Severity + + name = factory.Sequence(lambda n: "Severity {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class PriorityFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.models.Priority + + name = factory.Sequence(lambda n: "Priority {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class IssueTypeFactory(factory.DjangoModelFactory): + FACTORY_FOR = taiga.projects.models.IssueType + + name = factory.Sequence(lambda n: "Issue Type {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") diff --git a/tests/unit/test_neighbors.py b/tests/unit/test_neighbors.py new file mode 100644 index 00000000..655f0cdb --- /dev/null +++ b/tests/unit/test_neighbors.py @@ -0,0 +1,304 @@ +from functools import partial +from unittest import mock + +import pytest + +from taiga.projects.userstories.models import UserStory +from taiga.projects.issues.models import Issue +from taiga.base.utils.db import filter_by_tags +from taiga.base import neighbors as n +from .. import factories as f + + +class TestGetAttribute: + def test_no_attribute(self, object): + object.first_name = "name" + with pytest.raises(AttributeError): + n.get_attribute(object, "name") + + with pytest.raises(AttributeError): + n.get_attribute(object, "first_name__last_name") + + def test_one_level(self, object): + object.name = "name" + assert n.get_attribute(object, "name") == object.name + + def test_two_levels(self, object): + object.name = object + object.name.first_name = "first name" + assert n.get_attribute(object, "name__first_name") == object.name.first_name + + def test_three_levels(self, object): + object.info = object + object.info.name = object + object.info.name.first_name = "first name" + assert n.get_attribute(object, "info__name__first_name") == object.info.name.first_name + + +def test_transform_field_into_lookup(): + transform = partial(n.transform_field_into_lookup, value="chuck", operator="__lt", + operator_if_desc="__gt") + + assert transform(name="name") == {"name__lt": "chuck"} + assert transform(name="-name") == {"name__gt": "chuck"} + + +def test_disjunction_filters(): + filters = [{"age__lt": 21, "name__eq": "chuck"}] + result_str = str(n.disjunction_filters(filters)) + + assert result_str.startswith("(OR: ") + assert "('age__lt', 21)" in result_str + assert "('name__eq', 'chuck')" in result_str + + +@pytest.mark.django_db +@pytest.mark.slow +class TestUserStories: + def test_no_filters(self): + project = f.ProjectFactory.create() + + us1 = f.UserStoryFactory.create(project=project) + us2 = f.UserStoryFactory.create(project=project) + us3 = f.UserStoryFactory.create(project=project) + + neighbors = n.get_neighbors(us2) + + assert neighbors.left == us1 + assert neighbors.right == us3 + + def test_filtered_by_tags(self): + tags = ["test"] + project = f.ProjectFactory.create() + + f.UserStoryFactory.create(project=project) + us1 = f.UserStoryFactory.create(project=project, tags=tags) + us2 = f.UserStoryFactory.create(project=project, tags=tags) + + test_user_stories = filter_by_tags(tags, queryset=UserStory.objects.get_queryset()) + + neighbors = n.get_neighbors(us1, results_set=test_user_stories) + + assert neighbors.left is None + assert neighbors.right == us2 + + def test_filtered_by_milestone(self): + project = f.ProjectFactory.create() + milestone = f.MilestoneFactory.create(project=project) + + f.UserStoryFactory.create(project=project) + us1 = f.UserStoryFactory.create(project=project, milestone=milestone) + us2 = f.UserStoryFactory.create(project=project, milestone=milestone) + + milestone_user_stories = UserStory.objects.filter(milestone=milestone) + + neighbors = n.get_neighbors(us1, results_set=milestone_user_stories) + + assert neighbors.left is None + assert neighbors.right == us2 + + +@pytest.mark.django_db +@pytest.mark.slow +class TestIssues: + def test_no_filters(self): + project = f.ProjectFactory.create() + + issue1 = f.IssueFactory.create(project=project) + issue2 = f.IssueFactory.create(project=project) + issue3 = f.IssueFactory.create(project=project) + + neighbors = n.get_neighbors(issue2) + + assert neighbors.left == issue1 + assert neighbors.right == issue3 + + def test_ordering_by_severity(self): + project = f.ProjectFactory.create() + severity1 = f.SeverityFactory.create(project=project, order=1) + severity2 = f.SeverityFactory.create(project=project, order=2) + + issue1 = f.IssueFactory.create(project=project, severity=severity2) + issue2 = f.IssueFactory.create(project=project, severity=severity1) + issue3 = f.IssueFactory.create(project=project, severity=severity1) + + issues = Issue.objects.filter(project=project).order_by("severity") + + issue2_neighbors = n.get_neighbors(issue2, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue2_neighbors.left is None + assert issue2_neighbors.right == issue3 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right == issue1 + + def test_ordering_by_severity_desc(self): + project = f.ProjectFactory.create() + severity1 = f.SeverityFactory.create(project=project, order=1) + severity2 = f.SeverityFactory.create(project=project, order=2) + + issue1 = f.IssueFactory.create(project=project, severity=severity2) + issue2 = f.IssueFactory.create(project=project, severity=severity1) + issue3 = f.IssueFactory.create(project=project, severity=severity1) + + issues = Issue.objects.filter(project=project).order_by("-severity") + + issue1_neighbors = n.get_neighbors(issue1, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue1_neighbors.left is None + assert issue1_neighbors.right == issue2 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right is None + + def test_ordering_by_status(self): + project = f.ProjectFactory.create() + status1 = f.IssueStatusFactory.create(project=project, order=1) + status2 = f.IssueStatusFactory.create(project=project, order=2) + + issue1 = f.IssueFactory.create(project=project, status=status2) + issue2 = f.IssueFactory.create(project=project, status=status1) + issue3 = f.IssueFactory.create(project=project, status=status1) + + issues = Issue.objects.filter(project=project).order_by("status") + + issue2_neighbors = n.get_neighbors(issue2, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue2_neighbors.left is None + assert issue2_neighbors.right == issue3 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right == issue1 + + def test_ordering_by_status_desc(self): + project = f.ProjectFactory.create() + status1 = f.IssueStatusFactory.create(project=project, order=1) + status2 = f.IssueStatusFactory.create(project=project, order=2) + + issue1 = f.IssueFactory.create(project=project, status=status2) + issue2 = f.IssueFactory.create(project=project, status=status1) + issue3 = f.IssueFactory.create(project=project, status=status1) + + issues = Issue.objects.filter(project=project).order_by("-status") + + issue1_neighbors = n.get_neighbors(issue1, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue1_neighbors.left is None + assert issue1_neighbors.right == issue2 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right is None + + def test_ordering_by_priority(self): + project = f.ProjectFactory.create() + priority1 = f.PriorityFactory.create(project=project, order=1) + priority2 = f.PriorityFactory.create(project=project, order=2) + + issue1 = f.IssueFactory.create(project=project, priority=priority2) + issue2 = f.IssueFactory.create(project=project, priority=priority1) + issue3 = f.IssueFactory.create(project=project, priority=priority1) + + issues = Issue.objects.filter(project=project).order_by("priority") + + issue2_neighbors = n.get_neighbors(issue2, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue2_neighbors.left is None + assert issue2_neighbors.right == issue3 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right == issue1 + + def test_ordering_by_priority_desc(self): + project = f.ProjectFactory.create() + priority1 = f.PriorityFactory.create(project=project, order=1) + priority2 = f.PriorityFactory.create(project=project, order=2) + + issue1 = f.IssueFactory.create(project=project, priority=priority2) + issue2 = f.IssueFactory.create(project=project, priority=priority1) + issue3 = f.IssueFactory.create(project=project, priority=priority1) + + issues = Issue.objects.filter(project=project).order_by("-priority") + + issue1_neighbors = n.get_neighbors(issue1, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue1_neighbors.left is None + assert issue1_neighbors.right == issue2 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right is None + + def test_ordering_by_owner(self): + project = f.ProjectFactory.create() + owner1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") + owner2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + + issue1 = f.IssueFactory.create(project=project, owner=owner2) + issue2 = f.IssueFactory.create(project=project, owner=owner1) + issue3 = f.IssueFactory.create(project=project, owner=owner1) + + issues = Issue.objects.filter(project=project).order_by("owner__first_name") + + issue2_neighbors = n.get_neighbors(issue2, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue2_neighbors.left is None + assert issue2_neighbors.right == issue3 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right == issue1 + + def test_ordering_by_owner_desc(self): + project = f.ProjectFactory.create() + owner1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") + owner2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + + issue1 = f.IssueFactory.create(project=project, owner=owner2) + issue2 = f.IssueFactory.create(project=project, owner=owner1) + issue3 = f.IssueFactory.create(project=project, owner=owner1) + + issues = Issue.objects.filter(project=project).order_by("-owner__first_name") + + issue1_neighbors = n.get_neighbors(issue1, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue1_neighbors.left is None + assert issue1_neighbors.right == issue2 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right is None + + def test_ordering_by_assigned_to(self): + project = f.ProjectFactory.create() + assigned_to1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") + assigned_to2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + + issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2) + issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) + issue3 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) + + issues = Issue.objects.filter(project=project).order_by("assigned_to__first_name") + + issue2_neighbors = n.get_neighbors(issue2, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue2_neighbors.left is None + assert issue2_neighbors.right == issue3 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right == issue1 + + def test_ordering_by_assigned_to_desc(self): + project = f.ProjectFactory.create() + assigned_to1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") + assigned_to2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + + issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2) + issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) + issue3 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) + + issues = Issue.objects.filter(project=project).order_by("-assigned_to__first_name") + + issue1_neighbors = n.get_neighbors(issue1, results_set=issues) + issue3_neighbors = n.get_neighbors(issue3, results_set=issues) + + assert issue1_neighbors.left is None + assert issue1_neighbors.right == issue2 + assert issue3_neighbors.left == issue2 + assert issue3_neighbors.right is None