Task #3517 #3516: Order by fans and activity (last week/moth/year/all time)

remotes/origin/logger
Alejandro Alonso 2015-12-01 11:00:51 +01:00 committed by David Barragán Merino
parent 8aebfa4bae
commit 50e00b6d45
24 changed files with 538 additions and 308 deletions

View File

@ -33,3 +33,4 @@ django-transactional-cleanup==0.1.15
lxml==3.5.0
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
pyjwkest==1.0.9
python-dateutil==2.4.2

View File

@ -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]
class CreateModelMixin(object):
class CreateModelMixin:
"""
Create a model instance.
"""
@ -107,7 +107,7 @@ class CreateModelMixin(object):
return {}
class ListModelMixin(object):
class ListModelMixin:
"""
List a queryset.
"""
@ -137,7 +137,7 @@ class ListModelMixin(object):
return response.Ok(serializer.data)
class RetrieveModelMixin(object):
class RetrieveModelMixin:
"""
Retrieve a model instance.
"""
@ -153,7 +153,7 @@ class RetrieveModelMixin(object):
return response.Ok(serializer.data)
class UpdateModelMixin(object):
class UpdateModelMixin:
"""
Update a model instance.
"""
@ -220,7 +220,7 @@ class UpdateModelMixin(object):
obj.full_clean(exclude)
class DestroyModelMixin(object):
class DestroyModelMixin:
"""
Destroy a model instance.
"""

View File

@ -183,4 +183,5 @@ def dict_to_project(data, owner=None):
if service.get_errors(clear=False):
raise TaigaImportError(_("error importing timelines"))
proj.refresh_totals()
return proj

View File

@ -17,9 +17,13 @@
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.utils.translation import ugettext as _
from django.utils import timezone
from taiga.base import filters
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.projects.history.mixins import HistoryResourceMixin
from taiga.projects.notifications.models import NotifyPolicy
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
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.on_destroy import MoveOnDestroyMixin
@ -53,6 +54,7 @@ from . import models
from . import permissions
from . import services
from dateutil.relativedelta import relativedelta
######################################################
## Project
@ -64,6 +66,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
list_serializer_class = serializers.ProjectSerializer
permission_classes = (permissions.ProjectPermission, )
filter_backends = (filters.CanViewProjectObjFilterBackend,)
filter_fields = (('member', 'members'),
'is_looking_for_people',
'is_featured',
@ -71,34 +74,68 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
'is_kanban_activated')
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):
qs = super().get_queryset()
qs = self.attach_likes_attrs_to_queryset(qs)
qs = attach_project_total_watchers_attrs_to_queryset(qs)
if self.request.user.is_authenticated():
qs = attach_project_is_watcher_to_queryset(qs, self.request.user)
qs = attach_notify_level_to_project_queryset(qs, self.request.user)
# Prefetch doesn't work correctly if then if the field is filtered later (it generates more queries)
# so we add some custom prefetching
qs = qs.prefetch_related("members")
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
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.action == "list":
return self.list_serializer_class
elif self.action == "create":
return self.serializer_class
serializer_class = self.list_serializer_class
elif self.action != "create":
if self.action == "by_slug":
slug = self.request.QUERY_PARAMS.get("slug", None)
project = get_object_or_404(models.Project, slug=slug)
else:
project = self.get_object()
if self.action == "by_slug":
slug = self.request.QUERY_PARAMS.get("slug", None)
project = get_object_or_404(models.Project, slug=slug)
else:
project = self.get_object()
if permissions_service.is_project_owner(self.request.user, project):
serializer_class = self.admin_serializer_class
if permissions_service.is_project_owner(self.request.user, project):
return self.admin_serializer_class
return self.serializer_class
return serializer_class
@detail_route(methods=["POST"])
def watch(self, request, pk=None):

View File

@ -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',
),
]

View File

@ -20,12 +20,10 @@ from taiga.base.api import serializers
class FanResourceSerializerMixin(serializers.ModelSerializer):
is_fan = serializers.SerializerMethodField("get_is_fan")
total_fans = serializers.SerializerMethodField("get_total_fans")
def get_is_fan(self, obj):
# The "is_fan" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_fan", False) or False
if "request" in self.context:
user = self.context["request"].user
return user.is_authenticated() and user.is_fan(obj)
def get_total_fans(self, obj):
# The "total_fans" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "total_fans", 0) or 0
return False

View File

@ -24,23 +24,9 @@ from taiga.base.decorators import detail_route
from taiga.projects.likes import serializers
from taiga.projects.likes import services
from taiga.projects.likes.utils import attach_total_fans_to_queryset, attach_is_fan_to_queryset
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"])
def like(self, request, pk=None):
obj = self.get_object()

View File

@ -22,27 +22,6 @@ from django.db import models
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):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()

View File

@ -21,7 +21,7 @@ from django.db.transaction import atomic
from django.apps import apps
from django.contrib.auth import get_user_model
from .models import Likes, Like
from .models import Like
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)
with atomic():
like, created = Like.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
if not created:
return
if like.project is not None:
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
@ -60,11 +57,12 @@ def remove_like(obj, user):
if not qs.exists():
return
like = qs.first()
project = like.project
qs.delete()
likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
likes.count = F('count') - 1
likes.save()
if project is not None:
project.refresh_totals()
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)
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):
"""Get the objects liked by an user.

View File

@ -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

View File

@ -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),
]

View File

@ -84,7 +84,7 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
if self.request.user.is_authenticated():
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))

View File

@ -46,8 +46,12 @@ from taiga.projects.notifications.services import (
set_notify_policy_level_to_ignore,
create_notify_policy_if_not_exists)
from taiga.timeline.service import build_project_namespace
from . import choices
from dateutil.relativedelta import relativedelta
class Membership(models.Model):
# 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,
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
class Meta:
@ -233,6 +267,51 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
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):
return self.roles.all()

View File

@ -53,12 +53,12 @@ class WatchedResourceMixin:
_not_notify = False
def attach_watchers_attrs_to_queryset(self, queryset):
qs = attach_watchers_to_queryset(queryset)
qs = attach_total_watchers_to_queryset(qs)
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
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"])
def watch(self, request, pk=None):
@ -187,8 +187,11 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
total_watchers = serializers.SerializerMethodField("get_total_watchers")
def get_is_watcher(self, obj):
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_watcher", False) or False
if "request" in self.context:
user = self.context["request"].user
return user.is_authenticated() and user.is_watcher(obj)
return False
def get_total_watchers(self, obj):
# The "total_watchers" attribute is attached in the get_queryset of the viewset.

View File

@ -41,11 +41,11 @@ def attach_watchers_to_queryset(queryset, as_field="watchers"):
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.
:param user: A users.User object model
: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.
: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)
qs = queryset.extra(select={as_field: sql})
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})

View File

@ -26,6 +26,7 @@ from taiga.base.fields import PgArrayField
from taiga.base.fields import TagsField
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.serializers import UserSerializer
from taiga.users.serializers import UserBasicInfoSerializer
@ -318,6 +319,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
tags_colors = TagsColorsField(required=False)
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
notify_level = serializers.SerializerMethodField("get_notify_level")
total_watchers = serializers.SerializerMethodField("get_total_watchers")
class Meta:
model = models.Project
@ -336,10 +338,27 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return False
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()
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):

View File

@ -46,6 +46,8 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
namespace=build_project_namespace(project),
extra_data=extra_data)
project.refresh_totals()
if hasattr(obj, "get_related_people"):
related_people = obj.get_related_people()

View File

@ -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,
slug, projects_project.name, null::text AS subject,
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
FROM notifications_notifypolicy
INNER JOIN projects_project
@ -235,8 +235,6 @@ def _build_watched_sql_for_projects(for_user):
GROUP BY project_id
) type_watchers
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
notifications_notifypolicy.user_id = {for_user_id}
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,
slug, projects_project.name, null::text AS subject,
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
FROM likes_like
INNER JOIN projects_project
@ -265,8 +263,6 @@ def _build_liked_sql_for_projects(for_user):
GROUP BY project_id
) type_watchers
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
"""
sql = sql.format(

View File

@ -423,15 +423,6 @@ class LikeFactory(Factory):
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 Meta:
model = "votes.Vote"

View File

@ -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_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

View File

@ -76,26 +76,10 @@ def test_get_project_fan(client):
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):
user = f.UserFactory.create()
project = f.create_project(owner=user)
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_like = reverse("projects-like", args=(project.id,))
url_unlike = reverse("projects-unlike", args=(project.id,))

View File

@ -53,24 +53,6 @@ def 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():
project = f.ProjectFactory.create()

View File

@ -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

View File

@ -388,7 +388,6 @@ def test_get_liked_list():
membership = f.MembershipFactory(project=project, role=role, user=fan_user)
content_type = ContentType.objects.get_for_model(project)
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, type="project")) == 1
@ -495,8 +494,8 @@ def test_get_liked_list_valid_info():
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
content_type = ContentType.objects.get_for_model(project)
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]
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)
content_type = ContentType.objects.get_for_model(project)
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
# any vote