Merge pull request #260 from taigaio/public-projects

Public projects
remotes/origin/enhancement/email-actions
David Barragán Merino 2015-03-09 12:49:02 +01:00
commit 92c1c39790
9 changed files with 122 additions and 26 deletions

View File

@ -212,7 +212,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
qs = qs.filter((Q(id__in=projects_list) | qs = qs.filter((Q(id__in=projects_list) |
Q(public_permissions__contains=["view_project"]))) Q(public_permissions__contains=["view_project"])))
else: else:
qs = qs.filter(public_permissions__contains=["view_project"]) qs = qs.filter(anon_permissions__contains=["view_project"])
return super().filter_queryset(request, qs.distinct(), view) return super().filter_queryset(request, qs.distinct(), view)
@ -229,22 +229,50 @@ class IsProjectMemberFilterBackend(FilterBackend):
return super().filter_queryset(request, queryset.distinct(), view) return super().filter_queryset(request, queryset.distinct(), view)
class MembersFilterBackend(filters.BaseFilterBackend): class MembersFilterBackend(PermissionBasedFilterBackend):
permission = "view_project"
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
project_id = request.QUERY_PARAMS.get('project', None) project_id = None
project = None
qs = queryset
if "project" in request.QUERY_PARAMS:
try:
project_id = int(request.QUERY_PARAMS["project"])
except:
logger.error("Filtering project diferent value than an integer: {}".format(request.QUERY_PARAMS["project"]))
raise exc.BadRequest("'project' must be an integer value.")
if project_id: if project_id:
project_model = apps.get_model('projects', 'Project') Project = apps.get_model('projects', 'Project')
project = get_object_or_404(project_model, pk=project_id) project = get_object_or_404(Project, pk=project_id)
if (request.user.is_authenticated() and
project.memberships.filter(user=request.user).exists()): if request.user.is_authenticated() and request.user.is_superuser:
return queryset.filter(memberships__project=project).distinct() qs = qs
elif request.user.is_authenticated():
Membership = apps.get_model('projects', 'Membership')
memberships_qs = Membership.objects.filter(user=request.user)
if project_id:
memberships_qs = memberships_qs.filter(project_id=project_id)
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
Q(is_owner=True))
projects_list = [membership.project_id for membership in memberships_qs]
if project and not "view_project" in project.public_permissions:
qs = qs.none()
qs = qs.filter(Q(memberships__project_id__in=projects_list) |
Q(memberships__project__public_permissions__contains=[self.permission])|
Q(id=request.user.id))
else: else:
raise exc.PermissionDenied(_("You don't have permisions to see this project users.")) if project and not "view_project" in project.anon_permissions:
qs = qs.none()
if request.user.is_superuser: qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission])
return queryset
return [] return qs.distinct()
class BaseIsProjectAdminFilterBackend(object): class BaseIsProjectAdminFilterBackend(object):

View File

@ -103,3 +103,17 @@ def get_user_project_permissions(user, project):
anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
return set(owner_permissions + members_permissions + public_permissions + anon_permissions) return set(owner_permissions + members_permissions + public_permissions + anon_permissions)
def set_base_permissions_for_project(project):
if project.is_private:
project.anon_permissions = []
project.public_permissions = []
else:
"""
If a project is public anonymous and registered users should have at least visualization permissions
"""
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
project.anon_permissions = list(set(project.anon_permissions + anon_permissions))
project.public_permissions = list(set(project.public_permissions + anon_permissions))

View File

@ -36,6 +36,7 @@ from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
from taiga.projects.userstories.models import UserStory from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task from taiga.projects.tasks.models import Task
from taiga.projects.issues.models import Issue from taiga.projects.issues.models import Issue
from taiga.permissions import service as permissions_service
from . import serializers from . import serializers
from . import models from . import models
@ -46,7 +47,6 @@ from .votes import serializers as votes_serializers
from .votes import services as votes_service from .votes import services as votes_service
from .votes.utils import attach_votescount_to_queryset from .votes.utils import attach_votescount_to_queryset
###################################################### ######################################################
## Project ## Project
###################################################### ######################################################
@ -168,6 +168,20 @@ class ProjectViewSet(ModelCrudViewSet):
services.remove_user_from_project(request.user, project) services.remove_user_from_project(request.user, project)
return response.Ok() return response.Ok()
def _set_base_permissions(self, obj):
update_permissions = False
if not obj.id:
if not obj.is_private:
# Creating a public project
update_permissions = True
else:
if self.get_object().is_private != obj.is_private:
# Changing project public state
update_permissions = True
if update_permissions:
permissions_service.set_base_permissions_for_project(obj)
def pre_save(self, obj): def pre_save(self, obj):
if not obj.id: if not obj.id:
obj.owner = self.request.user obj.owner = self.request.user
@ -176,6 +190,7 @@ class ProjectViewSet(ModelCrudViewSet):
if not obj.id: if not obj.id:
obj.template = self.request.QUERY_PARAMS.get('template', None) obj.template = self.request.QUERY_PARAMS.get('template', None)
self._set_base_permissions(obj)
super().pre_save(obj) super().pre_save(obj)
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
def update_existing_projects(apps, schema_editor):
Project = apps.get_model("projects", "Project")
Project.objects.filter(is_private=False).update(is_private=True)
class Migration(migrations.Migration):
dependencies = [
('projects', '0016_fix_json_field_not_null'),
]
operations = [
migrations.AlterField(
model_name='project',
name='is_private',
field=models.BooleanField(verbose_name='is private', default=True),
preserve_default=True,
),
migrations.RunPython(update_existing_projects),
]

View File

@ -160,7 +160,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
default=[], default=[],
verbose_name=_("user permissions"), verbose_name=_("user permissions"),
choices=USER_PERMISSIONS) choices=USER_PERMISSIONS)
is_private = models.BooleanField(default=False, null=False, blank=True, is_private = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("is private")) verbose_name=_("is private"))
tags_colors = TextArrayField(dimension=2, null=False, blank=True, verbose_name=_("tags colors"), default=[]) tags_colors = TextArrayField(dimension=2, null=False, blank=True, verbose_name=_("tags colors"), default=[])

View File

@ -52,12 +52,12 @@ class ProjectPermission(TaigaResourcePermission):
destroy_perms = IsProjectOwner() destroy_perms = IsProjectOwner()
modules_perms = IsProjectOwner() modules_perms = IsProjectOwner()
list_perms = AllowAny() list_perms = AllowAny()
stats_perms = AllowAny() stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project')
star_perms = IsAuthenticated() star_perms = IsAuthenticated()
unstar_perms = IsAuthenticated() unstar_perms = IsAuthenticated()
issues_stats_perms = AllowAny() issues_stats_perms = HasProjectPerm('view_project')
issues_filters_data_perms = AllowAny() issues_filters_data_perms = HasProjectPerm('view_project')
tags_perms = HasProjectPerm('view_project') tags_perms = HasProjectPerm('view_project')
tags_colors_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project')
fans_perms = HasProjectPerm('view_project') fans_perms = HasProjectPerm('view_project')

View File

@ -30,6 +30,7 @@ from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
from taiga.base.filters import PermissionBasedFilterBackend
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.base.filters import MembersFilterBackend from taiga.base.filters import MembersFilterBackend
from taiga.projects.votes import services as votes_service from taiga.projects.votes import services as votes_service
@ -46,14 +47,11 @@ from . import permissions
from .signals import user_cancel_account as user_cancel_account_signal from .signals import user_cancel_account as user_cancel_account_signal
######################################################
## User
######################################################
class UsersViewSet(ModelCrudViewSet): class UsersViewSet(ModelCrudViewSet):
permission_classes = (permissions.UserPermission,) permission_classes = (permissions.UserPermission,)
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
queryset = models.User.objects.all() queryset = models.User.objects.all()
filter_backends = (MembersFilterBackend,)
def create(self, *args, **kwargs): def create(self, *args, **kwargs):
raise exc.NotSupported() raise exc.NotSupported()

View File

@ -86,7 +86,7 @@ def test_user_delete(client, data):
] ]
results = helper_test_http_method(client, 'delete', url, None, users) results = helper_test_http_method(client, 'delete', url, None, users)
assert results == [401, 403, 204] assert results == [404, 404, 204]
def test_user_list(client, data): def test_user_list(client, data):
@ -101,14 +101,14 @@ def test_user_list(client, data):
response = client.get(url) response = client.get(url)
users_data = json.loads(response.content.decode('utf-8')) users_data = json.loads(response.content.decode('utf-8'))
assert len(users_data) == 0 assert len(users_data) == 1
assert response.status_code == 200 assert response.status_code == 200
client.login(data.other_user) client.login(data.other_user)
response = client.get(url) response = client.get(url)
users_data = json.loads(response.content.decode('utf-8')) users_data = json.loads(response.content.decode('utf-8'))
assert len(users_data) == 0 assert len(users_data) == 1
assert response.status_code == 200 assert response.status_code == 200
client.login(data.superuser) client.login(data.superuser)
@ -146,7 +146,7 @@ def test_user_patch(client, data):
patch_data = json.dumps({"full_name": "test"}) patch_data = json.dumps({"full_name": "test"})
results = helper_test_http_method(client, 'patch', url, patch_data, users) results = helper_test_http_method(client, 'patch', url, patch_data, users)
assert results == [401, 200, 403, 200] assert results == [404, 200, 404, 200]
def test_user_action_change_password(client, data): def test_user_action_change_password(client, data):

View File

@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.services import stats as stats_services from taiga.projects.services import stats as stats_services
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
from taiga.permissions.permissions import ANON_PERMISSIONS
from .. import factories as f from .. import factories as f
@ -235,3 +236,19 @@ def test_edit_membership_only_owner(client):
response = client.json.patch(url, json.dumps(data)) response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400 assert response.status_code == 400
assert response.data["is_owner"][0] == "At least one of the user must be an active admin" assert response.data["is_owner"][0] == "At least one of the user must be an active admin"
def test_anon_permissions_generation_when_making_project_public(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(is_private=True)
role = f.RoleFactory.create(project=project, permissions=["view_project", "modify_project"])
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True)
assert project.anon_permissions == []
client.login(user)
url = reverse("projects-detail", kwargs={"pk": project.pk})
data = {"is_private": False}
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
assert set(anon_permissions).issubset(set(response.data["anon_permissions"]))
assert set(anon_permissions).issubset(set(response.data["public_permissions"]))