Refactoring and improving watchers

remotes/origin/enhancement/email-actions
Alejandro Alonso 2015-08-18 09:33:23 +02:00 committed by David Barragán Merino
parent 44eee5212a
commit f3641f5cfb
56 changed files with 1304 additions and 117 deletions

View File

@ -13,6 +13,8 @@
- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved. - Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved.
- US, tasks and Issues can be upvoted or downvoted and the voters list can be obtained. - US, tasks and Issues can be upvoted or downvoted and the voters list can be obtained.
- Project can be starred or unstarred and the fans list can be obtained. - Project can be starred or unstarred and the fans list can be obtained.
- Now users can watch public issues, tasks and user stories.
- Add endpoints to show the watchers list for issues, tasks and user stories.
- i18n. - i18n.
- Add polish (pl) translation. - Add polish (pl) translation.
- Add portuguese (Brazil) (pt_BR) translation. - Add portuguese (Brazil) (pt_BR) translation.

View File

@ -110,3 +110,12 @@ class TagsColorsField(serializers.WritableField):
def from_native(self, data): def from_native(self, data):
return list(data.items()) return list(data.items())
class WatchersField(serializers.WritableField):
def to_native(self, obj):
return obj
def from_native(self, data):
return data

View File

@ -18,6 +18,7 @@ from functools import reduce
import logging import logging
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -451,6 +452,33 @@ class TagsFilter(FilterBackend):
return super().filter_queryset(request, queryset, view) return super().filter_queryset(request, queryset, view)
class WatchersFilter(FilterBackend):
filter_name = 'watchers'
def __init__(self, filter_name=None):
if filter_name:
self.filter_name = filter_name
def _get_watchers_queryparams(self, params):
watchers = params.get(self.filter_name, None)
if watchers:
return watchers.split(",")
return None
def filter_queryset(self, request, queryset, view):
query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS)
model = queryset.model
if query_watchers:
WatchedModel = apps.get_model("notifications", "Watched")
watched_type = ContentType.objects.get_for_model(queryset.model)
watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
queryset = queryset.filter(id__in=watched_ids)
return super().filter_queryset(request, queryset, view)
##################################################################### #####################################################################
# Text search filters # Text search filters
##################################################################### #####################################################################

View File

@ -19,6 +19,7 @@ import copy
import os import os
from collections import OrderedDict from collections import OrderedDict
from django.apps import apps
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -43,6 +44,7 @@ from taiga.projects.attachments import models as attachments_models
from taiga.timeline import models as timeline_models from taiga.timeline import models as timeline_models
from taiga.timeline import service as timeline_service from taiga.timeline import service as timeline_service
from taiga.users import models as users_models from taiga.users import models as users_models
from taiga.projects.notifications import services as notifications_services
from taiga.projects.votes import services as votes_service from taiga.projects.votes import services as votes_service
from taiga.projects.history import services as history_service from taiga.projects.history import services as history_service
@ -223,6 +225,48 @@ class HistoryDiffField(JsonField):
return data return data
class WatcheableObjectModelSerializer(serializers.ModelSerializer):
watchers = UserRelatedField(many=True, required=False)
def __init__(self, *args, **kwargs):
self._watchers_field = self.base_fields.pop("watchers", None)
super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs)
"""
watchers is not a field from the model so we need to do some magic to make it work like a normal field
It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
"""
def restore_object(self, attrs, instance=None):
watcher_field = self.fields.pop("watchers", None)
instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance)
self._watchers = self.init_data.get("watchers", [])
return instance
def save_watchers(self):
new_watcher_emails = set(self._watchers)
old_watcher_emails = set(notifications_services.get_watchers(self.object).values_list("email", flat=True))
adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
User = apps.get_model("users", "User")
adding_users = User.objects.filter(email__in=adding_watcher_emails)
removing_users = User.objects.filter(email__in=removing_watcher_emails)
for user in adding_users:
notifications_services.add_watcher(self.object, user)
for user in removing_users:
notifications_services.remove_watcher(self.object, user)
self.object.watchers = notifications_services.get_watchers(self.object)
def to_native(self, obj):
ret = super(WatcheableObjectModelSerializer, self).to_native(obj)
ret["watchers"] = [user.email for user in notifications_services.get_watchers(obj)]
return ret
class HistoryExportSerializer(serializers.ModelSerializer): class HistoryExportSerializer(serializers.ModelSerializer):
user = HistoryUserField() user = HistoryUserField()
diff = HistoryDiffField(required=False) diff = HistoryDiffField(required=False)
@ -447,9 +491,8 @@ class RolePointsExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'user_story') exclude = ('id', 'user_story')
class MilestoneExportSerializer(serializers.ModelSerializer): class MilestoneExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -475,13 +518,12 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer): AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name") status = ProjectRelatedField(slug_field="name")
user_story = ProjectRelatedField(slug_field="ref", required=False) user_story = ProjectRelatedField(slug_field="ref", required=False)
milestone = ProjectRelatedField(slug_field="name", required=False) milestone = ProjectRelatedField(slug_field="name", required=False)
assigned_to = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
class Meta: class Meta:
@ -493,13 +535,12 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer): AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
role_points = RolePointsExportSerializer(many=True, required=False) role_points = RolePointsExportSerializer(many=True, required=False)
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
assigned_to = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name") status = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False) milestone = ProjectRelatedField(slug_field="name", required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
@ -512,7 +553,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer): AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name") status = ProjectRelatedField(slug_field="name")
assigned_to = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False)
@ -520,7 +561,6 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
severity = ProjectRelatedField(slug_field="name") severity = ProjectRelatedField(slug_field="name")
type = ProjectRelatedField(slug_field="name") type = ProjectRelatedField(slug_field="name")
milestone = ProjectRelatedField(slug_field="name", required=False) milestone = ProjectRelatedField(slug_field="name", required=False)
watchers = UserRelatedField(many=True, required=False)
votes = serializers.SerializerMethodField("get_votes") votes = serializers.SerializerMethodField("get_votes")
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
@ -536,10 +576,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
serializers.ModelSerializer): WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
last_modifier = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
class Meta: class Meta:
@ -586,7 +625,7 @@ class TimelineExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project', 'namespace', 'object_id') exclude = ('id', 'project', 'namespace', 'object_id')
class ProjectExportSerializer(serializers.ModelSerializer): class ProjectExportSerializer(WatcheableObjectModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
default_points = serializers.SlugRelatedField(slug_field="name", required=False) default_points = serializers.SlugRelatedField(slug_field="name", required=False)
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)

View File

@ -71,6 +71,7 @@ def store_project(data):
if serialized.is_valid(): if serialized.is_valid():
serialized.object._importing = True serialized.object._importing = True
serialized.object.save() serialized.object.save()
serialized.save_watchers()
return serialized return serialized
add_errors("project", serialized.errors) add_errors("project", serialized.errors)
return None return None
@ -217,6 +218,7 @@ def store_task(project, data):
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
if serialized.object.ref: if serialized.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
@ -257,6 +259,7 @@ def store_milestone(project, milestone):
serialized.object.project = project serialized.object.project = project
serialized.object._importing = True serialized.object._importing = True
serialized.save() serialized.save()
serialized.save_watchers()
for task_without_us in milestone.get("tasks_without_us", []): for task_without_us in milestone.get("tasks_without_us", []):
task_without_us["user_story"] = None task_without_us["user_story"] = None
@ -320,6 +323,7 @@ def store_wiki_page(project, wiki_page):
serialized.object._importing = True serialized.object._importing = True
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
for attachment in wiki_page.get("attachments", []): for attachment in wiki_page.get("attachments", []):
store_attachment(project, serialized.object, attachment) store_attachment(project, serialized.object, attachment)
@ -382,6 +386,7 @@ def store_user_story(project, data):
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
if serialized.object.ref: if serialized.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
@ -442,6 +447,7 @@ def store_issue(project, data):
serialized.object._not_notify = True serialized.object._not_notify = True
serialized.save() serialized.save()
serialized.save_watchers()
if serialized.object.ref: if serialized.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)

View File

@ -15,11 +15,12 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.models import Membership, Project
from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from django.apps import apps
def _get_user_project_membership(user, project): def _get_user_project_membership(user, project):
Membership = apps.get_model("projects", "Membership")
if user.is_anonymous(): if user.is_anonymous():
return None return None
@ -30,7 +31,7 @@ def _get_user_project_membership(user, project):
def _get_object_project(obj): def _get_object_project(obj):
project = None project = None
Project = apps.get_model("projects", "Project")
if isinstance(obj, Project): if isinstance(obj, Project):
project = obj project = obj
elif obj and hasattr(obj, 'project'): elif obj and hasattr(obj, 'project'):

View File

@ -31,6 +31,7 @@ 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.mixins import WatchedResourceMixin
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
@ -50,7 +51,7 @@ from .votes.mixins.viewsets import StarredResourceMixin, VotersViewSetMixin
## Project ## Project
###################################################### ######################################################
class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, ModelCrudViewSet): class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
queryset = models.Project.objects.all() queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer
@ -62,7 +63,8 @@ class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, ModelCrudViewSe
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
return self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs)
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_update_order(self, request, **kwargs): def bulk_update_order(self, request, **kwargs):

View File

@ -27,12 +27,7 @@ def connect_memberships_signals():
sender=apps.get_model("projects", "Membership"), sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete') dispatch_uid='membership_pre_delete')
# On membership object is deleted, update watchers of all objects relation. # On membership object is deleted, update notify policies of all objects relation.
signals.post_delete.connect(handlers.update_watchers_on_membership_post_delete,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='update_watchers_on_membership_post_delete')
# On membership object is deleted, update watchers of all objects relation.
signals.post_save.connect(handlers.create_notify_policy, signals.post_save.connect(handlers.create_notify_policy,
sender=apps.get_model("projects", "Membership"), sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy') dispatch_uid='create-notify-policy')
@ -67,7 +62,6 @@ def connect_task_status_signals():
def disconnect_memberships_signals(): def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete') signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete')
signals.post_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='update_watchers_on_membership_post_delete')
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy') signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')

View File

@ -288,7 +288,7 @@ def userstory_freezer(us) -> dict:
"milestone": us.milestone_id, "milestone": us.milestone_id,
"client_requirement": us.client_requirement, "client_requirement": us.client_requirement,
"team_requirement": us.team_requirement, "team_requirement": us.team_requirement,
"watchers": [x.id for x in us.watchers.all()], "watchers": [x.id for x in us.get_watchers()],
"attachments": extract_attachments(us), "attachments": extract_attachments(us),
"tags": us.tags, "tags": us.tags,
"points": points, "points": points,
@ -315,7 +315,7 @@ def issue_freezer(issue) -> dict:
"description": issue.description, "description": issue.description,
"description_html": mdrender(issue.project, issue.description), "description_html": mdrender(issue.project, issue.description),
"assigned_to": issue.assigned_to_id, "assigned_to": issue.assigned_to_id,
"watchers": [x.pk for x in issue.watchers.all()], "watchers": [x.pk for x in issue.get_watchers()],
"attachments": extract_attachments(issue), "attachments": extract_attachments(issue),
"tags": issue.tags, "tags": issue.tags,
"is_blocked": issue.is_blocked, "is_blocked": issue.is_blocked,
@ -337,7 +337,7 @@ def task_freezer(task) -> dict:
"description": task.description, "description": task.description,
"description_html": mdrender(task.project, task.description), "description_html": mdrender(task.project, task.description),
"assigned_to": task.assigned_to_id, "assigned_to": task.assigned_to_id,
"watchers": [x.pk for x in task.watchers.all()], "watchers": [x.pk for x in task.get_watchers()],
"attachments": extract_attachments(task), "attachments": extract_attachments(task),
"taskboard_order": task.taskboard_order, "taskboard_order": task.taskboard_order,
"us_order": task.us_order, "us_order": task.us_order,
@ -359,7 +359,7 @@ def wikipage_freezer(wiki) -> dict:
"owner": wiki.owner_id, "owner": wiki.owner_id,
"content": wiki.content, "content": wiki.content,
"content_html": mdrender(wiki.project, wiki.content), "content_html": mdrender(wiki.project, wiki.content),
"watchers": [x.pk for x in wiki.watchers.all()], "watchers": [x.pk for x in wiki.get_watchers()],
"attachments": extract_attachments(wiki), "attachments": extract_attachments(wiki),
} }

View File

@ -53,6 +53,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
filters.SeveritiesFilter, filters.SeveritiesFilter,
filters.PrioritiesFilter, filters.PrioritiesFilter,
filters.TagsFilter, filters.TagsFilter,
filters.WatchersFilter,
filters.QFilter, filters.QFilter,
filters.OrderByFilterMixin) filters.OrderByFilterMixin)
retrieve_exclude_filters = (filters.OwnersFilter, retrieve_exclude_filters = (filters.OwnersFilter,
@ -61,11 +62,11 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
filters.IssueTypesFilter, filters.IssueTypesFilter,
filters.SeveritiesFilter, filters.SeveritiesFilter,
filters.PrioritiesFilter, filters.PrioritiesFilter,
filters.TagsFilter,) filters.TagsFilter,
filters.WatchersFilter,)
filter_fields = ("project", filter_fields = ("project",
"status__is_closed", "status__is_closed")
"watchers")
order_by_fields = ("type", order_by_fields = ("type",
"status", "status",
"severity", "severity",
@ -142,7 +143,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("attachments") qs = qs.prefetch_related("attachments")
return self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj): def pre_save(self, obj):
if not obj.id: if not obj.id:

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
update_all_contenttypes()
migrations.RunSQL(sql="""
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
SELECT issue_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
FROM issues_issue_watchers""".format(content_type_id=ContentType.objects.get(model='issue').id))
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_watched'),
('issues', '0005_auto_20150623_1923'),
]
operations = [
migrations.RunPython(create_notifications),
migrations.RemoveField(
model_name='issue',
name='watchers',
),
]

View File

@ -35,6 +35,8 @@ class IssuePermission(TaigaResourcePermission):
delete_comment_perms= HasProjectPerm('modify_issue') delete_comment_perms= HasProjectPerm('modify_issue')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues') upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues') downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
watch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
class HasIssueIdUrlParam(PermissionComponent): class HasIssueIdUrlParam(PermissionComponent):

View File

@ -23,6 +23,7 @@ from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicIssueStatusSerializer from taiga.projects.serializers import BasicIssueStatusSerializer
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import UserBasicInfoSerializer
@ -30,7 +31,7 @@ from taiga.users.serializers import UserBasicInfoSerializer
from . import models from . import models
class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer): class IssueSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False) tags = TagsField(required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed") is_closed = serializers.Field(source="is_closed")

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import djorm_pgarray.fields
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('projects', '0023_auto_20150721_1511'),
]
operations = [
migrations.AddField(
model_name='project',
name='watchers',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, related_name='projects_project+', null=True, verbose_name='watchers'),
preserve_default=True,
),
migrations.AlterField(
model_name='project',
name='public_permissions',
field=djorm_pgarray.fields.TextArrayField(default=[], dbtype='text', choices=[('view_project', 'View project'), ('star_project', 'Star project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('vote_us', 'Vote user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('vote_task', 'Vote task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('vote_issue', 'Vote issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], verbose_name='user permissions'),
preserve_default=True,
),
]

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
update_all_contenttypes()
migrations.RunSQL(sql="""
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
SELECT project_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
FROM projects_project_watchers""".format(content_type_id=ContentType.objects.get(model='project').id))
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_watched'),
('projects', '0024_auto_20150810_1247'),
]
operations = [
migrations.RunPython(create_notifications),
migrations.RemoveField(
model_name='project',
name='watchers',
),
]

View File

@ -44,9 +44,7 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
"user_stories__role_points__points", "user_stories__role_points__points",
"user_stories__role_points__role", "user_stories__role_points__role",
"user_stories__generated_from_issue", "user_stories__generated_from_issue",
"user_stories__project", "user_stories__project")
"watchers",
"user_stories__watchers")
qs = qs.select_related("project") qs = qs.select_related("project")
qs = qs.order_by("-estimated_start") qs = qs.order_by("-estimated_start")
return qs return qs

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
update_all_contenttypes()
migrations.RunSQL(sql="""
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
SELECT milestone_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
FROM milestones_milestone_watchers""".format(content_type_id=ContentType.objects.get(model='milestone').id)),
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_watched'),
('milestones', '0001_initial'),
]
operations = [
migrations.RunPython(create_notifications),
migrations.RemoveField(
model_name='milestone',
name='watchers',
),
]

View File

@ -19,12 +19,14 @@ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from ..userstories.serializers import UserStorySerializer from ..userstories.serializers import UserStorySerializer
from . import models from . import models
class MilestoneSerializer(serializers.ModelSerializer): class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
user_stories = UserStorySerializer(many=True, required=False, read_only=True) user_stories = UserStorySerializer(many=True, required=False, read_only=True)
total_points = serializers.SerializerMethodField("get_total_points") total_points = serializers.SerializerMethodField("get_total_points")
closed_points = serializers.SerializerMethodField("get_closed_points") closed_points = serializers.SerializerMethodField("get_closed_points")

View File

@ -40,6 +40,8 @@ from taiga.base.utils.slug import slugify_uniquely_for_queryset
from . import choices from . import choices
from . notifications.mixins import WatchedModelMixin
class Membership(models.Model): class Membership(models.Model):
# This model stores all project memberships. Also # This model stores all project memberships. Also
@ -118,7 +120,7 @@ class ProjectDefaults(models.Model):
abstract = True abstract = True
class Project(ProjectDefaults, TaggedMixin, models.Model): class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model):
name = models.CharField(max_length=250, null=False, blank=False, name = models.CharField(max_length=250, null=False, blank=False,
verbose_name=_("name")) verbose_name=_("name"))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
def fill_watched_table(apps, schema_editor):
Watched = apps.get_model("notifications", "Watched")
print("test")
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0001_initial'),
('notifications', '0003_auto_20141029_1143'),
]
operations = [
migrations.CreateModel(
name='Watched',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('object_id', models.PositiveIntegerField()),
('created_date', models.DateTimeField(verbose_name='created date', auto_now_add=True)),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
('user', models.ForeignKey(related_name='watched', verbose_name='user', to=settings.AUTH_USER_MODEL)),
],
options={
},
bases=(models.Model,),
),
migrations.RunPython(fill_watched_table),
]

View File

@ -17,14 +17,22 @@
from functools import partial from functools import partial
from operator import is_not from operator import is_not
from django.conf import settings from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import serializers
from taiga.base.fields import WatchersField
from taiga.projects.notifications import services from taiga.projects.notifications import services
from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watched_to_queryset
from taiga.users.models import User
from . import models
class WatchedResourceMixin(object): class WatchedResourceMixin:
""" """
Rest Framework resource mixin for resources susceptible Rest Framework resource mixin for resources susceptible
to be notifiable about their changes. to be notifiable about their changes.
@ -36,6 +44,27 @@ class WatchedResourceMixin(object):
_not_notify = False _not_notify = False
def attach_watchers_attrs_to_queryset(self, queryset):
qs = attach_watchers_to_queryset(queryset)
if self.request.user.is_authenticated():
qs = attach_is_watched_to_queryset(self.request.user, qs)
return qs
@detail_route(methods=["POST"])
def watch(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "watch", obj)
services.add_watcher(obj, request.user)
return response.Ok()
@detail_route(methods=["POST"])
def unwatch(self, request, pk=None):
obj = self.get_object()
self.check_permissions(request, "unwatch", obj)
services.remove_watcher(obj, request.user)
return response.Ok()
def send_notifications(self, obj, history=None): def send_notifications(self, obj, history=None):
""" """
Shortcut method for resources with special save Shortcut method for resources with special save
@ -73,7 +102,7 @@ class WatchedResourceMixin(object):
super().pre_delete(obj) super().pre_delete(obj)
class WatchedModelMixin(models.Model): class WatchedModelMixin(object):
""" """
Generic model mixin that makes model compatible Generic model mixin that makes model compatible
with notification system. with notification system.
@ -82,11 +111,6 @@ class WatchedModelMixin(models.Model):
this mixin if you want send notifications about this mixin if you want send notifications about
your model class. your model class.
""" """
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name="%(app_label)s_%(class)s+",
verbose_name=_("watchers"))
class Meta:
abstract = True
def get_project(self) -> object: def get_project(self) -> object:
""" """
@ -97,6 +121,7 @@ class WatchedModelMixin(models.Model):
that should works in almost all cases. that should works in almost all cases.
""" """
return self.project return self.project
t
def get_watchers(self) -> frozenset: def get_watchers(self) -> frozenset:
""" """
@ -112,7 +137,13 @@ class WatchedModelMixin(models.Model):
very inefficient way for obtain watchers but at very inefficient way for obtain watchers but at
this momment is the simplest way. this momment is the simplest way.
""" """
return frozenset(self.watchers.all()) return frozenset(services.get_watchers(self))
def add_watcher(self, user):
services.add_watcher(self, user)
def remove_watcher(self, user):
services.remove_watcher(self, user)
def get_owner(self) -> object: def get_owner(self) -> object:
""" """
@ -140,3 +171,79 @@ class WatchedModelMixin(models.Model):
self.get_owner(),) self.get_owner(),)
is_not_none = partial(is_not, None) is_not_none = partial(is_not, None)
return frozenset(filter(is_not_none, participants)) return frozenset(filter(is_not_none, participants))
class WatchedResourceModelSerializer(serializers.ModelSerializer):
is_watched = serializers.SerializerMethodField("get_is_watched")
watchers = WatchersField(required=False)
def get_is_watched(self, obj):
# The "is_watched" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_watched", False) or False
def restore_object(self, attrs, instance=None):
#watchers is not a field from the model but can be attached in the get_queryset of the viewset.
#If that's the case we need to remove it before calling the super method
watcher_field = self.fields.pop("watchers", None)
instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
if instance is not None and self.validate_watchers(attrs, "watchers"):
new_watcher_ids = set(attrs.get("watchers", []))
old_watcher_ids = set(services.get_watchers(instance).values_list("id", flat=True))
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
User = apps.get_model("users", "User")
adding_users = User.objects.filter(id__in=adding_watcher_ids)
removing_users = User.objects.filter(id__in=removing_watcher_ids)
for user in adding_users:
services.add_watcher(instance, user)
for user in removing_users:
services.remove_watcher(instance, user)
instance.watchers = services.get_watchers(instance)
return instance
def to_native(self, obj):
#watchers is wasn't attached via the get_queryset of the viewset we need to manually add it
if not hasattr(obj, "watchers"):
obj.watchers = services.get_watchers(obj)
return super(WatchedResourceModelSerializer, self).to_native(obj)
class WatchersViewSetMixin:
# Is a ModelListViewSet with two required params: permission_classes and resource_model
serializer_class = WatcherSerializer
list_serializer_class = WatcherSerializer
permission_classes = None
resource_model = None
def retrieve(self, request, *args, **kwargs):
pk = kwargs.get("pk", None)
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
self.check_permissions(request, 'retrieve', resource)
try:
self.object = services.get_watchers(resource).get(pk=pk)
except ObjectDoesNotExist: # or User.DoesNotExist
return response.NotFound()
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
def list(self, request, *args, **kwargs):
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
self.check_permissions(request, 'list', resource)
return super().list(request, *args, **kwargs)
def get_queryset(self):
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
return services.get_watchers(resource)

View File

@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.contrib.contenttypes import generic
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -72,3 +74,18 @@ class HistoryChangeNotification(models.Model):
class Meta: class Meta:
unique_together = ("key", "owner", "project", "history_type") unique_together = ("key", "owner", "project", "history_type")
class Watched(models.Model):
content_type = models.ForeignKey("contenttypes.ContentType")
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False,
related_name="watched", verbose_name=_("user"))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_("created date"))
class Meta:
verbose_name = _("Watched")
verbose_name_plural = _("Watched")
unique_together = ("content_type", "object_id", "user")

View File

@ -17,10 +17,10 @@
from functools import partial from functools import partial
from django.apps import apps from django.apps import apps
from django.db import IntegrityError from django.db.transaction import atomic
from django.db import IntegrityError, transaction
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from django.db import transaction
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -36,7 +36,7 @@ from taiga.projects.history.services import (make_key_from_model_object,
from taiga.permissions.service import user_has_perm from taiga.permissions.service import user_has_perm
from taiga.users.models import User from taiga.users.models import User
from .models import HistoryChangeNotification from .models import HistoryChangeNotification, Watched
def notify_policy_exists(project, user) -> bool: def notify_policy_exists(project, user) -> bool:
@ -121,11 +121,11 @@ def analize_object_for_watchers(obj:object, history:object):
if data["mentions"]: if data["mentions"]:
for user in data["mentions"]: for user in data["mentions"]:
obj.watchers.add(user) obj.add_watcher(user)
# Adding the person who edited the object to the watchers # Adding the person who edited the object to the watchers
if history.comment and not history.owner.is_system: if history.comment and not history.owner.is_system:
obj.watchers.add(history.owner) obj.add_watcher(history.owner)
def _filter_by_permissions(obj, user): def _filter_by_permissions(obj, user):
UserStory = apps.get_model("userstories", "UserStory") UserStory = apps.get_model("userstories", "UserStory")
@ -282,3 +282,46 @@ def send_sync_notifications(notification_id):
def process_sync_notifications(): def process_sync_notifications():
for notification in HistoryChangeNotification.objects.all(): for notification in HistoryChangeNotification.objects.all():
send_sync_notifications(notification.pk) send_sync_notifications(notification.pk)
def get_watchers(obj):
User = apps.get_model("users", "User")
Watched = apps.get_model("notifications", "Watched")
content_type = ContentType.objects.get_for_model(obj)
watching_user_ids = Watched.objects.filter(content_type=content_type, object_id=obj.id).values_list("user__id", flat=True)
return User.objects.filter(id__in=watching_user_ids)
def add_watcher(obj, user):
"""Add a watcher to an object.
If the user is already watching the object nothing happends, so this function can be considered
idempotent.
:param obj: Any Django model instance.
:param user: User adding the watch. :class:`~taiga.users.models.User` instance.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
with atomic():
watched, created = Watched.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
if not created:
return
return watched
def remove_watcher(obj, user):
"""Remove an watching user from an object.
If the user has not watched the object nothing happens so this function can be considered
idempotent.
:param obj: Any Django model instance.
:param user: User removing the watch. :class:`~taiga.users.models.User` instance.
"""
obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj)
with atomic():
qs = Watched.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
if not qs.exists():
return
qs.delete()

View File

@ -0,0 +1,63 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014 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_watchers_to_queryset(queryset, as_field="watchers"):
"""Attach watching user ids to each object of the queryset.
:param queryset: A Django 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 array(SELECT user_id
FROM notifications_watched
WHERE notifications_watched.content_type_id = {type_id}
AND notifications_watched.object_id = {tbl}.id)""")
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
qs = queryset.extra(select={as_field: sql})
return qs
def attach_is_watched_to_queryset(user, queryset, as_field="is_watched"):
"""Attach is_watched boolean to each object of the queryset.
: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 notifications_watched
WHERE notifications_watched.content_type_id = {type_id}
AND notifications_watched.object_id = {tbl}.id
AND notifications_watched.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

@ -21,7 +21,7 @@ from taiga.base.api import serializers
class WatchersValidator: class WatchersValidator:
def validate_watchers(self, attrs, source): def validate_watchers(self, attrs, source):
users = attrs[source] users = attrs.get(source, [])
# Try obtain a valid project # Try obtain a valid project
if self.object is None and "project" in attrs: if self.object is None and "project" in attrs:
@ -39,7 +39,8 @@ class WatchersValidator:
# Check if incoming watchers are contained # Check if incoming watchers are contained
# in project members list # in project members list
result = set(users).difference(set(project.members.all())) member_ids = project.members.values_list("id", flat=True)
result = set(users).difference(member_ids)
if result: if result:
raise serializers.ValidationError(_("Watchers contains invalid users")) raise serializers.ValidationError(_("Watchers contains invalid users"))

View File

@ -62,6 +62,8 @@ class ProjectPermission(TaigaResourcePermission):
tags_colors_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project')
star_perms = IsAuthenticated() & HasProjectPerm('view_project') star_perms = IsAuthenticated() & HasProjectPerm('view_project')
unstar_perms = IsAuthenticated() & HasProjectPerm('view_project') unstar_perms = IsAuthenticated() & HasProjectPerm('view_project')
watch_perms = IsAuthenticated() & HasProjectPerm('view_project')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser() create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject() leave_perms = CanLeaveProject()

View File

@ -25,6 +25,8 @@ 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.validators import WatchersValidator
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
@ -40,6 +42,7 @@ from .validators import ProjectExistsValidator
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer from .custom_attributes.serializers import IssueCustomAttributeSerializer
from .notifications.mixins import WatchedResourceModelSerializer
from .votes.mixins.serializers import StarredResourceSerializerMixin from .votes.mixins.serializers import StarredResourceSerializerMixin
###################################################### ######################################################
@ -305,7 +308,7 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
## Projects ## Projects
###################################################### ######################################################
class ProjectSerializer(StarredResourceSerializerMixin, serializers.ModelSerializer): class ProjectSerializer(WatchersValidator, StarredResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
anon_permissions = PgArrayField(required=False) anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False)

View File

@ -45,24 +45,6 @@ def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points() instance.project.update_role_points()
def update_watchers_on_membership_post_delete(sender, instance, using, **kwargs):
models = [apps.get_model("userstories", "UserStory"),
apps.get_model("tasks", "Task"),
apps.get_model("issues", "Issue")]
# `user_id` is used beacuse in some momments
# instance.user can contain pointer to now
# removed object from a database.
for model in models:
#filter(project=instance.project)
filter = {
"user_id": instance.user_id,
"%s__project"%(model._meta.model_name): instance.project,
}
model.watchers.through.objects.filter(**filter).delete()
def create_notify_policy(sender, instance, using, **kwargs): def create_notify_policy(sender, instance, using, **kwargs):
if instance.user: if instance.user:
create_notify_policy_if_not_exists(instance.project, instance.user) create_notify_policy_if_not_exists(instance.project, instance.user)

View File

@ -40,9 +40,10 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
ModelCrudViewSet): ModelCrudViewSet):
queryset = models.Task.objects.all() queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,) permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend,) filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
retrieve_exclude_filters = (filters.WatchersFilter,)
filter_fields = ["user_story", "milestone", "project", "assigned_to", filter_fields = ["user_story", "milestone", "project", "assigned_to",
"status__is_closed", "watchers"] "status__is_closed"]
def get_serializer_class(self, *args, **kwargs): def get_serializer_class(self, *args, **kwargs):
if self.action in ["retrieve", "by_ref"]: if self.action in ["retrieve", "by_ref"]:
@ -86,7 +87,8 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
return self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj): def pre_save(self, obj):
if obj.user_story: if obj.user_story:

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
update_all_contenttypes()
migrations.RunSQL(sql="""
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
SELECT task_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
FROM tasks_task_watchers""".format(content_type_id=ContentType.objects.get(model='task').id)),
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_watched'),
('tasks', '0007_auto_20150629_1556'),
]
operations = [
migrations.RunPython(create_notifications),
migrations.RemoveField(
model_name='task',
name='watchers',
),
]

View File

@ -33,6 +33,8 @@ class TaskPermission(TaigaResourcePermission):
bulk_update_order_perms = HasProjectPerm('modify_task') bulk_update_order_perms = HasProjectPerm('modify_task')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks') upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks') downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
class TaskVotersPermission(TaigaResourcePermission): class TaskVotersPermission(TaigaResourcePermission):

View File

@ -27,6 +27,7 @@ from taiga.projects.milestones.validators import SprintExistsValidator
from taiga.projects.tasks.validators import TaskExistsValidator from taiga.projects.tasks.validators import TaskExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import UserBasicInfoSerializer
@ -34,7 +35,7 @@ from taiga.users.serializers import UserBasicInfoSerializer
from . import models from . import models
class TaskSerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer): class TaskSerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(required=False, default=[]) tags = TagsField(required=False, default=[])
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")

View File

@ -53,19 +53,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
filters.AssignedToFilter, filters.AssignedToFilter,
filters.StatusesFilter, filters.StatusesFilter,
filters.TagsFilter, filters.TagsFilter,
filters.WatchersFilter,
filters.QFilter, filters.QFilter,
filters.OrderByFilterMixin) filters.OrderByFilterMixin)
retrieve_exclude_filters = (filters.OwnersFilter, retrieve_exclude_filters = (filters.OwnersFilter,
filters.AssignedToFilter, filters.AssignedToFilter,
filters.StatusesFilter, filters.StatusesFilter,
filters.TagsFilter) filters.TagsFilter,
filters.WatchersFilter)
filter_fields = ["project", filter_fields = ["project",
"milestone", "milestone",
"milestone__isnull", "milestone__isnull",
"is_closed", "is_closed",
"status__is_archived", "status__is_archived",
"status__is_closed", "status__is_closed"]
"watchers"]
order_by_fields = ["backlog_order", order_by_fields = ["backlog_order",
"sprint_order", "sprint_order",
"kanban_order"] "kanban_order"]
@ -113,10 +114,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("role_points", qs = qs.prefetch_related("role_points",
"role_points__points", "role_points__points",
"role_points__role", "role_points__role")
"watchers")
qs = qs.select_related("milestone", "project") qs = qs.select_related("milestone", "project")
return self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs)
def pre_save(self, obj): def pre_save(self, obj):
# This is very ugly hack, but having # This is very ugly hack, but having

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
update_all_contenttypes()
migrations.RunSQL(sql="""
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
SELECT userstory_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
FROM userstories_userstory_watchers""".format(content_type_id=ContentType.objects.get(model='userstory').id)),
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_watched'),
('userstories', '0009_remove_userstory_is_archived'),
]
operations = [
migrations.RunPython(create_notifications),
migrations.RemoveField(
model_name='userstory',
name='watchers',
),
]

View File

@ -32,7 +32,8 @@ class UserStoryPermission(TaigaResourcePermission):
bulk_update_order_perms = HasProjectPerm('modify_us') bulk_update_order_perms = HasProjectPerm('modify_us')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_us') upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_us') downvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
watch_perms = IsAuthenticated() & HasProjectPerm('view_us')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_us')
class UserStoryVotersPermission(TaigaResourcePermission): class UserStoryVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser() enought_perms = IsProjectOwner() | IsSuperUser()

View File

@ -27,6 +27,7 @@ from taiga.projects.validators import UserStoryStatusExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer from taiga.projects.serializers import BasicUserStoryStatusSerializer
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VotedResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import UserBasicInfoSerializer
@ -44,7 +45,7 @@ class RolePointsField(serializers.WritableField):
return json.loads(obj) return json.loads(obj)
class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, serializers.ModelSerializer): class UserStorySerializer(WatchersValidator, VotedResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
points = RolePointsField(source="role_points", required=False) points = RolePointsField(source="role_points", required=False)

View File

@ -16,8 +16,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import TagsField
from taiga.users.models import User from taiga.users.models import User
from taiga.users.services import get_photo_or_gravatar_url
class VoterSerializer(serializers.ModelSerializer): class VoterSerializer(serializers.ModelSerializer):

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import apps
from django.db import models, migrations
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.management import update_all_contenttypes
def create_notifications(apps, schema_editor):
update_all_contenttypes()
migrations.RunSQL(sql="""
INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id)
SELECT wikipage_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id
FROM wiki_wikipage_watchers""".format(content_type_id=ContentType.objects.get(model='wikipage').id)),
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_watched'),
('wiki', '0001_initial'),
]
operations = [
migrations.RunPython(create_notifications),
migrations.RemoveField(
model_name='wikipage',
name='watchers',
),
]

View File

@ -15,6 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.projects.history import services as history_service
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.mdrender.service import render as mdrender
from . import models from . import models
@ -23,7 +27,7 @@ from taiga.projects.history import services as history_service
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
class WikiPageSerializer(serializers.ModelSerializer): class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
html = serializers.SerializerMethodField("get_html") html = serializers.SerializerMethodField("get_html")
editions = serializers.SerializerMethodField("get_editions") editions = serializers.SerializerMethodField("get_editions")
@ -39,6 +43,5 @@ class WikiPageSerializer(serializers.ModelSerializer):
class WikiLinkSerializer(serializers.ModelSerializer): class WikiLinkSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.WikiLink model = models.WikiLink

View File

@ -62,7 +62,7 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
## - Watchers ## - Watchers
watchers = getattr(obj, "watchers", None) watchers = getattr(obj, "watchers", None)
if watchers: if watchers:
related_people |= obj.watchers.all() related_people |= obj.get_watchers()
## - Exclude inactive and system users and remove duplicate ## - Exclude inactive and system users and remove duplicate
related_people = related_people.exclude(is_active=False) related_people = related_people.exclude(is_active=False)

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import djorm_pgarray.fields
class Migration(migrations.Migration):
dependencies = [
('users', '0011_user_theme'),
]
operations = [
migrations.AlterField(
model_name='role',
name='permissions',
field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('star_project', 'Star project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('vote_us', 'Vote user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('vote_task', 'Vote task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('vote_issue', 'Vote issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], verbose_name='permissions', default=[], dbtype='text'),
preserve_default=True,
),
]

View File

@ -23,8 +23,9 @@ from taiga.projects.userstories import models as us_models
from taiga.projects.tasks import models as task_models from taiga.projects.tasks import models as task_models
from taiga.projects.issues import models as issue_models from taiga.projects.issues import models as issue_models
from taiga.projects.milestones import models as milestone_models from taiga.projects.milestones import models as milestone_models
from taiga.projects.history import models as history_models
from taiga.projects.wiki import models as wiki_models from taiga.projects.wiki import models as wiki_models
from taiga.projects.history import models as history_models
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
from .models import Webhook, WebhookLog from .models import Webhook, WebhookLog
@ -103,7 +104,8 @@ class PointSerializer(serializers.Serializer):
return obj.value return obj.value
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
owner = UserSerializer() owner = UserSerializer()
@ -119,7 +121,8 @@ class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializ
return project.userstorycustomattributes.all() return project.userstorycustomattributes.all()
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
owner = UserSerializer() owner = UserSerializer()
assigned_to = UserSerializer() assigned_to = UserSerializer()
@ -132,7 +135,8 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.M
return project.taskcustomattributes.all() return project.taskcustomattributes.all()
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, WatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsField(default=[], required=False) tags = TagsField(default=[], required=False)
owner = UserSerializer() owner = UserSerializer()
assigned_to = UserSerializer() assigned_to = UserSerializer()

View File

@ -574,3 +574,45 @@ def test_issues_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
def test_issue_action_watch(client, data):
public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-watch', kwargs={"pk": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_issue_action_unwatch(client, data):
public_url = reverse('issues-unwatch', kwargs={"pk": data.public_issue.pk})
private_url1 = reverse('issues-unwatch', kwargs={"pk": data.private_issue1.pk})
private_url2 = reverse('issues-unwatch', kwargs={"pk": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]

View File

@ -416,3 +416,41 @@ def test_regenerate_issues_csv_uuid(client, data):
results = helper_test_http_method(client, 'post', private2_url, None, users) results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 403, 200] assert results == [404, 404, 403, 200]
def test_project_action_watch(client, data):
public_url = reverse('projects-watch', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-watch', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-watch', kwargs={"pk": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private1_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]
def test_project_action_unwatch(client, data):
public_url = reverse('projects-unwatch', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-unwatch', kwargs={"pk": data.private_project1.pk})
private2_url = reverse('projects-unwatch', kwargs={"pk": data.private_project2.pk})
users = [
None,
data.registered_user,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private1_url, None, users)
assert results == [401, 200, 200, 200]
results = helper_test_http_method(client, 'post', private2_url, None, users)
assert results == [404, 404, 200, 200]

View File

@ -529,3 +529,45 @@ def test_tasks_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
def test_task_action_watch(client, data):
public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-watch', kwargs={"pk": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_task_action_unwatch(client, data):
public_url = reverse('tasks-unwatch', kwargs={"pk": data.public_task.pk})
private_url1 = reverse('tasks-unwatch', kwargs={"pk": data.private_task1.pk})
private_url2 = reverse('tasks-unwatch', kwargs={"pk": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]

View File

@ -528,3 +528,45 @@ def test_user_stories_csv(client, data):
results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users)
assert results == [200, 200, 200, 200, 200] assert results == [200, 200, 200, 200, 200]
def test_user_story_action_watch(client, data):
public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-watch', kwargs={"pk": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]
def test_user_story_action_unwatch(client, data):
public_url = reverse('userstories-unwatch', kwargs={"pk": data.public_user_story.pk})
private_url1 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story1.pk})
private_url2 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'post', public_url, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url1, "", users)
assert results == [401, 200, 200, 200, 200]
results = helper_test_http_method(client, 'post', private_url2, "", users)
assert results == [404, 404, 404, 200, 200]

View File

@ -47,13 +47,15 @@ def test_invalid_project_import(client):
def test_valid_project_import_without_extra_data(client): def test_valid_project_import_without_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
client.login(user) client.login(user)
url = reverse("importer-list") url = reverse("importer-list")
data = { data = {
"name": "Imported project", "name": "Imported project",
"description": "Imported project", "description": "Imported project",
"roles": [{"name": "Role"}] "roles": [{"name": "Role"}],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
@ -66,6 +68,7 @@ def test_valid_project_import_without_extra_data(client):
] ]
assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children)) assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email]
def test_valid_project_import_with_not_existing_memberships(client): def test_valid_project_import_with_not_existing_memberships(client):
@ -383,6 +386,7 @@ def test_valid_issue_import_with_custom_attributes_values(client):
def test_valid_issue_import_with_extra_data(client): def test_valid_issue_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_type = f.IssueTypeFactory.create(project=project)
@ -403,7 +407,8 @@ def test_valid_issue_import_with_extra_data(client):
"name": "imported attachment", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
@ -413,6 +418,7 @@ def test_valid_issue_import_with_extra_data(client):
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["ref"] is not None assert response_data["ref"] is not None
assert response_data["finished_date"] == "2014-10-24T00:00:00+0000" assert response_data["finished_date"] == "2014-10-24T00:00:00+0000"
assert response_data["watchers"] == [user_watching.email]
def test_invalid_issue_import_with_extra_data(client): def test_invalid_issue_import_with_extra_data(client):
@ -535,6 +541,7 @@ def test_valid_us_import_without_extra_data(client):
def test_valid_us_import_with_extra_data(client): def test_valid_us_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.default_us_status = f.UserStoryStatusFactory.create(project=project)
@ -551,7 +558,8 @@ def test_valid_us_import_with_extra_data(client):
"name": "imported attachment", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
@ -560,6 +568,7 @@ def test_valid_us_import_with_extra_data(client):
assert len(response_data["attachments"]) == 1 assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["ref"] is not None assert response_data["ref"] is not None
assert response_data["watchers"] == [user_watching.email]
def test_invalid_us_import_with_extra_data(client): def test_invalid_us_import_with_extra_data(client):
@ -664,6 +673,7 @@ def test_valid_task_import_with_custom_attributes_values(client):
def test_valid_task_import_with_extra_data(client): def test_valid_task_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_task_status = f.TaskStatusFactory.create(project=project) project.default_task_status = f.TaskStatusFactory.create(project=project)
@ -680,7 +690,8 @@ def test_valid_task_import_with_extra_data(client):
"name": "imported attachment", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
@ -689,6 +700,7 @@ def test_valid_task_import_with_extra_data(client):
assert len(response_data["attachments"]) == 1 assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["ref"] is not None assert response_data["ref"] is not None
assert response_data["watchers"] == [user_watching.email]
def test_invalid_task_import_with_extra_data(client): def test_invalid_task_import_with_extra_data(client):
@ -787,6 +799,7 @@ def test_valid_wiki_page_import_without_extra_data(client):
def test_valid_wiki_page_import_with_extra_data(client): def test_valid_wiki_page_import_with_extra_data(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user) client.login(user)
@ -801,7 +814,8 @@ def test_valid_wiki_page_import_with_extra_data(client):
"name": "imported attachment", "name": "imported attachment",
"data": base64.b64encode(b"TEST").decode("utf-8") "data": base64.b64encode(b"TEST").decode("utf-8")
} }
}] }],
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
@ -809,6 +823,7 @@ def test_valid_wiki_page_import_with_extra_data(client):
response_data = response.data response_data = response.data
assert len(response_data["attachments"]) == 1 assert len(response_data["attachments"]) == 1
assert response_data["owner"] == user.email assert response_data["owner"] == user.email
assert response_data["watchers"] == [user_watching.email]
def test_invalid_wiki_page_import_with_extra_data(client): def test_invalid_wiki_page_import_with_extra_data(client):
@ -877,6 +892,7 @@ def test_invalid_milestone_import(client):
def test_valid_milestone_import(client): def test_valid_milestone_import(client):
user = f.UserFactory.create() user = f.UserFactory.create()
user_watching = f.UserFactory.create(email="testing@taiga.io")
project = f.ProjectFactory.create(owner=user) project = f.ProjectFactory.create(owner=user)
f.MembershipFactory(project=project, user=user, is_owner=True) f.MembershipFactory(project=project, user=user, is_owner=True)
client.login(user) client.login(user)
@ -886,11 +902,12 @@ def test_valid_milestone_import(client):
"name": "Imported milestone", "name": "Imported milestone",
"estimated_start": "2014-10-10", "estimated_start": "2014-10-10",
"estimated_finish": "2014-10-20", "estimated_finish": "2014-10-20",
"watchers": ["testing@taiga.io"]
} }
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 201 assert response.status_code == 201
response.data assert response.data["watchers"] == [user_watching.email]

View File

@ -97,7 +97,7 @@ def test_analize_object_for_watchers():
history.comment = "" history.comment = ""
services.analize_object_for_watchers(issue, history) services.analize_object_for_watchers(issue, history)
assert issue.watchers.add.call_count == 2 assert issue.add_watcher.call_count == 2
def test_analize_object_for_watchers_adding_owner_non_empty_comment(): def test_analize_object_for_watchers_adding_owner_non_empty_comment():
@ -112,7 +112,7 @@ def test_analize_object_for_watchers_adding_owner_non_empty_comment():
history.owner = user1 history.owner = user1
services.analize_object_for_watchers(issue, history) services.analize_object_for_watchers(issue, history)
assert issue.watchers.add.call_count == 1 assert issue.add_watcher.call_count == 1
def test_analize_object_for_watchers_no_adding_owner_empty_comment(): def test_analize_object_for_watchers_no_adding_owner_empty_comment():
@ -127,7 +127,7 @@ def test_analize_object_for_watchers_no_adding_owner_empty_comment():
history.owner = user1 history.owner = user1
services.analize_object_for_watchers(issue, history) services.analize_object_for_watchers(issue, history)
assert issue.watchers.add.call_count == 0 assert issue.add_watcher.call_count == 0
def test_users_to_notify(): def test_users_to_notify():
@ -180,7 +180,7 @@ def test_users_to_notify():
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with watchers # Test with watchers
issue.watchers.add(member3.user) issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 3 assert len(users) == 3
assert users == {member1.user, member3.user, issue.get_owner()} assert users == {member1.user, member3.user, issue.get_owner()}
@ -189,24 +189,24 @@ def test_users_to_notify():
policy2.notify_level = NotifyLevel.ignore policy2.notify_level = NotifyLevel.ignore
policy2.save() policy2.save()
issue.watchers.add(member3.user) issue.add_watcher(member3.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with watchers without permissions # Test with watchers without permissions
issue.watchers.add(member5.user) issue.add_watcher(member5.user)
users = services.get_users_to_notify(issue) users = services.get_users_to_notify(issue)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with inactive user # Test with inactive user
issue.watchers.add(inactive_member1.user) issue.add_watcher(inactive_member1.user)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
# Test with system user # Test with system user
issue.watchers.add(system_member1.user) issue.add_watcher(system_member1.user)
assert len(users) == 2 assert len(users) == 2
assert users == {member1.user, issue.get_owner()} assert users == {member1.user, issue.get_owner()}
@ -344,7 +344,7 @@ def test_watchers_assignation_for_issue(client):
issue = f.create_issue(project=project1, owner=user1) issue = f.create_issue(project=project1, owner=user1)
data = {"version": issue.version, data = {"version": issue.version,
"watchers": [user1.pk]} "watchersa": [user1.pk]}
url = reverse("issues-detail", args=[issue.pk]) url = reverse("issues-detail", args=[issue.pk])
response = client.json.patch(url, json.dumps(data)) response = client.json.patch(url, json.dumps(data))

View File

@ -265,7 +265,7 @@ def test_leave_project_respect_watching_items(client):
url = reverse("projects-leave", args=(project.id,)) url = reverse("projects-leave", args=(project.id,))
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
assert list(issue.watchers.all()) == [user] assert issue.watchers == [user]
def test_delete_membership_only_owner(client): def test_delete_membership_only_owner(client):

View File

@ -384,16 +384,6 @@ def test_assigned_to_user_story_timeline():
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline" assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_watchers_to_user_story_timeline():
membership = factories.MembershipFactory.create()
user_story = factories.UserStoryFactory.create(subject="test us timeline", project=membership.project)
user_story.watchers.add(membership.user)
history_services.take_snapshot(user_story, user=user_story.owner)
user_timeline = service.get_profile_timeline(membership.user)
assert user_timeline[0].event_type == "userstories.userstory.create"
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_user_data_for_non_system_users(): def test_user_data_for_non_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner) history_services.take_snapshot(user_story, user=user_story.owner)

View File

@ -0,0 +1,47 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 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
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_watch_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
url = reverse("issues-watch", args=(issue.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unwatch_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
f.MembershipFactory.create(project=issue.project, user=user, is_owner=True)
url = reverse("issues-watch", args=(issue.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 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 json
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_watch_milestone(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
url = reverse("milestones-watch", args=(milestone.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unwatch_milestone(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
url = reverse("milestones-watch", args=(milestone.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_milestone_watchers(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
f.WatchedFactory.create(content_object=milestone, user=user)
url = reverse("milestone-watchers-list", args=(milestone.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == user.id
def test_get_milestone_watcher(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
watch = f.WatchedFactory.create(content_object=milestone, user=user)
url = reverse("milestone-watchers-detail", args=(milestone.id, watch.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == watch.user.id
def test_get_milestone_watchers(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
url = reverse("milestones-detail", args=(milestone.id,))
f.WatchedFactory.create(content_object=milestone, user=user)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['watchers'] == [user.id]
def test_get_milestone_is_watched(client):
user = f.UserFactory.create()
milestone = f.MilestoneFactory(owner=user)
f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True)
url_detail = reverse("milestones-detail", args=(milestone.id,))
url_watch = reverse("milestones-watch", args=(milestone.id,))
url_unwatch = reverse("milestones-unwatch", args=(milestone.id,))
client.login(user)
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['watchers'] == []
assert response.data['is_watched'] == False
response = client.post(url_watch)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['watchers'] == [user.id]
assert response.data['is_watched'] == True
response = client.post(url_unwatch)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['watchers'] == []
assert response.data['is_watched'] == False

View File

@ -0,0 +1,47 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 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
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_watch_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
url = reverse("projects-watch", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unwacth_project(client):
user = f.UserFactory.create()
project = f.create_project(owner=user)
f.MembershipFactory.create(project=project, user=user, is_owner=True)
url = reverse("projects-unwatch", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200

View File

@ -0,0 +1,47 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 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
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_watch_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-watch", args=(task.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unwatch_task(client):
user = f.UserFactory.create()
task = f.create_task(owner=user)
f.MembershipFactory.create(project=task.project, user=user, is_owner=True)
url = reverse("tasks-watch", args=(task.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200

View File

@ -0,0 +1,47 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 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
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_watch_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-watch", args=(user_story.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unwatch_user_story(client):
user = f.UserFactory.create()
user_story = f.create_userstory(owner=user)
f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True)
url = reverse("userstories-unwatch", args=(user_story.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200

View File

@ -0,0 +1,123 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# Copyright (C) 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 json
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_watch_wikipage(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
url = reverse("wiki-watch", args=(wikipage.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_unwatch_wikipage(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
url = reverse("wiki-watch", args=(wikipage.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_wikipage_watchers(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
f.WatchedFactory.create(content_object=wikipage, user=user)
url = reverse("wiki-watchers-list", args=(wikipage.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == user.id
def test_get_wikipage_watcher(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
watch = f.WatchedFactory.create(content_object=wikipage, user=user)
url = reverse("wiki-watchers-detail", args=(wikipage.id, watch.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == watch.user.id
def test_get_wikipage_watchers(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
url = reverse("wiki-detail", args=(wikipage.id,))
f.WatchedFactory.create(content_object=wikipage, user=user)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['watchers'] == [user.id]
def test_get_wikipage_is_watched(client):
user = f.UserFactory.create()
wikipage = f.WikiPageFactory(owner=user)
f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True)
url_detail = reverse("wiki-detail", args=(wikipage.id,))
url_watch = reverse("wiki-watch", args=(wikipage.id,))
url_unwatch = reverse("wiki-unwatch", args=(wikipage.id,))
client.login(user)
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['watchers'] == []
assert response.data['is_watched'] == False
response = client.post(url_watch)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['watchers'] == [user.id]
assert response.data['is_watched'] == True
response = client.post(url_unwatch)
assert response.status_code == 200
response = client.get(url_detail)
assert response.status_code == 200
assert response.data['watchers'] == []
assert response.data['is_watched'] == False