Merge pull request #260 from taigaio/public-projects
Public projectsremotes/origin/enhancement/email-actions
commit
92c1c39790
|
@ -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()):
|
|
||||||
return queryset.filter(memberships__project=project).distinct()
|
|
||||||
else:
|
|
||||||
raise exc.PermissionDenied(_("You don't have permisions to see this project users."))
|
|
||||||
|
|
||||||
if request.user.is_superuser:
|
if request.user.is_authenticated() and request.user.is_superuser:
|
||||||
return queryset
|
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))
|
||||||
|
|
||||||
return []
|
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:
|
||||||
|
if project and not "view_project" in project.anon_permissions:
|
||||||
|
qs = qs.none()
|
||||||
|
|
||||||
|
qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission])
|
||||||
|
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
|
|
||||||
class BaseIsProjectAdminFilterBackend(object):
|
class BaseIsProjectAdminFilterBackend(object):
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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),
|
||||||
|
]
|
|
@ -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=[])
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"]))
|
||||||
|
|
Loading…
Reference in New Issue