parent
8aebfa4bae
commit
50e00b6d45
|
@ -33,3 +33,4 @@ django-transactional-cleanup==0.1.15
|
||||||
lxml==3.5.0
|
lxml==3.5.0
|
||||||
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
||||||
pyjwkest==1.0.9
|
pyjwkest==1.0.9
|
||||||
|
python-dateutil==2.4.2
|
||||||
|
|
|
@ -81,7 +81,7 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None)
|
||||||
return [field.name for field in obj._meta.fields if field.name not in include]
|
return [field.name for field in obj._meta.fields if field.name not in include]
|
||||||
|
|
||||||
|
|
||||||
class CreateModelMixin(object):
|
class CreateModelMixin:
|
||||||
"""
|
"""
|
||||||
Create a model instance.
|
Create a model instance.
|
||||||
"""
|
"""
|
||||||
|
@ -107,7 +107,7 @@ class CreateModelMixin(object):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class ListModelMixin(object):
|
class ListModelMixin:
|
||||||
"""
|
"""
|
||||||
List a queryset.
|
List a queryset.
|
||||||
"""
|
"""
|
||||||
|
@ -137,7 +137,7 @@ class ListModelMixin(object):
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class RetrieveModelMixin(object):
|
class RetrieveModelMixin:
|
||||||
"""
|
"""
|
||||||
Retrieve a model instance.
|
Retrieve a model instance.
|
||||||
"""
|
"""
|
||||||
|
@ -153,7 +153,7 @@ class RetrieveModelMixin(object):
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class UpdateModelMixin(object):
|
class UpdateModelMixin:
|
||||||
"""
|
"""
|
||||||
Update a model instance.
|
Update a model instance.
|
||||||
"""
|
"""
|
||||||
|
@ -220,7 +220,7 @@ class UpdateModelMixin(object):
|
||||||
obj.full_clean(exclude)
|
obj.full_clean(exclude)
|
||||||
|
|
||||||
|
|
||||||
class DestroyModelMixin(object):
|
class DestroyModelMixin:
|
||||||
"""
|
"""
|
||||||
Destroy a model instance.
|
Destroy a model instance.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -183,4 +183,5 @@ def dict_to_project(data, owner=None):
|
||||||
if service.get_errors(clear=False):
|
if service.get_errors(clear=False):
|
||||||
raise TaigaImportError(_("error importing timelines"))
|
raise TaigaImportError(_("error importing timelines"))
|
||||||
|
|
||||||
|
proj.refresh_totals()
|
||||||
return proj
|
return proj
|
||||||
|
|
|
@ -17,9 +17,13 @@
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db.models import signals
|
from django.apps import apps
|
||||||
|
from django.db.models import signals, Prefetch
|
||||||
|
from django.db.models import Value as V
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
@ -32,12 +36,9 @@ from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
|
from taiga.projects.notifications.models import NotifyPolicy
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
from taiga.projects.notifications.utils import (
|
|
||||||
attach_project_total_watchers_attrs_to_queryset,
|
|
||||||
attach_project_is_watcher_to_queryset,
|
|
||||||
attach_notify_level_to_project_queryset)
|
|
||||||
|
|
||||||
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
||||||
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
||||||
|
@ -53,6 +54,7 @@ from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Project
|
## Project
|
||||||
|
@ -64,6 +66,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
list_serializer_class = serializers.ProjectSerializer
|
list_serializer_class = serializers.ProjectSerializer
|
||||||
permission_classes = (permissions.ProjectPermission, )
|
permission_classes = (permissions.ProjectPermission, )
|
||||||
filter_backends = (filters.CanViewProjectObjFilterBackend,)
|
filter_backends = (filters.CanViewProjectObjFilterBackend,)
|
||||||
|
|
||||||
filter_fields = (('member', 'members'),
|
filter_fields = (('member', 'members'),
|
||||||
'is_looking_for_people',
|
'is_looking_for_people',
|
||||||
'is_featured',
|
'is_featured',
|
||||||
|
@ -71,24 +74,58 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
'is_kanban_activated')
|
'is_kanban_activated')
|
||||||
|
|
||||||
order_by_fields = ("memberships__user_order",
|
order_by_fields = ("memberships__user_order",
|
||||||
"total_fans")
|
"total_fans",
|
||||||
|
"total_fans_last_week",
|
||||||
|
"total_fans_last_month",
|
||||||
|
"total_fans_last_year",
|
||||||
|
"total_activity",
|
||||||
|
"total_activity_last_week",
|
||||||
|
"total_activity_last_month",
|
||||||
|
"total_activity_last_year")
|
||||||
|
|
||||||
|
def _get_order_by_field_name(self):
|
||||||
|
order_by_query_param = filters.CanViewProjectObjFilterBackend.order_by_query_param
|
||||||
|
order_by = self.request.QUERY_PARAMS.get(order_by_query_param, None)
|
||||||
|
if order_by is not None and order_by.startswith("-"):
|
||||||
|
return order_by[1:]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = self.attach_likes_attrs_to_queryset(qs)
|
|
||||||
qs = attach_project_total_watchers_attrs_to_queryset(qs)
|
# Prefetch doesn't work correctly if then if the field is filtered later (it generates more queries)
|
||||||
if self.request.user.is_authenticated():
|
# so we add some custom prefetching
|
||||||
qs = attach_project_is_watcher_to_queryset(qs, self.request.user)
|
qs = qs.prefetch_related("members")
|
||||||
qs = attach_notify_level_to_project_queryset(qs, self.request.user)
|
qs = qs.prefetch_related(Prefetch("notify_policies",
|
||||||
|
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
|
||||||
|
|
||||||
|
Milestone = apps.get_model("milestones", "Milestone")
|
||||||
|
qs = qs.prefetch_related(Prefetch("milestones",
|
||||||
|
Milestone.objects.filter(closed=True), to_attr="closed_milestones"))
|
||||||
|
|
||||||
|
# If filtering an activity period we must exclude the activities not updated recently enough
|
||||||
|
now = timezone.now()
|
||||||
|
order_by_field_name = self._get_order_by_field_name()
|
||||||
|
if order_by_field_name == "total_fans_last_week":
|
||||||
|
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1))
|
||||||
|
elif order_by_field_name == "total_fans_last_month":
|
||||||
|
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1))
|
||||||
|
elif order_by_field_name == "total_fans_last_year":
|
||||||
|
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1))
|
||||||
|
elif order_by_field_name == "total_activity_last_week":
|
||||||
|
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1))
|
||||||
|
elif order_by_field_name == "total_activity_last_month":
|
||||||
|
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1))
|
||||||
|
elif order_by_field_name == "total_activity_last_year":
|
||||||
|
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1))
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == "list":
|
serializer_class = self.serializer_class
|
||||||
return self.list_serializer_class
|
|
||||||
elif self.action == "create":
|
|
||||||
return self.serializer_class
|
|
||||||
|
|
||||||
|
if self.action == "list":
|
||||||
|
serializer_class = self.list_serializer_class
|
||||||
|
elif self.action != "create":
|
||||||
if self.action == "by_slug":
|
if self.action == "by_slug":
|
||||||
slug = self.request.QUERY_PARAMS.get("slug", None)
|
slug = self.request.QUERY_PARAMS.get("slug", None)
|
||||||
project = get_object_or_404(models.Project, slug=slug)
|
project = get_object_or_404(models.Project, slug=slug)
|
||||||
|
@ -96,9 +133,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
|
|
||||||
if permissions_service.is_project_owner(self.request.user, project):
|
if permissions_service.is_project_owner(self.request.user, project):
|
||||||
return self.admin_serializer_class
|
serializer_class = self.admin_serializer_class
|
||||||
|
|
||||||
return self.serializer_class
|
return serializer_class
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def watch(self, request, pk=None):
|
def watch(self, request, pk=None):
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('likes', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='likes',
|
||||||
|
unique_together=set([]),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='likes',
|
||||||
|
name='content_type',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Likes',
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,12 +20,10 @@ from taiga.base.api import serializers
|
||||||
|
|
||||||
class FanResourceSerializerMixin(serializers.ModelSerializer):
|
class FanResourceSerializerMixin(serializers.ModelSerializer):
|
||||||
is_fan = serializers.SerializerMethodField("get_is_fan")
|
is_fan = serializers.SerializerMethodField("get_is_fan")
|
||||||
total_fans = serializers.SerializerMethodField("get_total_fans")
|
|
||||||
|
|
||||||
def get_is_fan(self, obj):
|
def get_is_fan(self, obj):
|
||||||
# The "is_fan" attribute is attached in the get_queryset of the viewset.
|
if "request" in self.context:
|
||||||
return getattr(obj, "is_fan", False) or False
|
user = self.context["request"].user
|
||||||
|
return user.is_authenticated() and user.is_fan(obj)
|
||||||
|
|
||||||
def get_total_fans(self, obj):
|
return False
|
||||||
# The "total_fans" attribute is attached in the get_queryset of the viewset.
|
|
||||||
return getattr(obj, "total_fans", 0) or 0
|
|
||||||
|
|
|
@ -24,23 +24,9 @@ from taiga.base.decorators import detail_route
|
||||||
|
|
||||||
from taiga.projects.likes import serializers
|
from taiga.projects.likes import serializers
|
||||||
from taiga.projects.likes import services
|
from taiga.projects.likes import services
|
||||||
from taiga.projects.likes.utils import attach_total_fans_to_queryset, attach_is_fan_to_queryset
|
|
||||||
|
|
||||||
|
|
||||||
class LikedResourceMixin:
|
class LikedResourceMixin:
|
||||||
# Note: Update get_queryset method:
|
|
||||||
# def get_queryset(self):
|
|
||||||
# qs = super().get_queryset()
|
|
||||||
# return self.attach_likes_attrs_to_queryset(qs)
|
|
||||||
|
|
||||||
def attach_likes_attrs_to_queryset(self, queryset):
|
|
||||||
qs = attach_total_fans_to_queryset(queryset)
|
|
||||||
|
|
||||||
if self.request.user.is_authenticated():
|
|
||||||
qs = attach_is_fan_to_queryset(self.request.user, qs)
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def like(self, request, pk=None):
|
def like(self, request, pk=None):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
|
|
@ -22,27 +22,6 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Likes(models.Model):
|
|
||||||
content_type = models.ForeignKey("contenttypes.ContentType")
|
|
||||||
object_id = models.PositiveIntegerField()
|
|
||||||
content_object = generic.GenericForeignKey("content_type", "object_id")
|
|
||||||
count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count"))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Likes")
|
|
||||||
verbose_name_plural = _("Likes")
|
|
||||||
unique_together = ("content_type", "object_id")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def project(self):
|
|
||||||
if hasattr(self.content_object, 'project'):
|
|
||||||
return self.content_object.project
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.count
|
|
||||||
|
|
||||||
|
|
||||||
class Like(models.Model):
|
class Like(models.Model):
|
||||||
content_type = models.ForeignKey("contenttypes.ContentType")
|
content_type = models.ForeignKey("contenttypes.ContentType")
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
|
|
|
@ -21,7 +21,7 @@ from django.db.transaction import atomic
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from .models import Likes, Like
|
from .models import Like
|
||||||
|
|
||||||
|
|
||||||
def add_like(obj, user):
|
def add_like(obj, user):
|
||||||
|
@ -36,12 +36,9 @@ def add_like(obj, user):
|
||||||
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
with atomic():
|
with atomic():
|
||||||
like, created = Like.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
|
like, created = Like.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
|
||||||
if not created:
|
if like.project is not None:
|
||||||
return
|
like.project.refresh_totals()
|
||||||
|
|
||||||
likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
|
|
||||||
likes.count = F('count') + 1
|
|
||||||
likes.save()
|
|
||||||
return like
|
return like
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,11 +57,12 @@ def remove_like(obj, user):
|
||||||
if not qs.exists():
|
if not qs.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
like = qs.first()
|
||||||
|
project = like.project
|
||||||
qs.delete()
|
qs.delete()
|
||||||
|
|
||||||
likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
|
if project is not None:
|
||||||
likes.count = F('count') - 1
|
project.refresh_totals()
|
||||||
likes.save()
|
|
||||||
|
|
||||||
|
|
||||||
def get_fans(obj):
|
def get_fans(obj):
|
||||||
|
@ -78,21 +76,6 @@ def get_fans(obj):
|
||||||
return get_user_model().objects.filter(likes__content_type=obj_type, likes__object_id=obj.id)
|
return get_user_model().objects.filter(likes__content_type=obj_type, likes__object_id=obj.id)
|
||||||
|
|
||||||
|
|
||||||
def get_likes(obj):
|
|
||||||
"""Get the number of likes an object has.
|
|
||||||
|
|
||||||
:param obj: Any Django model instance.
|
|
||||||
|
|
||||||
:return: Number of likes or `0` if the object has no likes at all.
|
|
||||||
"""
|
|
||||||
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return Likes.objects.get(content_type=obj_type, object_id=obj.id).count
|
|
||||||
except Likes.DoesNotExist:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_liked(user_or_id, model):
|
def get_liked(user_or_id, model):
|
||||||
"""Get the objects liked by an user.
|
"""Get the objects liked by an user.
|
||||||
|
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
|
|
||||||
|
|
||||||
def attach_total_fans_to_queryset(queryset, as_field="total_fans"):
|
|
||||||
"""Attach likes count to each object of the queryset.
|
|
||||||
|
|
||||||
Because of laziness of like objects creation, this makes much simpler and more efficient to
|
|
||||||
access to liked-object number of likes.
|
|
||||||
|
|
||||||
(The other way was to do it in the serializer with some try/except blocks and additional
|
|
||||||
queries)
|
|
||||||
|
|
||||||
:param queryset: A Django queryset object.
|
|
||||||
:param as_field: Attach the likes-count as an attribute with this name.
|
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
|
||||||
"""
|
|
||||||
model = queryset.model
|
|
||||||
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
|
||||||
sql = """SELECT coalesce(SUM(total_fans), 0) FROM (
|
|
||||||
SELECT coalesce(likes_likes.count, 0) total_fans
|
|
||||||
FROM likes_likes
|
|
||||||
WHERE likes_likes.content_type_id = {type_id}
|
|
||||||
AND likes_likes.object_id = {tbl}.id
|
|
||||||
) as e"""
|
|
||||||
|
|
||||||
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
|
|
||||||
qs = queryset.extra(select={as_field: sql})
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def attach_is_fan_to_queryset(user, queryset, as_field="is_fan"):
|
|
||||||
"""Attach is_like boolean to each object of the queryset.
|
|
||||||
|
|
||||||
Because of laziness of like objects creation, this makes much simpler and more efficient to
|
|
||||||
access to likes-object and check if the curren user like it.
|
|
||||||
|
|
||||||
(The other way was to do it in the serializer with some try/except blocks and additional
|
|
||||||
queries)
|
|
||||||
|
|
||||||
:param user: A users.User object model
|
|
||||||
:param queryset: A Django queryset object.
|
|
||||||
:param as_field: Attach the boolean as an attribute with this name.
|
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
|
||||||
"""
|
|
||||||
model = queryset.model
|
|
||||||
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
|
||||||
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
|
||||||
FROM likes_like
|
|
||||||
WHERE likes_like.content_type_id = {type_id}
|
|
||||||
AND likes_like.object_id = {tbl}.id
|
|
||||||
AND likes_like.user_id = {user_id}) > 0
|
|
||||||
THEN TRUE
|
|
||||||
ELSE FALSE
|
|
||||||
END""")
|
|
||||||
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
|
|
||||||
qs = queryset.extra(select={as_field: sql})
|
|
||||||
return qs
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import connection, migrations, models
|
||||||
|
from django.utils.timezone import utc
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def update_totals(apps, schema_editor):
|
||||||
|
model = apps.get_model("projects", "Project")
|
||||||
|
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
||||||
|
sql="""
|
||||||
|
UPDATE projects_project
|
||||||
|
SET
|
||||||
|
totals_updated_datetime = totals.totals_updated_datetime,
|
||||||
|
total_fans = totals.total_fans,
|
||||||
|
total_fans_last_week = totals.total_fans_last_week,
|
||||||
|
total_fans_last_month = totals.total_fans_last_month,
|
||||||
|
total_fans_last_year = totals.total_fans_last_year,
|
||||||
|
total_activity = totals.total_activity,
|
||||||
|
total_activity_last_week = totals.total_activity_last_week,
|
||||||
|
total_activity_last_month = totals.total_activity_last_month,
|
||||||
|
total_activity_last_year = totals.total_activity_last_year
|
||||||
|
FROM (
|
||||||
|
WITH
|
||||||
|
totals_activity AS (SELECT
|
||||||
|
split_part(timeline_timeline.namespace, ':', 2)::integer as project_id,
|
||||||
|
count(timeline_timeline.namespace) total_activity,
|
||||||
|
MAX (created) updated_datetime
|
||||||
|
FROM timeline_timeline
|
||||||
|
WHERE namespace LIKE 'project:%'
|
||||||
|
GROUP BY namespace),
|
||||||
|
totals_activity_week AS (SELECT
|
||||||
|
split_part(timeline_timeline.namespace, ':', 2)::integer as project_id,
|
||||||
|
count(timeline_timeline.namespace) total_activity_last_week
|
||||||
|
FROM timeline_timeline
|
||||||
|
WHERE namespace LIKE 'project:%'
|
||||||
|
AND timeline_timeline.created > current_date - interval '7' day
|
||||||
|
GROUP BY namespace),
|
||||||
|
totals_activity_month AS (SELECT
|
||||||
|
split_part(timeline_timeline.namespace, ':', 2)::integer as project_id,
|
||||||
|
count(timeline_timeline.namespace) total_activity_last_month
|
||||||
|
FROM timeline_timeline
|
||||||
|
WHERE namespace LIKE 'project:%'
|
||||||
|
AND timeline_timeline.created > current_date - interval '30' day
|
||||||
|
GROUP BY namespace),
|
||||||
|
totals_activity_year AS (SELECT
|
||||||
|
split_part(timeline_timeline.namespace, ':', 2)::integer as project_id,
|
||||||
|
count(timeline_timeline.namespace) total_activity_last_year
|
||||||
|
FROM timeline_timeline
|
||||||
|
WHERE namespace LIKE 'project:%'
|
||||||
|
AND timeline_timeline.created > current_date - interval '365' day
|
||||||
|
GROUP BY namespace),
|
||||||
|
totals_fans AS (SELECT
|
||||||
|
object_id as project_id,
|
||||||
|
COUNT(likes_like.object_id) total_fans,
|
||||||
|
MAX (created_date) updated_datetime
|
||||||
|
FROM likes_like
|
||||||
|
WHERE content_type_id = {type_id}
|
||||||
|
GROUP BY object_id),
|
||||||
|
totals_fans_week AS (SELECT
|
||||||
|
object_id as project_id,
|
||||||
|
COUNT(likes_like.object_id) total_fans_last_week
|
||||||
|
FROM likes_like
|
||||||
|
WHERE content_type_id = {type_id}
|
||||||
|
AND likes_like.created_date > current_date - interval '7' day
|
||||||
|
GROUP BY object_id),
|
||||||
|
totals_fans_month AS (SELECT
|
||||||
|
object_id as project_id,
|
||||||
|
COUNT(likes_like.object_id) total_fans_last_month
|
||||||
|
FROM likes_like
|
||||||
|
WHERE content_type_id = {type_id}
|
||||||
|
AND likes_like.created_date > current_date - interval '30' day
|
||||||
|
GROUP BY object_id),
|
||||||
|
totals_fans_year AS (SELECT
|
||||||
|
object_id as project_id,
|
||||||
|
COUNT(likes_like.object_id) total_fans_last_year
|
||||||
|
FROM likes_like
|
||||||
|
WHERE content_type_id = {type_id}
|
||||||
|
AND likes_like.created_date > current_date - interval '365' day
|
||||||
|
GROUP BY object_id)
|
||||||
|
SELECT
|
||||||
|
totals_activity.project_id,
|
||||||
|
COALESCE(total_activity, 0) total_activity,
|
||||||
|
COALESCE(total_activity_last_week, 0) total_activity_last_week,
|
||||||
|
COALESCE(total_activity_last_month, 0) total_activity_last_month,
|
||||||
|
COALESCE(total_activity_last_year, 0) total_activity_last_year,
|
||||||
|
COALESCE(total_fans, 0) total_fans,
|
||||||
|
COALESCE(total_fans_last_week, 0) total_fans_last_week,
|
||||||
|
COALESCE(total_fans_last_month, 0) total_fans_last_month,
|
||||||
|
COALESCE(total_fans_last_year, 0) total_fans_last_year,
|
||||||
|
totals_activity.updated_datetime totals_updated_datetime
|
||||||
|
FROM totals_activity
|
||||||
|
LEFT JOIN totals_fans ON totals_activity.project_id = totals_fans.project_id
|
||||||
|
LEFT JOIN totals_fans_week ON totals_activity.project_id = totals_fans_week.project_id
|
||||||
|
LEFT JOIN totals_fans_month ON totals_activity.project_id = totals_fans_month.project_id
|
||||||
|
LEFT JOIN totals_fans_year ON totals_activity.project_id = totals_fans_year.project_id
|
||||||
|
LEFT JOIN totals_activity_week ON totals_activity.project_id = totals_activity_week.project_id
|
||||||
|
LEFT JOIN totals_activity_month ON totals_activity.project_id = totals_activity_month.project_id
|
||||||
|
LEFT JOIN totals_activity_year ON totals_activity.project_id = totals_activity_year.project_id
|
||||||
|
) totals
|
||||||
|
WHERE projects_project.id = totals.project_id
|
||||||
|
""".format(type_id=type.id)
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0029_project_is_looking_for_people'),
|
||||||
|
('timeline', '0004_auto_20150603_1312'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_activity',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='count', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_activity_last_month',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='activity last month', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_activity_last_week',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='activity last week', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_activity_last_year',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='activity last year', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_fans',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='count', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_fans_last_month',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='fans last month', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_fans_last_week',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='fans last week', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='total_fans_last_year',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='fans last year', db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='totals_updated_datetime',
|
||||||
|
field=models.DateTimeField(default=datetime.datetime(2015, 11, 28, 7, 57, 11, 743976, tzinfo=utc), auto_now_add=True, verbose_name='updated date time', db_index=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_totals),
|
||||||
|
]
|
|
@ -84,7 +84,7 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
|
||||||
|
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated():
|
||||||
us_qs = attach_is_voter_to_queryset(self.request.user, us_qs)
|
us_qs = attach_is_voter_to_queryset(self.request.user, us_qs)
|
||||||
us_qs = attach_is_watcher_to_queryset(self.request.user, us_qs)
|
us_qs = attach_is_watcher_to_queryset(us_qs, self.request.user)
|
||||||
|
|
||||||
qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs))
|
qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs))
|
||||||
|
|
||||||
|
|
|
@ -46,8 +46,12 @@ from taiga.projects.notifications.services import (
|
||||||
set_notify_policy_level_to_ignore,
|
set_notify_policy_level_to_ignore,
|
||||||
create_notify_policy_if_not_exists)
|
create_notify_policy_if_not_exists)
|
||||||
|
|
||||||
|
from taiga.timeline.service import build_project_namespace
|
||||||
|
|
||||||
from . import choices
|
from . import choices
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
# This model stores all project memberships. Also
|
# This model stores all project memberships. Also
|
||||||
|
@ -198,6 +202,36 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
||||||
|
|
||||||
tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
|
tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
|
||||||
verbose_name=_("tags colors"))
|
verbose_name=_("tags colors"))
|
||||||
|
|
||||||
|
#Totals:
|
||||||
|
totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
|
||||||
|
verbose_name=_("updated date time"), db_index=True)
|
||||||
|
|
||||||
|
total_fans = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("count"), db_index=True)
|
||||||
|
|
||||||
|
total_fans_last_week = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("fans last week"), db_index=True)
|
||||||
|
|
||||||
|
total_fans_last_month = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("fans last month"), db_index=True)
|
||||||
|
|
||||||
|
total_fans_last_year = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("fans last year"), db_index=True)
|
||||||
|
|
||||||
|
total_activity = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("count"), db_index=True)
|
||||||
|
|
||||||
|
total_activity_last_week = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("activity last week"), db_index=True)
|
||||||
|
|
||||||
|
total_activity_last_month = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("activity last month"), db_index=True)
|
||||||
|
|
||||||
|
total_activity_last_year = models.PositiveIntegerField(null=False, blank=False, default=0,
|
||||||
|
verbose_name=_("activity last year"), db_index=True)
|
||||||
|
|
||||||
|
_cached_user_stories = None
|
||||||
_importing = None
|
_importing = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -233,6 +267,51 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def refresh_totals(self, save=True):
|
||||||
|
now = timezone.now()
|
||||||
|
self.totals_updated_datetime = now
|
||||||
|
|
||||||
|
Like = apps.get_model("likes", "Like")
|
||||||
|
content_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(Project)
|
||||||
|
qs = Like.objects.filter(content_type=content_type, object_id=self.id)
|
||||||
|
|
||||||
|
self.total_fans = qs.count()
|
||||||
|
|
||||||
|
qs_week = qs.filter(created_date__gte=now-relativedelta(weeks=1))
|
||||||
|
self.total_fans_last_week = qs_week.count()
|
||||||
|
|
||||||
|
qs_month = qs.filter(created_date__gte=now-relativedelta(months=1))
|
||||||
|
self.total_fans_last_month = qs_month.count()
|
||||||
|
|
||||||
|
qs_year = qs.filter(created_date__gte=now-relativedelta(years=1))
|
||||||
|
self.total_fans_last_year = qs_year.count()
|
||||||
|
|
||||||
|
tl_model = apps.get_model("timeline", "Timeline")
|
||||||
|
namespace = build_project_namespace(self)
|
||||||
|
|
||||||
|
qs = tl_model.objects.filter(namespace=namespace)
|
||||||
|
self.total_activity = qs.count()
|
||||||
|
|
||||||
|
qs_week = qs.filter(created__gte=now-relativedelta(weeks=1))
|
||||||
|
self.total_activity_last_week = qs_week.count()
|
||||||
|
|
||||||
|
qs_month = qs.filter(created__gte=now-relativedelta(months=1))
|
||||||
|
self.total_activity_last_month = qs_month.count()
|
||||||
|
|
||||||
|
qs_year = qs.filter(created__gte=now-relativedelta(years=1))
|
||||||
|
self.total_activity_last_year = qs_year.count()
|
||||||
|
|
||||||
|
if save:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cached_user_stories(self):
|
||||||
|
print(1111111, self._cached_user_stories)
|
||||||
|
if self._cached_user_stories is None:
|
||||||
|
self._cached_user_stories = list(self.user_stories.all())
|
||||||
|
|
||||||
|
return self._cached_user_stories
|
||||||
|
|
||||||
def get_roles(self):
|
def get_roles(self):
|
||||||
return self.roles.all()
|
return self.roles.all()
|
||||||
|
|
||||||
|
|
|
@ -53,12 +53,12 @@ class WatchedResourceMixin:
|
||||||
_not_notify = False
|
_not_notify = False
|
||||||
|
|
||||||
def attach_watchers_attrs_to_queryset(self, queryset):
|
def attach_watchers_attrs_to_queryset(self, queryset):
|
||||||
qs = attach_watchers_to_queryset(queryset)
|
queryset = attach_watchers_to_queryset(queryset)
|
||||||
qs = attach_total_watchers_to_queryset(qs)
|
queryset = attach_total_watchers_to_queryset(queryset)
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated():
|
||||||
qs = attach_is_watcher_to_queryset(self.request.user, qs)
|
queryset = attach_is_watcher_to_queryset(queryset, self.request.user)
|
||||||
|
|
||||||
return qs
|
return queryset
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def watch(self, request, pk=None):
|
def watch(self, request, pk=None):
|
||||||
|
@ -187,8 +187,11 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
||||||
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
||||||
|
|
||||||
def get_is_watcher(self, obj):
|
def get_is_watcher(self, obj):
|
||||||
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
|
if "request" in self.context:
|
||||||
return getattr(obj, "is_watcher", False) or False
|
user = self.context["request"].user
|
||||||
|
return user.is_authenticated() and user.is_watcher(obj)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def get_total_watchers(self, obj):
|
def get_total_watchers(self, obj):
|
||||||
# The "total_watchers" attribute is attached in the get_queryset of the viewset.
|
# The "total_watchers" attribute is attached in the get_queryset of the viewset.
|
||||||
|
|
|
@ -41,11 +41,11 @@ def attach_watchers_to_queryset(queryset, as_field="watchers"):
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def attach_is_watcher_to_queryset(user, queryset, as_field="is_watcher"):
|
def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"):
|
||||||
"""Attach is_watcher boolean to each object of the queryset.
|
"""Attach is_watcher boolean to each object of the queryset.
|
||||||
|
|
||||||
:param user: A users.User object model
|
|
||||||
:param queryset: A Django queryset object.
|
:param queryset: A Django queryset object.
|
||||||
|
:param user: A users.User object model
|
||||||
:param as_field: Attach the boolean as an attribute with this name.
|
:param as_field: Attach the boolean as an attribute with this name.
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
:return: Queryset object with the additional `as_field` field.
|
||||||
|
@ -83,74 +83,3 @@ def attach_total_watchers_to_queryset(queryset, as_field="total_watchers"):
|
||||||
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
|
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
|
||||||
qs = queryset.extra(select={as_field: sql})
|
qs = queryset.extra(select={as_field: sql})
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def attach_project_is_watcher_to_queryset(queryset, user, as_field="is_watcher"):
|
|
||||||
"""Attach is_watcher boolean to each object of the projects queryset.
|
|
||||||
|
|
||||||
:param user: A users.User object model
|
|
||||||
:param queryset: A Django projects queryset object.
|
|
||||||
:param as_field: Attach the boolean as an attribute with this name.
|
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
|
||||||
"""
|
|
||||||
model = queryset.model
|
|
||||||
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
|
||||||
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
|
||||||
FROM notifications_notifypolicy
|
|
||||||
WHERE notifications_notifypolicy.project_id = {tbl}.id
|
|
||||||
AND notifications_notifypolicy.user_id = {user_id}
|
|
||||||
AND notifications_notifypolicy.notify_level != {ignore_notify_level}) > 0
|
|
||||||
|
|
||||||
THEN TRUE
|
|
||||||
ELSE FALSE
|
|
||||||
END""")
|
|
||||||
sql = sql.format(tbl=model._meta.db_table, user_id=user.id, ignore_notify_level=NotifyLevel.none)
|
|
||||||
qs = queryset.extra(select={as_field: sql})
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def attach_project_total_watchers_attrs_to_queryset(queryset, as_field="total_watchers"):
|
|
||||||
"""Attach watching user ids to each project of the queryset.
|
|
||||||
|
|
||||||
:param queryset: A Django projects queryset object.
|
|
||||||
:param as_field: Attach the watchers as an attribute with this name.
|
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
|
||||||
"""
|
|
||||||
model = queryset.model
|
|
||||||
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
|
||||||
|
|
||||||
sql = ("""SELECT count(user_id)
|
|
||||||
FROM notifications_notifypolicy
|
|
||||||
WHERE notifications_notifypolicy.project_id = {tbl}.id
|
|
||||||
AND notifications_notifypolicy.notify_level != {ignore_notify_level}""")
|
|
||||||
sql = sql.format(tbl=model._meta.db_table, ignore_notify_level=NotifyLevel.none)
|
|
||||||
qs = queryset.extra(select={as_field: sql})
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def attach_notify_level_to_project_queryset(queryset, user):
|
|
||||||
"""
|
|
||||||
Function that attach "notify_level" attribute on each queryset
|
|
||||||
result for query notification level of current user for each
|
|
||||||
project in the most efficient way.
|
|
||||||
|
|
||||||
:param queryset: A Django queryset object.
|
|
||||||
:param user: A User model object.
|
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
|
||||||
"""
|
|
||||||
user_id = getattr(user, "id", None) or "NULL"
|
|
||||||
default_level = NotifyLevel.involved
|
|
||||||
|
|
||||||
sql = strip_lines("""
|
|
||||||
COALESCE((SELECT notifications_notifypolicy.notify_level
|
|
||||||
FROM notifications_notifypolicy
|
|
||||||
WHERE notifications_notifypolicy.project_id = projects_project.id
|
|
||||||
AND notifications_notifypolicy.user_id = {user_id}),
|
|
||||||
{default_level})
|
|
||||||
""")
|
|
||||||
sql = sql.format(user_id=user_id, default_level=default_level)
|
|
||||||
return queryset.extra(select={"notify_level": sql})
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.fields import TagsField
|
from taiga.base.fields import TagsField
|
||||||
from taiga.base.fields import TagsColorsField
|
from taiga.base.fields import TagsColorsField
|
||||||
|
|
||||||
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
from taiga.users.services import get_photo_or_gravatar_url
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
from taiga.users.serializers import UserSerializer
|
from taiga.users.serializers import UserSerializer
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -318,6 +319,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
|
||||||
tags_colors = TagsColorsField(required=False)
|
tags_colors = TagsColorsField(required=False)
|
||||||
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
|
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
|
||||||
notify_level = serializers.SerializerMethodField("get_notify_level")
|
notify_level = serializers.SerializerMethodField("get_notify_level")
|
||||||
|
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Project
|
model = models.Project
|
||||||
|
@ -336,10 +338,27 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_total_closed_milestones(self, obj):
|
def get_total_closed_milestones(self, obj):
|
||||||
|
# The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
|
||||||
|
qs_closed_milestones = getattr(obj, "closed_milestones", None)
|
||||||
|
if qs_closed_milestones is not None:
|
||||||
|
return qs_closed_milestones
|
||||||
|
|
||||||
return obj.milestones.filter(closed=True).count()
|
return obj.milestones.filter(closed=True).count()
|
||||||
|
|
||||||
def get_notify_level(self, obj):
|
def get_notify_level(self, obj):
|
||||||
return getattr(obj, "notify_level", None)
|
if "request" in self.context:
|
||||||
|
user = self.context["request"].user
|
||||||
|
return user.is_authenticated() and user.get_notify_level(obj)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_total_watchers(self, obj):
|
||||||
|
# The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset.
|
||||||
|
qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None)
|
||||||
|
if qs_valid_notify_policies is not None:
|
||||||
|
return len(qs_valid_notify_policies)
|
||||||
|
|
||||||
|
return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailSerializer(ProjectSerializer):
|
class ProjectDetailSerializer(ProjectSerializer):
|
||||||
|
|
|
@ -46,6 +46,8 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
|
||||||
namespace=build_project_namespace(project),
|
namespace=build_project_namespace(project),
|
||||||
extra_data=extra_data)
|
extra_data=extra_data)
|
||||||
|
|
||||||
|
project.refresh_totals()
|
||||||
|
|
||||||
if hasattr(obj, "get_related_people"):
|
if hasattr(obj, "get_related_people"):
|
||||||
related_people = obj.get_related_people()
|
related_people = obj.get_related_people()
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,7 @@ def _build_watched_sql_for_projects(for_user):
|
||||||
tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project,
|
tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project,
|
||||||
slug, projects_project.name, null::text AS subject,
|
slug, projects_project.name, null::text AS subject,
|
||||||
notifications_notifypolicy.created_at as created_date,
|
notifications_notifypolicy.created_at as created_date,
|
||||||
coalesce(watchers, 0) AS total_watchers, coalesce(likes_likes.count, 0) AS total_fans, null::integer AS total_voters,
|
coalesce(watchers, 0) AS total_watchers, projects_project.total_fans AS total_fans, null::integer AS total_voters,
|
||||||
null::integer AS assigned_to, null::text as status, null::text as status_color
|
null::integer AS assigned_to, null::text as status, null::text as status_color
|
||||||
FROM notifications_notifypolicy
|
FROM notifications_notifypolicy
|
||||||
INNER JOIN projects_project
|
INNER JOIN projects_project
|
||||||
|
@ -235,8 +235,6 @@ def _build_watched_sql_for_projects(for_user):
|
||||||
GROUP BY project_id
|
GROUP BY project_id
|
||||||
) type_watchers
|
) type_watchers
|
||||||
ON projects_project.id = type_watchers.project_id
|
ON projects_project.id = type_watchers.project_id
|
||||||
LEFT JOIN likes_likes
|
|
||||||
ON (projects_project.id = likes_likes.object_id AND {project_content_type_id} = likes_likes.content_type_id)
|
|
||||||
WHERE
|
WHERE
|
||||||
notifications_notifypolicy.user_id = {for_user_id}
|
notifications_notifypolicy.user_id = {for_user_id}
|
||||||
AND notifications_notifypolicy.notify_level != {none_notify_level}
|
AND notifications_notifypolicy.notify_level != {none_notify_level}
|
||||||
|
@ -254,7 +252,7 @@ def _build_liked_sql_for_projects(for_user):
|
||||||
tags, likes_like.object_id AS object_id, projects_project.id AS project,
|
tags, likes_like.object_id AS object_id, projects_project.id AS project,
|
||||||
slug, projects_project.name, null::text AS subject,
|
slug, projects_project.name, null::text AS subject,
|
||||||
likes_like.created_date,
|
likes_like.created_date,
|
||||||
coalesce(watchers, 0) AS total_watchers, coalesce(likes_likes.count, 0) AS total_fans,
|
coalesce(watchers, 0) AS total_watchers, projects_project.total_fans AS total_fans,
|
||||||
null::integer AS assigned_to, null::text as status, null::text as status_color
|
null::integer AS assigned_to, null::text as status, null::text as status_color
|
||||||
FROM likes_like
|
FROM likes_like
|
||||||
INNER JOIN projects_project
|
INNER JOIN projects_project
|
||||||
|
@ -265,8 +263,6 @@ def _build_liked_sql_for_projects(for_user):
|
||||||
GROUP BY project_id
|
GROUP BY project_id
|
||||||
) type_watchers
|
) type_watchers
|
||||||
ON projects_project.id = type_watchers.project_id
|
ON projects_project.id = type_watchers.project_id
|
||||||
LEFT JOIN likes_likes
|
|
||||||
ON (projects_project.id = likes_likes.object_id AND {project_content_type_id} = likes_likes.content_type_id)
|
|
||||||
WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id
|
WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id
|
||||||
"""
|
"""
|
||||||
sql = sql.format(
|
sql = sql.format(
|
||||||
|
|
|
@ -423,15 +423,6 @@ class LikeFactory(Factory):
|
||||||
user = factory.SubFactory("tests.factories.UserFactory")
|
user = factory.SubFactory("tests.factories.UserFactory")
|
||||||
|
|
||||||
|
|
||||||
class LikesFactory(Factory):
|
|
||||||
class Meta:
|
|
||||||
model = "likes.Likes"
|
|
||||||
strategy = factory.CREATE_STRATEGY
|
|
||||||
|
|
||||||
content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
|
|
||||||
object_id = factory.Sequence(lambda n: n)
|
|
||||||
|
|
||||||
|
|
||||||
class VoteFactory(Factory):
|
class VoteFactory(Factory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = "votes.Vote"
|
model = "votes.Vote"
|
||||||
|
|
|
@ -77,10 +77,6 @@ def data():
|
||||||
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
|
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms)
|
||||||
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
|
f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner)
|
||||||
|
|
||||||
f.LikesFactory(content_type=project_ct, object_id=m.public_project.pk, count=2)
|
|
||||||
f.LikesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2)
|
|
||||||
f.LikesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2)
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -76,26 +76,10 @@ def test_get_project_fan(client):
|
||||||
assert response.data['id'] == like.user.id
|
assert response.data['id'] == like.user.id
|
||||||
|
|
||||||
|
|
||||||
def test_get_project_total_fans(client):
|
|
||||||
user = f.UserFactory.create()
|
|
||||||
project = f.create_project(owner=user)
|
|
||||||
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
|
||||||
url = reverse("projects-detail", args=(project.id,))
|
|
||||||
|
|
||||||
f.LikesFactory.create(content_object=project, count=5)
|
|
||||||
|
|
||||||
client.login(user)
|
|
||||||
response = client.get(url)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data['total_fans'] == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_project_is_fan(client):
|
def test_get_project_is_fan(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
project = f.create_project(owner=user)
|
project = f.create_project(owner=user)
|
||||||
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
f.MembershipFactory.create(project=project, user=user, is_owner=True)
|
||||||
f.LikesFactory.create(content_object=project)
|
|
||||||
url_detail = reverse("projects-detail", args=(project.id,))
|
url_detail = reverse("projects-detail", args=(project.id,))
|
||||||
url_like = reverse("projects-like", args=(project.id,))
|
url_like = reverse("projects-like", args=(project.id,))
|
||||||
url_unlike = reverse("projects-unlike", args=(project.id,))
|
url_unlike = reverse("projects-unlike", args=(project.id,))
|
||||||
|
|
|
@ -53,24 +53,6 @@ def mail():
|
||||||
return mail
|
return mail
|
||||||
|
|
||||||
|
|
||||||
def test_attach_notify_level_to_project_queryset():
|
|
||||||
project1 = f.ProjectFactory.create()
|
|
||||||
f.ProjectFactory.create()
|
|
||||||
|
|
||||||
qs = project1.__class__.objects.order_by("id")
|
|
||||||
qs = utils.attach_notify_level_to_project_queryset(qs, project1.owner)
|
|
||||||
|
|
||||||
assert len(qs) == 2
|
|
||||||
assert qs[0].notify_level == NotifyLevel.involved
|
|
||||||
assert qs[1].notify_level == NotifyLevel.involved
|
|
||||||
|
|
||||||
services.create_notify_policy(project1, project1.owner, NotifyLevel.all)
|
|
||||||
qs = project1.__class__.objects.order_by("id")
|
|
||||||
qs = utils.attach_notify_level_to_project_queryset(qs, project1.owner)
|
|
||||||
assert qs[0].notify_level == NotifyLevel.all
|
|
||||||
assert qs[1].notify_level == NotifyLevel.involved
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_retrieve_notify_policy():
|
def test_create_retrieve_notify_policy():
|
||||||
project = f.ProjectFactory.create()
|
project = f.ProjectFactory.create()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2015 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
from taiga.projects.history.choices import HistoryType
|
||||||
|
from taiga.projects.models import Project
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
def test_project_totals_updated_on_activity(client):
|
||||||
|
project = f.create_project()
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
assert project.total_activity == 0
|
||||||
|
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
us = f.UserStoryFactory.create(project=project, owner=project.owner)
|
||||||
|
f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": project.owner.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[],
|
||||||
|
created_at=now - datetime.timedelta(days=3)
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(id=project.id)
|
||||||
|
assert project.total_activity == 1
|
||||||
|
assert project.total_activity_last_week == 1
|
||||||
|
assert project.total_activity_last_month == 1
|
||||||
|
assert project.total_activity_last_year == 1
|
||||||
|
assert project.totals_updated_datetime > totals_updated_datetime
|
||||||
|
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": project.owner.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[],
|
||||||
|
created_at=now - datetime.timedelta(days=13)
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(id=project.id)
|
||||||
|
assert project.total_activity == 2
|
||||||
|
assert project.total_activity_last_week == 1
|
||||||
|
assert project.total_activity_last_month == 2
|
||||||
|
assert project.total_activity_last_year == 2
|
||||||
|
assert project.totals_updated_datetime > totals_updated_datetime
|
||||||
|
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": project.owner.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[],
|
||||||
|
created_at=now - datetime.timedelta(days=33)
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(id=project.id)
|
||||||
|
assert project.total_activity == 3
|
||||||
|
assert project.total_activity_last_week == 1
|
||||||
|
assert project.total_activity_last_month == 2
|
||||||
|
assert project.total_activity_last_year == 3
|
||||||
|
assert project.totals_updated_datetime > totals_updated_datetime
|
||||||
|
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": project.owner.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[],
|
||||||
|
created_at=now - datetime.timedelta(days=380)
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(id=project.id)
|
||||||
|
assert project.total_activity == 4
|
||||||
|
assert project.total_activity_last_week == 1
|
||||||
|
assert project.total_activity_last_month == 2
|
||||||
|
assert project.total_activity_last_year == 3
|
||||||
|
assert project.totals_updated_datetime > totals_updated_datetime
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_totals_updated_on_like(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner, is_owner=True)
|
||||||
|
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
assert project.total_activity == 0
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
totals_updated_datetime = project.totals_updated_datetime
|
||||||
|
us = f.UserStoryFactory.create(project=project, owner=project.owner)
|
||||||
|
|
||||||
|
l = f.LikeFactory.create(content_object=project)
|
||||||
|
l.created_date=now-datetime.timedelta(days=13)
|
||||||
|
l.save()
|
||||||
|
|
||||||
|
l = f.LikeFactory.create(content_object=project)
|
||||||
|
l.created_date=now-datetime.timedelta(days=33)
|
||||||
|
l.save()
|
||||||
|
|
||||||
|
l = f.LikeFactory.create(content_object=project)
|
||||||
|
l.created_date=now-datetime.timedelta(days=633)
|
||||||
|
l.save()
|
||||||
|
|
||||||
|
project.refresh_totals()
|
||||||
|
project = Project.objects.get(id=project.id)
|
||||||
|
|
||||||
|
assert project.total_fans == 3
|
||||||
|
assert project.total_fans_last_week == 0
|
||||||
|
assert project.total_fans_last_month == 1
|
||||||
|
assert project.total_fans_last_year == 2
|
||||||
|
assert project.totals_updated_datetime > totals_updated_datetime
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
url_like = reverse("projects-like", args=(project.id,))
|
||||||
|
response = client.post(url_like)
|
||||||
|
print(response.data)
|
||||||
|
|
||||||
|
project = Project.objects.get(id=project.id)
|
||||||
|
assert project.total_fans == 4
|
||||||
|
assert project.total_fans_last_week == 1
|
||||||
|
assert project.total_fans_last_month == 2
|
||||||
|
assert project.total_fans_last_year == 3
|
||||||
|
assert project.totals_updated_datetime > totals_updated_datetime
|
|
@ -388,7 +388,6 @@ def test_get_liked_list():
|
||||||
membership = f.MembershipFactory(project=project, role=role, user=fan_user)
|
membership = f.MembershipFactory(project=project, role=role, user=fan_user)
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
f.LikesFactory(content_type=content_type, object_id=project.id, count=1)
|
|
||||||
|
|
||||||
assert len(get_liked_list(fan_user, viewer_user)) == 1
|
assert len(get_liked_list(fan_user, viewer_user)) == 1
|
||||||
assert len(get_liked_list(fan_user, viewer_user, type="project")) == 1
|
assert len(get_liked_list(fan_user, viewer_user, type="project")) == 1
|
||||||
|
@ -495,7 +494,7 @@ def test_get_liked_list_valid_info():
|
||||||
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
f.LikesFactory(content_type=content_type, object_id=project.id, count=1)
|
project.refresh_totals()
|
||||||
|
|
||||||
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
|
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
|
||||||
project_like_info = LikedObjectSerializer(raw_project_like_info).data
|
project_like_info = LikedObjectSerializer(raw_project_like_info).data
|
||||||
|
@ -762,7 +761,6 @@ def test_get_liked_list_permissions():
|
||||||
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
|
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
f.LikesFactory(content_type=content_type, object_id=project.id, count=1)
|
|
||||||
|
|
||||||
#If the project is private a viewer user without any permission shouldn' see
|
#If the project is private a viewer user without any permission shouldn' see
|
||||||
# any vote
|
# any vote
|
||||||
|
|
Loading…
Reference in New Issue