From 0697190b3cb0b82f6110b834bccafe9f13a0d8bb Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:36:25 +0200 Subject: [PATCH 01/11] Adds permission add_project. --- pootle/apps/pootle_app/views/admin/permissions.py | 5 ++++- pootle/core/initdb.py | 4 ++++ pootle/settings/30-site.conf | 3 +++ pytest_pootle/env.py | 4 ++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pootle/apps/pootle_app/views/admin/permissions.py b/pootle/apps/pootle_app/views/admin/permissions.py index 3060aeb3368..5be31011ac9 100644 --- a/pootle/apps/pootle_app/views/admin/permissions.py +++ b/pootle/apps/pootle_app/views/admin/permissions.py @@ -20,7 +20,10 @@ User = get_user_model() PERMISSIONS = { - 'positive': ['view', 'suggest', 'translate', 'review', 'administrate'], + 'positive': [ + 'view', 'suggest', 'translate', 'review', + 'administrate', 'add_project' + ], 'negative': ['hide'], } diff --git a/pootle/core/initdb.py b/pootle/core/initdb.py index c5266b27a3f..15800a6297f 100644 --- a/pootle/core/initdb.py +++ b/pootle/core/initdb.py @@ -155,6 +155,10 @@ def create_pootle_permissions(self): 'name': _("Can perform administrative tasks"), 'codename': "administrate", }, + { + 'name': _("Can add projects"), + 'codename': "add_project", + }, ] criteria = { diff --git a/pootle/settings/30-site.conf b/pootle/settings/30-site.conf index 21691f33ace..96eafb50a54 100644 --- a/pootle/settings/30-site.conf +++ b/pootle/settings/30-site.conf @@ -31,6 +31,9 @@ POOTLE_CONTACT_ENABLED = True # Whether to email reviewer's feedback to suggesters. POOTLE_EMAIL_FEEDBACK_ENABLED = False +# Whether administrators of projects can edit projects +POOTLE_PROJECTADMIN_CAN_EDITPROJECTS = True + # By default Pootle uses SMTP server on localhost, if the server is # not configured for sending emails use these settings to setup an # external outgoing SMTP server. diff --git a/pytest_pootle/env.py b/pytest_pootle/env.py index aed1034ad87..b65bf3ab892 100644 --- a/pytest_pootle/env.py +++ b/pytest_pootle/env.py @@ -207,6 +207,10 @@ def setup_permissions(self): 'administrate', 'Can administrate a TP', pootle_content_type) + _require_permission( + 'add_project', + 'Can add projects', + pootle_content_type) def setup_fs(self): from pytest_pootle.utils import add_store_fs From 74549ee233b20fb775ffbb78ad80512f0dce1faa Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:43:01 +0200 Subject: [PATCH 02/11] Adds requires_permission_class decorator to check permission in API class. --- pootle/core/views/decorators.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pootle/core/views/decorators.py b/pootle/core/views/decorators.py index c5a7b86ae71..1d91b20387a 100644 --- a/pootle/core/views/decorators.py +++ b/pootle/core/views/decorators.py @@ -11,7 +11,7 @@ from django.core.exceptions import PermissionDenied from pootle.i18n.gettext import ugettext as _ -from pootle_app.models.permissions import get_matching_permissions +from pootle_app.models.permissions import get_matching_permissions, check_permission def check_directory_permission(permission_codename, request, directory): @@ -71,3 +71,21 @@ def method_wrapper(self, request, *args, **kwargs): return f(self, request, *args, **kwargs) return method_wrapper return class_wrapper + + +def requires_permission_class(permission): + + def class_wrapper(f): + + @functools.wraps(f) + def method_wrapper(self, request, *args, **kwargs): + has_permission = ( + request.user.is_authenticated + and check_permission(permission, request)) + + if not has_permission: + raise PermissionDenied( + _("Insufficient rights to access this page."), ) + return f(self, request, *args, **kwargs) + return method_wrapper + return class_wrapper From 58b7eb0637e701970ac4de25cb4ea15cfef6e5aa Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:46:02 +0200 Subject: [PATCH 03/11] Updates admin API to take permissions into account. --- .../apps/pootle_app/views/admin/__init__.py | 6 +- .../apps/pootle_app/views/admin/projects.py | 67 +++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/pootle/apps/pootle_app/views/admin/__init__.py b/pootle/apps/pootle_app/views/admin/__init__.py index e0478b1e05e..cf7f4ac4d14 100644 --- a/pootle/apps/pootle_app/views/admin/__init__.py +++ b/pootle/apps/pootle_app/views/admin/__init__.py @@ -7,10 +7,12 @@ # AUTHORS file for copyright and authorship information. from .languages import LanguageAdminView, LanguageAPIView -from .projects import ProjectAdminView, ProjectAPIView +from .projects import ( + ProjectGenericAdminView, ProjectAdminView, ProjectAPIView) from .users import UserAdminView, UserAPIView __all__ = ( - 'LanguageAdminView', 'LanguageAPIView', 'ProjectAdminView', + 'LanguageAdminView', 'LanguageAPIView', + 'ProjectGenericAdminView', 'ProjectAdminView', 'ProjectAPIView', 'UserAdminView', 'UserAPIView') diff --git a/pootle/apps/pootle_app/views/admin/projects.py b/pootle/apps/pootle_app/views/admin/projects.py index 1b2868113dd..2ff7f479016 100644 --- a/pootle/apps/pootle_app/views/admin/projects.py +++ b/pootle/apps/pootle_app/views/admin/projects.py @@ -6,12 +6,20 @@ # or later license. See the LICENSE file for a copy of the license and the # AUTHORS file for copyright and authorship information. +import json + from django.views.generic import TemplateView from pootle.core.delegate import formats +from pootle.core.http import JsonResponse from pootle.core.views import APIView +from pootle.core.views.decorators import ( + set_permissions, requires_permission_class) from pootle.core.views.mixins import SuperuserRequiredMixin from pootle_app.forms import ProjectForm +from pootle_app.models.directory import Directory +from pootle_app.models.permissions import ( + check_user_permission, get_pootle_permission, PermissionSet) from pootle_language.models import Language from pootle_project.models import PROJECT_CHECKERS, Project @@ -19,8 +27,9 @@ __all__ = ('ProjectAdminView', 'ProjectAPIView') -class ProjectAdminView(SuperuserRequiredMixin, TemplateView): +class ProjectGenericAdminView(TemplateView): template_name = 'admin/projects.html' + page_code = 'admin-projects' def get_context_data(self, **kwargs): languages = Language.objects.exclude(code='templates') @@ -42,7 +51,7 @@ def get_context_data(self, **kwargs): in sorted(PROJECT_CHECKERS.keys())] return { - 'page': 'admin-projects', + 'page': self.page_code, 'form_choices': { 'checkstyle': project_checker_choices, 'filetypes': filetypes, @@ -55,11 +64,61 @@ def get_context_data(self, **kwargs): } -class ProjectAPIView(SuperuserRequiredMixin, APIView): +class ProjectAdminView(SuperuserRequiredMixin, ProjectGenericAdminView): + pass + + +class ProjectAPIView(APIView): model = Project base_queryset = Project.objects.order_by('-id') add_form_class = ProjectForm edit_form_class = ProjectForm page_size = 10 search_fields = ('code', 'fullname', 'disabled') - m2m = ("filetypes", ) + m2m = ("filetypes",) + + @property + def permission_context(self): + return Directory.objects.root + + @set_permissions + @requires_permission_class("add_project") + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: + exclude_projects = [project.pk + for project in self.base_queryset.all() + if not check_user_permission( + request.user, + "administrate", + project.directory + )] + self.base_queryset = self.base_queryset.exclude( + pk__in=exclude_projects) + return super(ProjectAPIView, self).dispatch(request, + *args, **kwargs) + + def post(self, request, *args, **kwargs): + try: + request_dict = json.loads(request.body) + except ValueError: + return self.status_msg('Invalid JSON data', status=400) + + form = self.add_form_class(request_dict) + + if form.is_valid(): + new_object = form.save() + permissionset = PermissionSet.objects.create( + user=request.user, + directory=new_object.directory + ) + permissionset.positive_permissions.add( + get_pootle_permission("administrate") + ) + request.user.permissionset_set.add(permissionset) + + wrapper_qs = self.base_queryset.filter(pk=new_object.pk) + return JsonResponse( + self.qs_to_values(wrapper_qs, single_object=True) + ) + + return self.form_invalid(form) From d484540162006bf5a07e04f7499fd0c43ba2f211 Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:48:46 +0200 Subject: [PATCH 04/11] Adds userside views for project creation/edition. --- pootle/apps/pootle_app/views/user/__init__.py | 13 ++++++++ pootle/apps/pootle_app/views/user/projects.py | 30 ++++++++++++++++++ pootle/apps/pootle_app/views/user/urls.py | 31 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 pootle/apps/pootle_app/views/user/__init__.py create mode 100644 pootle/apps/pootle_app/views/user/projects.py create mode 100644 pootle/apps/pootle_app/views/user/urls.py diff --git a/pootle/apps/pootle_app/views/user/__init__.py b/pootle/apps/pootle_app/views/user/__init__.py new file mode 100644 index 00000000000..6c1d6610db8 --- /dev/null +++ b/pootle/apps/pootle_app/views/user/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from pootle_app.views.admin import ProjectAPIView +from .projects import ProjectUserView + + +__all__ = ('ProjectAPIView', 'ProjectUserView') diff --git a/pootle/apps/pootle_app/views/user/projects.py b/pootle/apps/pootle_app/views/user/projects.py new file mode 100644 index 00000000000..21958a4baf6 --- /dev/null +++ b/pootle/apps/pootle_app/views/user/projects.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + + +from pootle.core.views.decorators import ( + set_permissions, requires_permission_class) +from pootle_app.models.directory import Directory +from pootle_app.views.admin import ProjectGenericAdminView + + +__all__ = ('ProjectUserView',) + + +class ProjectUserView(ProjectGenericAdminView): + template_name = 'projects/user/projects.html' + page_code = 'user-projects' + + @property + def permission_context(self): + return Directory.objects.root + + @set_permissions + @requires_permission_class("add_project") + def dispatch(self, request, *args, **kwargs): + return super(ProjectUserView, self).dispatch(request, *args, **kwargs) diff --git a/pootle/apps/pootle_app/views/user/urls.py b/pootle/apps/pootle_app/views/user/urls.py new file mode 100644 index 00000000000..2db76e70634 --- /dev/null +++ b/pootle/apps/pootle_app/views/user/urls.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from django.conf.urls import url + +from . import ProjectUserView, ProjectAPIView + + +urlpatterns = [ + url(r'^projects/$', + ProjectUserView.as_view(), + name='pootle-user-projects'), + url(r'^projects/(?P[0-9]*)/?$', + ProjectUserView.as_view(), + name='pootle-user-project-edit'), +] + + +api_patterns = [ + url(r'^projects/?$', + ProjectAPIView.as_view(), + name='pootle-xhr-user-projects'), + url(r'^projects/(?P[0-9]*)/?$', + ProjectAPIView.as_view(), + name='pootle-xhr-user-project'), +] From 580d92b3e52c758282da1b7ba574bb5bd7063634 Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:50:09 +0200 Subject: [PATCH 05/11] Adds templates for creation and edition. --- pootle/templates/projects/admin/project.html | 36 ++++++++++++++++++++ pootle/templates/projects/user/projects.html | 34 ++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 pootle/templates/projects/admin/project.html create mode 100644 pootle/templates/projects/user/projects.html diff --git a/pootle/templates/projects/admin/project.html b/pootle/templates/projects/admin/project.html new file mode 100644 index 00000000000..d916f76e094 --- /dev/null +++ b/pootle/templates/projects/admin/project.html @@ -0,0 +1,36 @@ +{% extends "projects/base.html" %} + +{% load i18n locale %} +{% load assets %} + +{% block css %} +{{ block.super }} +{% assets "css_editor" %} + +{% endassets %} +{% endblock %} + +{% block title %}{{ page_project_title }} | {{ block.super }}{% endblock %} + +{% block content %} +
+

{{ page_project_description }}

+
+ {% csrf_token %} + {% if error_msg %} +
{% trans error_msg %}
+ {% endif %} + {% if messages %} +
    + {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + {{ form.as_p }} +

+ +

+
+
+{% endblock %} \ No newline at end of file diff --git a/pootle/templates/projects/user/projects.html b/pootle/templates/projects/user/projects.html new file mode 100644 index 00000000000..939065fa48f --- /dev/null +++ b/pootle/templates/projects/user/projects.html @@ -0,0 +1,34 @@ +{% extends "projects/admin/base.html" %} + +{% load core i18n assets %} + +{% get_current_language as LANGUAGE_CODE %} + +{% block title %}{% trans "Projects" %} | {{ block.super }}{% endblock %} + +{% block scripts_extra %} +{{ block.super }} +{% assets 'js_admin_app' %} + + +{% endassets %} +{% endblock %} + +{% block content %} + +{% block breadcrumbs %} +{{ block.super }}
  • {% trans "Projects" %}
  • +{% endblock %} +
    +

    {% block section_description %}{% endblock %}

    + +
    +
    + +{% endblock %} From 4c0c5170d69930cda814ac13ca6ea145f02a5a0e Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:50:36 +0200 Subject: [PATCH 06/11] Adds urls for linking to project edition views. --- pootle/apps/pootle_app/urls.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pootle/apps/pootle_app/urls.py b/pootle/apps/pootle_app/urls.py index f9ec82f5ea3..10fa543a166 100644 --- a/pootle/apps/pootle_app/urls.py +++ b/pootle/apps/pootle_app/urls.py @@ -9,6 +9,7 @@ from django.conf.urls import include, url from .views.admin import urls as admin_urls +from .views.user import urls as user_urls urlpatterns = [ @@ -16,6 +17,10 @@ include(admin_urls)), url(r'^xhr/admin/', include(admin_urls.api_patterns)), + url(r'^my/', + include(user_urls)), + url(r'^xhr/my/', + include(user_urls.api_patterns)), url(r'', include('pootle_app.views.index.urls')), ] From baf1c1145aaab9c5803d92f511d9c0c8800f551f Mon Sep 17 00:00:00 2001 From: giliam Date: Tue, 27 Jun 2017 15:51:12 +0200 Subject: [PATCH 07/11] Updates _nav and layout templates. Adds context processor to access setting. --- pootle/apps/pootle_misc/context_processors.py | 2 ++ pootle/templates/layout.html | 3 +++ pootle/templates/projects/_nav.html | 9 +++++++++ pootle/templates/translation_projects/_nav.html | 9 +++++++++ 4 files changed, 23 insertions(+) diff --git a/pootle/apps/pootle_misc/context_processors.py b/pootle/apps/pootle_misc/context_processors.py index ebca212b70d..8733547f6d1 100644 --- a/pootle/apps/pootle_misc/context_processors.py +++ b/pootle/apps/pootle_misc/context_processors.py @@ -51,6 +51,8 @@ def pootle_context(request): 'POOTLE_SIGNUP_ENABLED': settings.POOTLE_SIGNUP_ENABLED, 'SCRIPT_NAME': settings.SCRIPT_NAME, 'POOTLE_CACHE_TIMEOUT': settings.POOTLE_CACHE_TIMEOUT, + 'POOTLE_PROJECTADMIN_CAN_EDITPROJECTS': + settings.POOTLE_PROJECTADMIN_CAN_EDITPROJECTS, 'DEBUG': settings.DEBUG, }, 'custom': settings.POOTLE_CUSTOM_TEMPLATE_CONTEXT, diff --git a/pootle/templates/layout.html b/pootle/templates/layout.html index 0122151b77b..495d36f33a7 100644 --- a/pootle/templates/layout.html +++ b/pootle/templates/layout.html @@ -130,6 +130,9 @@