diff --git a/taiga/auth/api.py b/taiga/auth/api.py index e2393868..f2ed0161 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -20,13 +20,13 @@ from enum import Enum from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from rest_framework.response import Response -from rest_framework import status from rest_framework import serializers from taiga.base.api import viewsets from taiga.base.decorators import list_route from taiga.base import exceptions as exc +from taiga.base import response + from taiga.users.services import get_and_validate_user from .serializers import PublicRegisterSerializer @@ -108,7 +108,7 @@ class AuthViewSet(viewsets.ViewSet): raise exc.BadRequest(e.detail) data = make_auth_response_data(user) - return Response(data, status=status.HTTP_201_CREATED) + return response.Created(data) def _private_register(self, request): register_type = parse_register_type(request.DATA) @@ -121,7 +121,7 @@ class AuthViewSet(viewsets.ViewSet): user = private_register_for_new_user(**data) data = make_auth_response_data(user) - return Response(data, status=status.HTTP_201_CREATED) + return response.Created(data) @list_route(methods=["POST"]) def register(self, request, **kwargs): @@ -134,7 +134,6 @@ class AuthViewSet(viewsets.ViewSet): return self._private_register(request) raise exc.BadRequest(_("invalid register type")) - # Login view: /api/v1/auth def create(self, request, **kwargs): self.check_permissions(request, 'create', None) diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 19bb6ebe..238f229f 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -29,12 +29,10 @@ from django.db import transaction as tx from django.db import IntegrityError from django.utils.translation import ugettext as _ -from rest_framework.response import Response -from rest_framework import status - from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail from taiga.base import exceptions as exc +from taiga.base import response from taiga.users.serializers import UserSerializer from taiga.users.services import get_and_validate_user from taiga.base.utils.slug import slugify_uniquely @@ -203,7 +201,7 @@ def normal_login_func(request): user = get_and_validate_user(username=username, password=password) data = make_auth_response_data(user) - return Response(data, status=status.HTTP_200_OK) + return response.Ok(data) register_auth_plugin("normal", normal_login_func); diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index bca89e63..ed2b2f24 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -23,9 +23,7 @@ from django.core.exceptions import ValidationError from django.http import Http404 from django.db import transaction as tx -from rest_framework import status -from rest_framework.response import Response -from rest_framework.request import clone_request +from taiga.base import response from rest_framework.settings import api_settings from .utils import get_object_or_404 @@ -73,10 +71,9 @@ class CreateModelMixin(object): self.object = serializer.save(force_insert=True) self.post_save(self.object, created=True) headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, - headers=headers) + return response.Created(serializer.data, headers=headers) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return response.BadRequest(serializer.errors) def get_success_headers(self, data): try: @@ -114,7 +111,7 @@ class ListModelMixin(object): else: serializer = self.get_serializer(self.object_list, many=True) - return Response(serializer.data) + return response.Ok(serializer.data) class RetrieveModelMixin(object): @@ -130,7 +127,7 @@ class RetrieveModelMixin(object): raise Http404 serializer = self.get_serializer(self.object) - return Response(serializer.data) + return response.Ok(serializer.data) class UpdateModelMixin(object): @@ -149,7 +146,7 @@ class UpdateModelMixin(object): files=request.FILES, partial=partial) if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return response.BadRequest(serializer.errors) # Hooks try: @@ -158,16 +155,16 @@ class UpdateModelMixin(object): except ValidationError as err: # full_clean on model instance may be called in pre_save, # so we have to handle eventual errors. - return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) + return response.BadRequest(err.message_dict) if self.object is None: self.object = serializer.save(force_insert=True) self.post_save(self.object, created=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return response.Created(serializer.data) self.object = serializer.save(force_update=True) self.post_save(self.object, created=False) - return Response(serializer.data, status=status.HTTP_200_OK) + return response.Ok(serializer.data) def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True @@ -216,4 +213,4 @@ class DestroyModelMixin(object): self.pre_conditions_on_delete(obj) obj.delete() self.post_delete(obj) - return Response(status=status.HTTP_204_NO_CONTENT) + return response.NoContent() diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py index cde7113c..638245a0 100644 --- a/taiga/base/api/views.py +++ b/taiga/base/api/views.py @@ -27,10 +27,13 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions from rest_framework.compat import smart_text, HttpResponseBase, View from rest_framework.request import Request -from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.utils import formatting +from taiga.base.response import Response +from taiga.base.response import Ok +from taiga.base.response import NotFound +from taiga.base.response import Forbidden from taiga.base.utils.iterators import as_tuple from django.conf import settings @@ -89,12 +92,10 @@ def exception_handler(exc): headers=headers) elif isinstance(exc, Http404): - return Response({'detail': 'Not found'}, - status=status.HTTP_404_NOT_FOUND) + return NotFound({'detail': 'Not found'}) elif isinstance(exc, PermissionDenied): - return Response({'detail': 'Permission denied'}, - status=status.HTTP_403_FORBIDDEN) + return Forbidden({'detail': 'Permission denied'}) # Note: Unhandled exceptions will raise a 500 error. return None @@ -425,7 +426,7 @@ class APIView(View): We may as well implement this as Django will otherwise provide a less useful default implementation. """ - return Response(self.metadata(request), status=status.HTTP_200_OK) + return Ok(self.metadata(request)) def metadata(self, request): """ diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index 53a56345..3eb05f5f 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -14,17 +14,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - from rest_framework import exceptions from rest_framework import status -from rest_framework.response import Response from django.core.exceptions import PermissionDenied as DjangoPermissionDenied from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.http import Http404 -from .utils.json import to_json +from taiga.base import response +from taiga.base.utils.json import to_json class BaseException(exceptions.APIException): @@ -129,15 +128,13 @@ def exception_handler(exc): headers["X-Throttle-Wait-Seconds"] = "%d" % exc.wait detail = format_exception(exc) - return Response(detail, status=exc.status_code, headers=headers) + return response.Response(detail, status=exc.status_code, headers=headers) elif isinstance(exc, Http404): - return Response({'_error_message': str(exc)}, - status=status.HTTP_404_NOT_FOUND) + return response.NotFound({'_error_message': str(exc)}) elif isinstance(exc, DjangoPermissionDenied): - return Response({"_error_message": str(exc)}, - status=status.HTTP_403_FORBIDDEN) + return response.Forbidden({"_error_message": str(exc)}) # Note: Unhandled exceptions will raise a 500 error. return None diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 5c73ed95..1cc73a60 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -17,15 +17,13 @@ import operator from functools import reduce import logging +from django.apps import apps from django.db.models import Q -from django.db.models.sql.where import ExtraWhere, OR, AND from rest_framework import filters -from taiga.base import tags from taiga.base import exceptions as exc -from taiga.projects.models import Membership logger = logging.getLogger(__name__) @@ -101,7 +99,8 @@ class PermissionBasedFilterBackend(FilterBackend): try: project_id = int(request.QUERY_PARAMS["project"]) except: - logger.error("Filtering project diferent value than an integer: {}".format(request.QUERY_PARAMS["project"])) + logger.error("Filtering project diferent value than an integer: {}".format( + request.QUERY_PARAMS["project"])) raise exc.BadRequest("'project' must be an integer value.") qs = queryset @@ -109,7 +108,8 @@ class PermissionBasedFilterBackend(FilterBackend): if request.user.is_authenticated() and request.user.is_superuser: qs = qs elif request.user.is_authenticated(): - memberships_qs = Membership.objects.filter(user=request.user) + membership_model = apps.get_model('projects', 'Membership') + memberships_qs = membership_model.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]) | @@ -187,7 +187,8 @@ class CanViewProjectObjFilterBackend(FilterBackend): try: project_id = int(request.QUERY_PARAMS["project"]) except: - logger.error("Filtering project diferent value than an integer: {}".format(request.QUERY_PARAMS["project"])) + logger.error("Filtering project diferent value than an integer: {}".format( + request.QUERY_PARAMS["project"])) raise exc.BadRequest("'project' must be an integer value.") qs = queryset @@ -195,7 +196,8 @@ class CanViewProjectObjFilterBackend(FilterBackend): if request.user.is_authenticated() and request.user.is_superuser: qs = qs elif request.user.is_authenticated(): - memberships_qs = Membership.objects.filter(user=request.user) + membership_model = apps.get_model("projects", "Membership") + memberships_qs = membership_model.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=['view_project']) | @@ -203,7 +205,8 @@ class CanViewProjectObjFilterBackend(FilterBackend): projects_list = [membership.project_id for membership in memberships_qs] - qs = qs.filter(Q(id__in=projects_list) | Q(public_permissions__contains=["view_project"])) + qs = qs.filter((Q(id__in=projects_list) | + Q(public_permissions__contains=["view_project"]))) else: qs = qs.filter(public_permissions__contains=["view_project"]) @@ -221,6 +224,25 @@ class IsProjectMemberFilterBackend(FilterBackend): return super().filter_queryset(request, queryset.distinct(), view) + +class MembersFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + project_id = request.QUERY_PARAMS.get('project', None) + if project_id: + project_model = apps.get_model('projects', 'Project') + project = get_object_or_404(project_model, 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: + return queryset + + return [] + + class BaseIsProjectAdminFilterBackend(object): def get_project_ids(self, request, view): project_id = None @@ -233,7 +255,8 @@ class BaseIsProjectAdminFilterBackend(object): if not request.user.is_authenticated(): return [] - memberships_qs = Membership.objects.filter(user=request.user, is_owner=True) + membership_model = apps.get_model('projects', 'Membership') + memberships_qs = membership_model.objects.filter(user=request.user, is_owner=True) if project_id: memberships_qs = memberships_qs.filter(project_id=project_id) diff --git a/taiga/base/routers.py b/taiga/base/routers.py index 407b40a5..3a3a5b8a 100644 --- a/taiga/base/routers.py +++ b/taiga/base/routers.py @@ -23,7 +23,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import NoReverseMatch from rest_framework import views -from rest_framework.response import Response +from taiga.base import response from rest_framework.reverse import reverse from rest_framework.urlpatterns import format_suffix_patterns @@ -292,7 +292,7 @@ class DRFDefaultRouter(SimpleRouter): except NoReverseMatch: # Support resources that are prefixed by a parametrized url ret[key] = request.build_absolute_uri() + key - return Response(ret) + return response.Response(ret) return APIRoot.as_view() diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index a993b6af..edf8911d 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -18,10 +18,6 @@ import json import codecs import uuid -from rest_framework.response import Response -from rest_framework.decorators import throttle_classes -from rest_framework import status - from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.db.transaction import atomic @@ -30,10 +26,13 @@ from django.conf import settings from django.core.files.storage import default_storage from django.core.files.base import ContentFile -from taiga.base.api.mixins import CreateModelMixin -from taiga.base.api.viewsets import GenericViewSet +from rest_framework.decorators import throttle_classes + from taiga.base.decorators import detail_route, list_route from taiga.base import exceptions as exc +from taiga.base import response +from taiga.base.api.mixins import CreateModelMixin +from taiga.base.api.viewsets import GenericViewSet from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue from taiga.projects.serializers import ProjectSerializer @@ -65,8 +64,9 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet) if settings.CELERY_ENABLED: task = tasks.dump_project.delay(request.user, project) - tasks.delete_project_dump.apply_async((project.pk, project.slug), countdown=settings.EXPORTS_TTL) - return Response({"export_id": task.id}, status=status.HTTP_202_ACCEPTED) + tasks.delete_project_dump.apply_async((project.pk, project.slug), + countdown=settings.EXPORTS_TTL) + return response.Accepted({"export_id": task.id}) path = "exports/{}/{}-{}.json".format(project.pk, project.slug, uuid.uuid4().hex) content = ContentFile(ExportRenderer().render(service.project_to_dict(project), @@ -76,7 +76,7 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet) response_data = { "url": default_storage.url(path) } - return Response(response_data, status=status.HTTP_200_OK) + return response.Ok(response_data) class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixin, GenericViewSet): @@ -152,7 +152,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi response_data = project_serialized.data response_data['id'] = project_serialized.object.id headers = self.get_success_headers(response_data) - return Response(response_data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(response_data, headers=headers) @list_route(methods=["POST"]) @method_decorator(atomic) @@ -181,11 +181,11 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if settings.CELERY_ENABLED: task = tasks.load_project_dump.delay(request.user, dump) - return Response({"import_id": task.id}, status=status.HTTP_202_ACCEPTED) + return response.Accepted({"import_id": task.id}) project = dump_service.dict_to_project(dump, request.user.email) response_data = ProjectSerializer(project).data - return Response(response_data, status=status.HTTP_201_CREATED) + return response.Created(response_data) @detail_route(methods=['post']) @@ -204,7 +204,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) headers = self.get_success_headers(issue.data) - return Response(issue.data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(issue.data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -219,7 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) headers = self.get_success_headers(task.data) - return Response(task.data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(task.data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -234,7 +234,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) headers = self.get_success_headers(us.data) - return Response(us.data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(us.data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -249,7 +249,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) headers = self.get_success_headers(milestone.data) - return Response(milestone.data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(milestone.data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -264,7 +264,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) headers = self.get_success_headers(wiki_page.data) - return Response(wiki_page.data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(wiki_page.data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -279,4 +279,4 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) headers = self.get_success_headers(wiki_link.data) - return Response(wiki_link.data, status=status.HTTP_201_CREATED, headers=headers) + return response.Created(wiki_link.data, headers=headers) diff --git a/taiga/hooks/api.py b/taiga/hooks/api.py index 105d0189..a95d6fbe 100644 --- a/taiga/hooks/api.py +++ b/taiga/hooks/api.py @@ -14,11 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from rest_framework.response import Response from django.utils.translation import ugettext_lazy as _ -from taiga.base.api.viewsets import GenericViewSet from taiga.base import exceptions as exc +from taiga.base import response +from taiga.base.api.viewsets import GenericViewSet from taiga.base.utils import json from taiga.projects.models import Project @@ -75,4 +75,4 @@ class BaseWebhookApiViewSet(GenericViewSet): except ActionSyntaxException as e: raise exc.BadRequest(e) - return Response({}) + return response.NoContent() diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py index cb1c75e8..a73e9958 100644 --- a/taiga/hooks/bitbucket/api.py +++ b/taiga/hooks/bitbucket/api.py @@ -14,18 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from rest_framework.response import Response from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from taiga.base.api.viewsets import GenericViewSet from taiga.base import exceptions as exc -from taiga.base.utils import json from taiga.projects.models import Project from taiga.hooks.api import BaseWebhookApiViewSet from . import event_hooks -from ..exceptions import ActionSyntaxException from urllib.parse import parse_qs from ipware.ip import get_real_ip @@ -61,7 +57,9 @@ class BitBucketViewSet(BaseWebhookApiViewSet): if not project_secret: return False - valid_origin_ips = project.modules_config.config.get("bitbucket", {}).get("valid_origin_ips", settings.BITBUCKET_VALID_ORIGIN_IPS) + bitbucket_config = project.modules_config.config.get("bitbucket", {}) + valid_origin_ips = bitbucket_config.get("valid_origin_ips", + settings.BITBUCKET_VALID_ORIGIN_IPS) origin_ip = get_real_ip(request) if valid_origin_ips and (not origin_ip or not origin_ip in valid_origin_ips): return False diff --git a/taiga/hooks/github/api.py b/taiga/hooks/github/api.py index c0f32c16..767aefd4 100644 --- a/taiga/hooks/github/api.py +++ b/taiga/hooks/github/api.py @@ -14,13 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from rest_framework.response import Response -from django.utils.translation import ugettext_lazy as _ - -from taiga.base.api.viewsets import GenericViewSet -from taiga.base import exceptions as exc -from taiga.base.utils import json -from taiga.projects.models import Project from taiga.hooks.api import BaseWebhookApiViewSet from . import event_hooks @@ -51,7 +44,8 @@ class GitHubViewSet(BaseWebhookApiViewSet): if project.modules_config.config is None: return False - secret = bytes(project.modules_config.config.get("github", {}).get("secret", "").encode("utf-8")) + secret = project.modules_config.config.get("github", {}).get("secret", "") + secret = bytes(secret.encode("utf-8")) mac = hmac.new(secret, msg=request.body,digestmod=hashlib.sha1) return hmac.compare_digest(mac.hexdigest(), signature) diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py index a7596910..1f776b8e 100644 --- a/taiga/hooks/gitlab/api.py +++ b/taiga/hooks/gitlab/api.py @@ -14,20 +14,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from rest_framework.response import Response from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from taiga.base.api.viewsets import GenericViewSet -from taiga.base import exceptions as exc +from ipware.ip import get_real_ip + from taiga.base.utils import json + from taiga.projects.models import Project from taiga.hooks.api import BaseWebhookApiViewSet from . import event_hooks -from ipware.ip import get_real_ip - class GitLabViewSet(BaseWebhookApiViewSet): event_hook_classes = { @@ -51,7 +49,8 @@ class GitLabViewSet(BaseWebhookApiViewSet): if not project_secret: return False - valid_origin_ips = project.modules_config.config.get("gitlab", {}).get("valid_origin_ips", settings.GITLAB_VALID_ORIGIN_IPS) + gitlab_config = project.modules_config.config.get("gitlab", {}) + valid_origin_ips = gitlab_config.get("valid_origin_ips", settings.GITLAB_VALID_ORIGIN_IPS) origin_ip = get_real_ip(request) if valid_origin_ips and (not origin_ip or origin_ip not in valid_origin_ips): return False diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 406438a7..0f17c668 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -20,7 +20,8 @@ from django.db.models import signals from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ -from taiga.base import filters, response +from taiga.base import filters +from taiga.base import response from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.decorators import detail_route @@ -32,7 +33,6 @@ from taiga.base.utils.slug import slugify_uniquely from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin -from taiga.users.models import Role from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue @@ -74,7 +74,7 @@ class ProjectViewSet(ModelCrudViewSet): modules_config = services.get_modules_config(project) if request.method == "GET": - return response.Ok(data=modules_config.config) + return response.Ok(modules_config.config) else: modules_config.config.update(request.DATA) @@ -85,31 +85,31 @@ class ProjectViewSet(ModelCrudViewSet): def stats(self, request, pk=None): project = self.get_object() self.check_permissions(request, "stats", project) - return response.Ok(data=services.get_stats_for_project(project)) + return response.Ok(services.get_stats_for_project(project)) @detail_route(methods=["GET"]) def member_stats(self, request, pk=None): project = self.get_object() self.check_permissions(request, "member_stats", project) - return response.Ok(data=services.get_member_stats_for_project(project)) + return response.Ok(services.get_member_stats_for_project(project)) @detail_route(methods=["GET"]) def issues_stats(self, request, pk=None): project = self.get_object() self.check_permissions(request, "issues_stats", project) - return response.Ok(data=services.get_stats_for_project_issues(project)) + return response.Ok(services.get_stats_for_project_issues(project)) @detail_route(methods=["GET"]) def issue_filters_data(self, request, pk=None): project = self.get_object() self.check_permissions(request, "issues_filters_data", project) - return response.Ok(data=services.get_issues_filters_data(project)) + return response.Ok(services.get_issues_filters_data(project)) @detail_route(methods=["GET"]) def tags_colors(self, request, pk=None): project = self.get_object() self.check_permissions(request, "tags_colors", project) - return response.Ok(data=dict(project.tags_colors)) + return response.Ok(dict(project.tags_colors)) @detail_route(methods=["POST"]) def star(self, request, pk=None): @@ -132,7 +132,7 @@ class ProjectViewSet(ModelCrudViewSet): voters = votes_service.get_voters(project) voters_data = votes_serializers.VoterSerializer(voters, many=True) - return response.Ok(data=voters_data.data) + return response.Ok(voters_data.data) @detail_route(methods=["POST"]) def create_template(self, request, **kwargs): @@ -325,7 +325,7 @@ class ProjectTemplateViewSet(ModelCrudViewSet): ###################################################### -## Members Invitations and Roles +## Members & Invitations ###################################################### class MembershipViewSet(ModelCrudViewSet): @@ -359,7 +359,7 @@ class MembershipViewSet(ModelCrudViewSet): return response.BadRequest(err.message_dict) members_serialized = self.serializer_class(members, many=True) - return response.Ok(data=members_serialized.data) + return response.Ok(members_serialized.data) @detail_route(methods=["POST"]) def resend_invitation(self, request, **kwargs): @@ -403,20 +403,3 @@ class InvitationViewSet(ModelListViewSet): def list(self, *args, **kwargs): raise exc.PermissionDenied(_("You don't have permisions to see that.")) - - -class RolesViewSet(ModelCrudViewSet): - model = Role - serializer_class = serializers.RoleSerializer - permission_classes = (permissions.RolesPermission, ) - filter_backends = (filters.CanViewProjectFilterBackend,) - filter_fields = ('project',) - - def pre_delete(self, obj): - move_to = self.request.QUERY_PARAMS.get('moveTo', None) - if move_to: - role_dest = get_object_or_404(self.model, project=obj.project, id=move_to) - qs = models.Membership.objects.filter(project_id=obj.project.pk, role=obj) - qs.update(role=role_dest) - - super().pre_delete(obj) diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 2c89d57f..383dcbba 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -21,14 +21,15 @@ import mimetypes mimetypes.init() from django.contrib.contenttypes.models import ContentType -from taiga.base.api.utils import get_object_or_404 from django.conf import settings from django import http -from taiga.base.api import ModelCrudViewSet -from taiga.base.api import generics from taiga.base import filters from taiga.base import exceptions as exc +from taiga.base.api import generics +from taiga.base.api import ModelCrudViewSet +from taiga.base.api.utils import get_object_or_404 + from taiga.users.models import User from taiga.projects.notifications.mixins import WatchedResourceMixin diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 078f1f63..49023f94 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -17,12 +17,10 @@ from django.contrib.contenttypes.models import ContentType from django.utils import timezone -from rest_framework.response import Response -from rest_framework import status - -from taiga.base.api.utils import get_object_or_404 +from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ReadOnlyListViewSet +from taiga.base.api.utils import get_object_or_404 from . import permissions from . import serializers @@ -54,7 +52,7 @@ class HistoryViewSet(ReadOnlyListViewSet): else: serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + return response.Ok(serializer.data) @detail_route(methods=['post']) def delete_comment(self, request, pk): @@ -65,15 +63,15 @@ class HistoryViewSet(ReadOnlyListViewSet): self.check_permissions(request, 'delete_comment', comment) if comment is None: - return Response(status=status.HTTP_404_NOT_FOUND) + return response.NotFound() if comment.delete_comment_date or comment.delete_comment_user: - return Response({"error": "Comment already deleted"}, status=status.HTTP_400_BAD_REQUEST) + return response.BadRequest({"error": "Comment already deleted"}) comment.delete_comment_date = timezone.now() comment.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()} comment.save() - return Response(status=status.HTTP_200_OK) + return response.Ok() @detail_route(methods=['post']) def undelete_comment(self, request, pk): @@ -84,20 +82,20 @@ class HistoryViewSet(ReadOnlyListViewSet): self.check_permissions(request, 'undelete_comment', comment) if comment is None: - return Response(status=status.HTTP_404_NOT_FOUND) + return response.NotFound() if not comment.delete_comment_date and not comment.delete_comment_user: - return Response({"error": "Comment not deleted"}, status=status.HTTP_400_BAD_REQUEST) + return response.BadRequest({"error": "Comment not deleted"}) comment.delete_comment_date = None comment.delete_comment_user = None comment.save() - return Response(status=status.HTTP_200_OK) + return response.Ok() # Just for restframework! Because it raises # 404 on main api root if this method not exists. def list(self, request): - return Response({}) + return response.NotFound() def retrieve(self, request, pk): obj = self.get_object() diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index eb2dedcc..c09ad0f4 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -18,14 +18,12 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Q from django.http import Http404 -from rest_framework.response import Response -from rest_framework import status - -from taiga.base.api.utils import get_object_or_404 -from taiga.base import filters, response +from taiga.base import filters from taiga.base import exceptions as exc +from taiga.base import response from taiga.base.decorators import detail_route, list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.utils import get_object_or_404 from taiga.base import tags from taiga.users.models import User @@ -139,19 +137,24 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, super().pre_conditions_on_save(obj) if obj.milestone and obj.milestone.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this milestone to this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this sprint " + "to this issue.")) if obj.status and obj.status.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this status to this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this status " + "to this issue.")) if obj.severity and obj.severity.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this severity to this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this severity " + "to this issue.")) if obj.priority and obj.priority.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this priority to this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this priority " + "to this issue.")) if obj.type and obj.type.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this type to this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this type " + "to this issue.")) @list_route(methods=["GET"]) def by_ref(self, request): @@ -185,7 +188,7 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, self.check_permissions(request, 'upvote', issue) votes_service.add_vote(issue, user=request.user) - return Response(status=status.HTTP_200_OK) + return response.Ok() @detail_route(methods=['post']) def downvote(self, request, pk=None): @@ -194,7 +197,7 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, self.check_permissions(request, 'downvote', issue) votes_service.remove_vote(issue, user=request.user) - return Response(status=status.HTTP_200_OK) + return response.Ok() class VotersViewSet(ModelListViewSet): @@ -215,7 +218,7 @@ class VotersViewSet(ModelListViewSet): raise Http404 serializer = self.get_serializer(self.object) - return Response(serializer.data) + return response.Ok(serializer.data) def list(self, request, *args, **kwargs): issue_id = kwargs.get("issue_id", None) diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 3e26309d..132f9bf2 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -14,16 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ - -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from taiga.base.api.utils import get_object_or_404 from taiga.base import filters -from taiga.base import exceptions as exc +from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet +from taiga.base.api.utils import get_object_or_404 from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin @@ -97,4 +92,4 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView current_date = current_date + datetime.timedelta(days=1) optimal_points -= optimal_points_per_day - return Response(milestone_stats) + return response.Ok(milestone_stats) diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py index e2c0e91e..f0dfbf8f 100644 --- a/taiga/projects/notifications/api.py +++ b/taiga/projects/notifications/api.py @@ -14,20 +14,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ from django.db.models import Q -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from taiga.base.api.utils import get_object_or_404 -from taiga.base import filters -from taiga.base import exceptions as exc -from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet -from taiga.projects.models import Project - from taiga.projects.notifications.choices import NotifyLevel from . import serializers diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 4d0fcdbd..5fa9180b 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -16,9 +16,13 @@ from django.utils.translation import ugettext_lazy as _ -from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectOwner, - AllowAny, IsSuperUser, PermissionComponent) +from taiga.base.api.permissions import TaigaResourcePermission +from taiga.base.api.permissions import HasProjectPerm +from taiga.base.api.permissions import IsAuthenticated +from taiga.base.api.permissions import IsProjectOwner +from taiga.base.api.permissions import AllowAny +from taiga.base.api.permissions import IsSuperUser +from taiga.base.api.permissions import PermissionComponent from taiga.base import exceptions as exc from taiga.projects.models import Membership @@ -32,8 +36,8 @@ class CanLeaveProject(PermissionComponent): try: if not services.can_user_leave_project(request.user, obj): - raise exc.PermissionDenied(_("You can't leave the project if there are no more owners")) - + raise exc.PermissionDenied(_("You can't leave the project if there are no " + "more owners")) return True except Membership.DoesNotExist: return False @@ -140,14 +144,6 @@ class IssueTypePermission(TaigaResourcePermission): bulk_update_order_perms = IsProjectOwner() -class RolesPermission(TaigaResourcePermission): - retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() - list_perms = AllowAny() - - # Project Templates class ProjectTemplatePermission(TaigaResourcePermission): diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 8e53e179..97b41de4 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -16,14 +16,13 @@ from django.apps import apps -from rest_framework.response import Response - -from taiga.base.api.utils import get_object_or_404 from taiga.base import exceptions as exc +from taiga.base import response from taiga.base.api import viewsets -from .serializers import ResolverSerializer +from taiga.base.api.utils import get_object_or_404 from taiga.permissions.service import user_has_perm +from .serializers import ResolverSerializer from . import permissions @@ -42,19 +41,22 @@ class ResolverViewSet(viewsets.ViewSet): self.check_permissions(request, "list", project) - result = { - "project": project.pk - } + result = {"project": project.pk} if data["us"] and user_has_perm(request.user, "view_us", project): - result["us"] = get_object_or_404(project.user_stories.all(), ref=data["us"]).pk + result["us"] = get_object_or_404(project.user_stories.all(), + ref=data["us"]).pk if data["task"] and user_has_perm(request.user, "view_tasks", project): - result["task"] = get_object_or_404(project.tasks.all(), ref=data["task"]).pk + result["task"] = get_object_or_404(project.tasks.all(), + ref=data["task"]).pk if data["issue"] and user_has_perm(request.user, "view_issues", project): - result["issue"] = get_object_or_404(project.issues.all(), ref=data["issue"]).pk + result["issue"] = get_object_or_404(project.issues.all(), + ref=data["issue"]).pk if data["milestone"] and user_has_perm(request.user, "view_milestones", project): - result["milestone"] = get_object_or_404(project.milestones.all(), slug=data["milestone"]).pk + result["milestone"] = get_object_or_404(project.milestones.all(), + slug=data["milestone"]).pk if data["wikipage"] and user_has_perm(request.user, "view_wiki_pages", project): - result["wikipage"] = get_object_or_404(project.wiki_pages.all(), slug=data["wikipage"]).pk + result["wikipage"] = get_object_or_404(project.wiki_pages.all(), + slug=data["wikipage"]).pk - return Response(result) + return response.Ok(result) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 5c89484f..ad0e99a7 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -14,27 +14,32 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from os import path from django.utils.translation import ugettext_lazy as _ from django.db.models import Q from rest_framework import serializers -from taiga.base.serializers import JsonField, PgArrayField, ModelSerializer, TagsColorsField -from taiga.users.models import Role, User +from taiga.base.serializers import JsonField +from taiga.base.serializers import PgArrayField +from taiga.base.serializers import ModelSerializer +from taiga.base.serializers import TagsColorsField from taiga.users.services import get_photo_or_gravatar_url from taiga.users.serializers import UserSerializer +from taiga.users.serializers import ProjectRoleSerializer from taiga.users.validators import RoleExistsValidator -from taiga.permissions.service import get_user_project_permissions, is_project_owner +from taiga.permissions.service import get_user_project_permissions +from taiga.permissions.service import is_project_owner from . import models from . import services from . validators import ProjectExistsValidator -# User Stories common serializers +###################################################### +## Custom values for selectors +###################################################### class PointsSerializer(ModelSerializer): class Meta: @@ -69,10 +74,12 @@ class UserStoryStatusSerializer(ModelSerializer): qs = None # If the user story status exists: if self.object and attrs.get("name", None): - qs = models.UserStoryStatus.objects.filter(project=self.object.project, name=attrs[source]) + qs = models.UserStoryStatus.objects.filter(project=self.object.project, + name=attrs[source]) if not self.object and attrs.get("project", None) and attrs.get("name", None): - qs = models.UserStoryStatus.objects.filter(project=attrs["project"], name=attrs[source]) + qs = models.UserStoryStatus.objects.filter(project=attrs["project"], + name=attrs[source]) if qs and qs.exists(): raise serializers.ValidationError("Name duplicated for the project") @@ -80,8 +87,6 @@ class UserStoryStatusSerializer(ModelSerializer): return attrs -# Task common serializers - class TaskStatusSerializer(ModelSerializer): class Meta: model = models.TaskStatus @@ -103,7 +108,6 @@ class TaskStatusSerializer(ModelSerializer): return attrs -# Issues common serializers class SeveritySerializer(ModelSerializer): class Meta: @@ -142,13 +146,16 @@ class IssueTypeSerializer(ModelSerializer): model = models.IssueType -# Projects +###################################################### +## Members +###################################################### class MembershipSerializer(ModelSerializer): role_name = serializers.CharField(source='role.name', required=False, read_only=True) full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True) user_email = serializers.EmailField(source='user.email', required=False, read_only=True) - is_user_active = serializers.BooleanField(source='user.is_active', required=False, read_only=True) + is_user_active = serializers.BooleanField(source='user.is_active', required=False, + read_only=True) email = serializers.EmailField(required=True) color = serializers.CharField(source='user.color', required=False, read_only=True) photo = serializers.SerializerMethodField("get_photo") @@ -210,7 +217,8 @@ class MembershipSerializer(ModelSerializer): if project is None: project = self.object.project - if self.object and not services.project_has_valid_owners(project, exclude_user=self.object.user): + if (self.object and + not services.project_has_valid_owners(project, exclude_user=self.object.user)): raise serializers.ValidationError(_("At least one of the user must be an active admin")) return attrs @@ -229,6 +237,21 @@ class ProjectMembershipSerializer(ModelSerializer): return get_photo_or_gravatar_url(project.user) +class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer): + email = serializers.EmailField() + role_id = serializers.IntegerField() + + +class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer): + project_id = serializers.IntegerField() + bulk_memberships = MemberBulkSerializer(many=True) + invitation_extra_text = serializers.CharField(required=False, max_length=255) + + +###################################################### +## Projects +###################################################### + class ProjectSerializer(ModelSerializer): tags = PgArrayField(required=False) anon_permissions = PgArrayField(required=False) @@ -300,23 +323,20 @@ class ProjectDetailSerializer(ProjectSerializer): return serializer.data -class ProjectRoleSerializer(ModelSerializer): +###################################################### +## Starred +###################################################### + +class StarredSerializer(ModelSerializer): class Meta: - model = Role - fields = ('id', 'name', 'slug', 'order', 'computable') + model = models.Project + fields = ['id', 'name', 'slug'] -class RoleSerializer(ModelSerializer): - members_count = serializers.SerializerMethodField("get_members_count") - permissions = PgArrayField(required=False) - - class Meta: - model = Role - fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count') - - def get_members_count(self, obj): - return obj.memberships.count() +###################################################### +## Project Templates +###################################################### class ProjectTemplateSerializer(ModelSerializer): default_options = JsonField(required=False, label=_("Default options")) @@ -332,20 +352,3 @@ class ProjectTemplateSerializer(ModelSerializer): class Meta: model = models.ProjectTemplate read_only_fields = ("created_date", "modified_date") - - -class StarredSerializer(ModelSerializer): - class Meta: - model = models.Project - fields = ['id', 'name', 'slug'] - - -class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer): - email = serializers.EmailField() - role_id = serializers.IntegerField() - - -class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - bulk_memberships = MemberBulkSerializer(many=True) - invitation_extra_text = serializers.CharField(required=False, max_length=255) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index b0725f68..32d40242 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -16,19 +16,19 @@ from contextlib import suppress +from rest_framework import status + from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ from django.core.exceptions import ObjectDoesNotExist -from rest_framework.response import Response -from rest_framework import status - -from taiga.base.api.utils import get_object_or_404 -from taiga.base import filters, response +from taiga.base import filters from taiga.base import exceptions as exc +from taiga.base import response from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet +from taiga.base.api.utils import get_object_or_404 from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index 45b7f5a2..f67150bf 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -17,13 +17,12 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework import status -from taiga.base.api.utils import get_object_or_404 from taiga.base import filters from taiga.base import exceptions as exc +from taiga.base import response from taiga.base.api import ModelCrudViewSet +from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route from taiga.projects.models import Project from taiga.mdrender.service import render as mdrender @@ -58,17 +57,17 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, project_id = request.DATA.get("project_id", None) if not content: - raise exc.WrongArguments({"content": "No content parameter"}) + raise exc.WrongArguments({"content": _("No content parameter")}) if not project_id: - raise exc.WrongArguments({"project_id": "No project_id parameter"}) + raise exc.WrongArguments({"project_id": _("No project_id parameter")}) project = get_object_or_404(Project, pk=project_id) self.check_permissions(request, "render", project) data = mdrender(project, content) - return Response({"data": data}) + return response.Ok({"data": data}) def pre_save(self, obj): if not obj.owner: diff --git a/taiga/routers.py b/taiga/routers.py index 7ca20aa6..ad39b94c 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -18,15 +18,18 @@ from taiga.base import routers router = routers.DefaultRouter(trailing_slash=False) -# taiga.users -from taiga.users.api import UsersViewSet + +# Users & Roles from taiga.auth.api import AuthViewSet +from taiga.users.api import UsersViewSet +from taiga.users.api import RolesViewSet -router.register(r"users", UsersViewSet, base_name="users") router.register(r"auth", AuthViewSet, base_name="auth") +router.register(r"users", UsersViewSet, base_name="users") +router.register(r"roles", RolesViewSet, base_name="roles") -#taiga.userstorage +# User Storage from taiga.userstorage.api import StorageEntriesViewSet router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage") @@ -51,8 +54,7 @@ router.register(r"importer", ProjectImporterViewSet, base_name="importer") router.register(r"exporter", ProjectExporterViewSet, base_name="exporter") -# Projects & Types -from taiga.projects.api import RolesViewSet +# Projects & Selectors from taiga.projects.api import ProjectViewSet from taiga.projects.api import MembershipViewSet from taiga.projects.api import InvitationViewSet @@ -65,8 +67,6 @@ from taiga.projects.api import PriorityViewSet from taiga.projects.api import SeverityViewSet from taiga.projects.api import ProjectTemplateViewSet - -router.register(r"roles", RolesViewSet, base_name="roles") router.register(r"projects", ProjectViewSet, base_name="projects") router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates") router.register(r"memberships", MembershipViewSet, base_name="memberships") @@ -79,22 +79,27 @@ router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types") router.register(r"priorities", PriorityViewSet, base_name="priorities") router.register(r"severities",SeverityViewSet , base_name="severities") + # Attachments from taiga.projects.attachments.api import UserStoryAttachmentViewSet from taiga.projects.attachments.api import IssueAttachmentViewSet from taiga.projects.attachments.api import TaskAttachmentViewSet from taiga.projects.attachments.api import WikiAttachmentViewSet -router.register(r"userstories/attachments", UserStoryAttachmentViewSet, base_name="userstory-attachments") +router.register(r"userstories/attachments", UserStoryAttachmentViewSet, + base_name="userstory-attachments") router.register(r"tasks/attachments", TaskAttachmentViewSet, base_name="task-attachments") router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments") router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments") + # Webhooks from taiga.webhooks.api import WebhookViewSet, WebhookLogViewSet + router.register(r"webhooks", WebhookViewSet, base_name="webhooks") router.register(r"webhooklogs", WebhookLogViewSet, base_name="webhooklogs") + # History & Components from taiga.projects.history.api import UserStoryHistory from taiga.projects.history.api import TaskHistory @@ -131,22 +136,30 @@ router.register(r"issues/(?P\d+)/voters", VotersViewSet, base_name="is router.register(r"wiki", WikiViewSet, base_name="wiki") router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") + # Notify policies from taiga.projects.notifications.api import NotifyPolicyViewSet router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications") + # GitHub webhooks from taiga.hooks.github.api import GitHubViewSet + router.register(r"github-hook", GitHubViewSet, base_name="github-hook") + # Gitlab webhooks from taiga.hooks.gitlab.api import GitLabViewSet + router.register(r"gitlab-hook", GitLabViewSet, base_name="gitlab-hook") + # Bitbucket webhooks from taiga.hooks.bitbucket.api import BitBucketViewSet + router.register(r"bitbucket-hook", BitBucketViewSet, base_name="bitbucket-hook") + # feedback # - see taiga.feedback.routers and taiga.feedback.apps diff --git a/taiga/searches/api.py b/taiga/searches/api.py index 182e8213..0a8bb788 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -16,10 +16,9 @@ from django.apps import apps -from rest_framework.response import Response from rest_framework import viewsets -from taiga.base import exceptions as excp +from taiga.base import response from taiga.base.api.utils import get_object_or_404 from taiga.projects.userstories.serializers import UserStorySerializer from taiga.projects.tasks.serializers import TaskSerializer @@ -48,7 +47,7 @@ class SearchViewSet(viewsets.ViewSet): result["wikipages"] = self._search_wiki_pages(project, text) result["count"] = sum(map(lambda x: len(x), result.values())) - return Response(result) + return response.Ok(result) def _get_project(self, project_id): project_model = apps.get_model("projects", "Project") diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index 10795cef..4d907a69 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -16,8 +16,7 @@ from django.contrib.contenttypes.models import ContentType -from rest_framework.response import Response - +from taiga.base import response from taiga.base.api.utils import get_object_or_404 from taiga.base.api import GenericViewSet @@ -52,12 +51,12 @@ class TimelineViewSet(GenericViewSet): else: serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + return response.Ok(serializer.data) # Just for restframework! Because it raises # 404 on main api root if this method not exists. def list(self, request): - return Response({}) + return response.NotFound() def retrieve(self, request, pk): obj = self.get_object() diff --git a/taiga/users/api.py b/taiga/users/api.py index b634a962..547e99c6 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -23,23 +23,22 @@ from django.core.validators import validate_email from django.core.exceptions import ValidationError from django.conf import settings -from easy_thumbnails.source_generators import pil_image - -from rest_framework.response import Response -from rest_framework.filters import BaseFilterBackend -from rest_framework import status - -from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail - -from taiga.auth.tokens import get_user_for_token -from taiga.base.decorators import list_route, detail_route from taiga.base import exceptions as exc +from taiga.base import filters +from taiga.base import response +from taiga.auth.tokens import get_user_for_token +from taiga.base.decorators import list_route +from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet from taiga.base.api.utils import get_object_or_404 -from taiga.base.utils.slug import slugify_uniquely +from taiga.base.filters import MembersFilterBackend from taiga.projects.votes import services as votes_service from taiga.projects.serializers import StarredSerializer -from taiga.permissions.service import is_project_owner + +from easy_thumbnails.source_generators import pil_image + +from djmail.template_mail import MagicMailBuilder +from djmail.template_mail import InlineCSSTemplateMail from . import models from . import serializers @@ -47,22 +46,9 @@ from . import permissions from .signals import user_cancel_account as user_cancel_account_signal -class MembersFilterBackend(BaseFilterBackend): - def filter_queryset(self, request, queryset, view): - project_id = request.QUERY_PARAMS.get('project', None) - if project_id: - Project = apps.get_model('projects', 'Project') - 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: - return queryset - - return [] - +###################################################### +## User +###################################################### class UsersViewSet(ModelCrudViewSet): permission_classes = (permissions.UserPermission,) @@ -73,7 +59,9 @@ class UsersViewSet(ModelCrudViewSet): raise exc.NotSupported() def list(self, request, *args, **kwargs): - self.object_list = MembersFilterBackend().filter_queryset(request, self.get_queryset(), self) + self.object_list = MembersFilterBackend().filter_queryset(request, + self.get_queryset(), + self) page = self.paginate_queryset(self.object_list) if page is not None: @@ -81,7 +69,7 @@ class UsersViewSet(ModelCrudViewSet): else: serializer = self.get_serializer(self.object_list, many=True) - return Response(serializer.data) + return response.Ok(serializer.data) @list_route(methods=["POST"]) def password_recovery(self, request, pk=None): @@ -106,8 +94,8 @@ class UsersViewSet(ModelCrudViewSet): email = mbuilder.password_recovery(user.email, {"user": user}) email.send() - return Response({"detail": _("Mail sended successful!"), - "email": user.email}) + return response.Ok({"detail": _("Mail sended successful!"), + "email": user.email}) @list_route(methods=["POST"]) def change_password_from_recovery(self, request, pk=None): @@ -130,7 +118,7 @@ class UsersViewSet(ModelCrudViewSet): user.token = None user.save(update_fields=["password", "token"]) - return Response(status=status.HTTP_204_NO_CONTENT) + return response.NoContent() @list_route(methods=["POST"]) def change_password(self, request, pk=None): @@ -158,7 +146,7 @@ class UsersViewSet(ModelCrudViewSet): request.user.set_password(password) request.user.save(update_fields=["password"]) - return Response(status=status.HTTP_204_NO_CONTENT) + return response.NoContent() @list_route(methods=["POST"]) def change_avatar(self, request): @@ -181,7 +169,7 @@ class UsersViewSet(ModelCrudViewSet): request.user.save(update_fields=["photo"]) user_data = serializers.UserSerializer(request.user).data - return Response(user_data, status=status.HTTP_200_OK) + return response.Ok(user_data) @list_route(methods=["POST"]) def remove_avatar(self, request): @@ -192,7 +180,7 @@ class UsersViewSet(ModelCrudViewSet): request.user.photo = None request.user.save(update_fields=["photo"]) user_data = serializers.UserSerializer(request.user).data - return Response(user_data, status=status.HTTP_200_OK) + return response.Ok(user_data) @detail_route(methods=["GET"]) def starred(self, request, pk=None): @@ -201,7 +189,7 @@ class UsersViewSet(ModelCrudViewSet): stars = votes_service.get_voted(user.pk, model=apps.get_model('projects', 'Project')) stars_data = StarredSerializer(stars, many=True) - return Response(stars_data.data) + return response.Ok(stars_data.data) #TODO: commit_on_success def partial_update(self, request, *args, **kwargs): @@ -249,12 +237,14 @@ class UsersViewSet(ModelCrudViewSet): """ serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False) if not serializer.is_valid(): - raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you didn't use it before?")) + raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you " + "didn't use it before?")) try: user = models.User.objects.get(email_token=serializer.data["email_token"]) except models.User.DoesNotExist: - raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you didn't use it before?")) + raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you " + "didn't use it before?")) self.check_permissions(request, "change_email", user) user.email = user.new_email @@ -262,7 +252,7 @@ class UsersViewSet(ModelCrudViewSet): user.email_token = None user.save(update_fields=["email", "new_email", "email_token"]) - return Response(status=status.HTTP_204_NO_CONTENT) + return response.NoContent() @list_route(methods=["GET"]) def me(self, request, pk=None): @@ -271,7 +261,7 @@ class UsersViewSet(ModelCrudViewSet): """ self.check_permissions(request, "me", None) user_data = serializers.UserSerializer(request.user).data - return Response(user_data, status=status.HTTP_200_OK) + return response.Ok(user_data) @list_route(methods=["POST"]) def cancel(self, request, pk=None): @@ -294,7 +284,7 @@ class UsersViewSet(ModelCrudViewSet): raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) user.cancel() - return Response(status=status.HTTP_204_NO_CONTENT) + return response.NoContent() def destroy(self, request, pk=None): user = self.get_object() @@ -303,4 +293,26 @@ class UsersViewSet(ModelCrudViewSet): request_data = stream is not None and stream.GET or None user_cancel_account_signal.send(sender=user.__class__, user=user, request_data=request_data) user.cancel() - return Response(status=status.HTTP_204_NO_CONTENT) + return response.NoContent() + + +###################################################### +## Role +###################################################### + +class RolesViewSet(ModelCrudViewSet): + model = models.Role + serializer_class = serializers.RoleSerializer + permission_classes = (permissions.RolesPermission, ) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ('project',) + + def pre_delete(self, obj): + move_to = self.request.QUERY_PARAMS.get('moveTo', None) + if move_to: + membership_model = apps.get_model("projects", "Membership") + role_dest = get_object_or_404(self.model, project=obj.project, id=move_to) + qs = membership_model.objects.filter(project_id=obj.project.pk, role=obj) + qs.update(role=role_dest) + + super().pre_delete(obj) diff --git a/taiga/users/models.py b/taiga/users/models.py index 83226725..e2ffe491 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -174,6 +174,7 @@ class User(AbstractBaseUser, PermissionsMixin): self.save() self.auth_data.all().delete() + class Role(models.Model): name = models.CharField(max_length=200, null=False, blank=False, verbose_name=_("name")) diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py index 5b7a8f27..cbabe22c 100644 --- a/taiga/users/permissions.py +++ b/taiga/users/permissions.py @@ -14,9 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api.permissions import (TaigaResourcePermission, IsSuperUser, - AllowAny, PermissionComponent, - IsAuthenticated) +from taiga.base.api.permissions import TaigaResourcePermission +from taiga.base.api.permissions import IsSuperUser +from taiga.base.api.permissions import AllowAny +from taiga.base.api.permissions import IsAuthenticated +from taiga.base.api.permissions import HasProjectPerm +from taiga.base.api.permissions import IsProjectOwner +from taiga.base.api.permissions import PermissionComponent class IsTheSameUser(PermissionComponent): @@ -39,3 +43,11 @@ class UserPermission(TaigaResourcePermission): remove_avatar_perms = IsAuthenticated() starred_perms = AllowAny() change_email_perms = IsTheSameUser() + + +class RolesPermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 5fb28310..acd42741 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -16,16 +16,24 @@ from django.core import validators from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from taiga.base.serializers import Serializer +from taiga.base.serializers import ModelSerializer +from taiga.base.serializers import PgArrayField -from .models import User +from .models import User, Role from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url import re -class UserSerializer(serializers.ModelSerializer): +###################################################### +## User +###################################################### + +class UserSerializer(ModelSerializer): full_name_display = serializers.SerializerMethodField("get_full_name_display") photo = serializers.SerializerMethodField("get_photo") big_photo = serializers.SerializerMethodField("get_big_photo") @@ -39,16 +47,19 @@ class UserSerializer(serializers.ModelSerializer): def validate_username(self, attrs, source): value = attrs[source] - validator = validators.RegexValidator(re.compile('^[\w.-]+$'), "invalid username", "invalid") + validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), + _("invalid")) try: validator(value) except ValidationError: - raise serializers.ValidationError("Required. 255 characters or fewer. Letters, numbers " - "and /./-/_ characters'") + raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, " + "numbers and /./-/_ characters'")) - if self.object and self.object.username != value and User.objects.filter(username=value).exists(): - raise serializers.ValidationError("Invalid username. Try with a different one.") + if (self.object and + self.object.username != value and + User.objects.filter(username=value).exists()): + raise serializers.ValidationError(_("Invalid username. Try with a different one.")) return attrs @@ -62,14 +73,36 @@ class UserSerializer(serializers.ModelSerializer): return get_big_photo_or_gravatar_url(user) -class RecoverySerializer(serializers.Serializer): +class RecoverySerializer(Serializer): token = serializers.CharField(max_length=200) password = serializers.CharField(min_length=6) -class ChangeEmailSerializer(serializers.Serializer): +class ChangeEmailSerializer(Serializer): email_token = serializers.CharField(max_length=200) -class CancelAccountSerializer(serializers.Serializer): +class CancelAccountSerializer(Serializer): cancel_token = serializers.CharField(max_length=200) + + +###################################################### +## Role +###################################################### + +class RoleSerializer(ModelSerializer): + members_count = serializers.SerializerMethodField("get_members_count") + permissions = PgArrayField(required=False) + + class Meta: + model = Role + fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count') + + def get_members_count(self, obj): + return obj.memberships.count() + + +class ProjectRoleSerializer(ModelSerializer): + class Meta: + model = Role + fields = ('id', 'name', 'slug', 'order', 'computable') diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index 31242102..55f3fca4 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -16,10 +16,11 @@ import json -from rest_framework.response import Response - from taiga.base import filters -from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base import response +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelListViewSet + from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import detail_route @@ -44,7 +45,8 @@ class WebhookViewSet(ModelCrudViewSet): webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key) log = serializers.WebhookLogSerializer(webhooklog) - return Response(log.data) + return response.Ok(log.data) + class WebhookLogViewSet(ModelListViewSet): model = models.WebhookLog @@ -60,7 +62,9 @@ class WebhookLogViewSet(ModelListViewSet): webhook = webhooklog.webhook - webhooklog = tasks.resend_webhook(webhook.id, webhook.url, webhook.key, webhooklog.request_data) + webhooklog = tasks.resend_webhook(webhook.id, webhook.url, webhook.key, + webhooklog.request_data) + log = serializers.WebhookLogSerializer(webhooklog) - return Response(log.data) + return response.Ok(log.data) diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index ed0a6a77..0c31df3d 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import serializers +from taiga.users.serializers import RoleSerializer from taiga.permissions.permissions import MEMBERS_PERMISSIONS from tests import factories as f @@ -140,19 +141,19 @@ def test_roles_update(client, data): data.project_owner ] - role_data = serializers.RoleSerializer(data.public_project.roles.all()[0]).data + role_data = RoleSerializer(data.public_project.roles.all()[0]).data role_data["name"] = "test" role_data = json.dumps(role_data) results = helper_test_http_method(client, 'put', public_url, role_data, users) assert results == [401, 403, 403, 403, 200] - role_data = serializers.RoleSerializer(data.private_project1.roles.all()[0]).data + role_data = RoleSerializer(data.private_project1.roles.all()[0]).data role_data["name"] = "test" role_data = json.dumps(role_data) results = helper_test_http_method(client, 'put', private1_url, role_data, users) assert results == [401, 403, 403, 403, 200] - role_data = serializers.RoleSerializer(data.private_project2.roles.all()[0]).data + role_data = RoleSerializer(data.private_project2.roles.all()[0]).data role_data["name"] = "test" role_data = json.dumps(role_data) results = helper_test_http_method(client, 'put', private2_url, role_data, users) diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index f071400e..e5e0416b 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -55,7 +55,7 @@ def test_ok_signature(client): urllib.parse.urlencode(data, True), content_type="application/x-www-form-urlencoded", REMOTE_ADDR=settings.BITBUCKET_VALID_ORIGIN_IPS[0]) - assert response.status_code == 200 + assert response.status_code == 204 def test_invalid_ip(client): project=f.ProjectFactory() @@ -91,7 +91,7 @@ def test_not_ip_filter(client): urllib.parse.urlencode(data, True), content_type="application/x-www-form-urlencoded", REMOTE_ADDR="111.111.111.112") - assert response.status_code == 200 + assert response.status_code == 204 def test_push_event_detected(client): @@ -108,7 +108,7 @@ def test_push_event_detected(client): assert process_event_mock.call_count == 1 - assert response.status_code == 200 + assert response.status_code == 204 def test_push_event_issue_processing(client): diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 08a33bbd..5115be6e 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -50,7 +50,7 @@ def test_ok_signature(client): HTTP_X_HUB_SIGNATURE="sha1=3c8e83fdaa266f81c036ea0b71e98eb5e054581a", content_type="application/json") - assert response.status_code == 200 + assert response.status_code == 204 def test_push_event_detected(client): @@ -70,7 +70,7 @@ def test_push_event_detected(client): assert process_event_mock.call_count == 1 - assert response.status_code == 200 + assert response.status_code == 204 def test_push_event_issue_processing(client): diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index 2bb4305a..888d745a 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -55,7 +55,7 @@ def test_ok_signature(client): content_type="application/json", REMOTE_ADDR="111.111.111.111") - assert response.status_code == 200 + assert response.status_code == 204 def test_invalid_ip(client): @@ -95,7 +95,7 @@ def test_not_ip_filter(client): content_type="application/json", REMOTE_ADDR="111.111.111.111") - assert response.status_code == 200 + assert response.status_code == 204 def test_push_event_detected(client): @@ -115,7 +115,7 @@ def test_push_event_detected(client): assert process_event_mock.call_count == 1 - assert response.status_code == 200 + assert response.status_code == 204 def test_push_event_issue_processing(client): @@ -340,7 +340,6 @@ def test_issues_event_bad_issue(client): assert len(mail.outbox) == 0 - def test_api_get_project_modules(client): project = f.create_project() membership = f.MembershipFactory(project=project, user=project.owner, is_owner=True)