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')), ] 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/permissions.py b/pootle/apps/pootle_app/views/admin/permissions.py index 3060aeb3368..b5abfb32b7f 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', 'create_project' + ], 'negative': ['hide'], } diff --git a/pootle/apps/pootle_app/views/admin/projects.py b/pootle/apps/pootle_app/views/admin/projects.py index 1b2868113dd..3e21fbd5f03 100644 --- a/pootle/apps/pootle_app/views/admin/projects.py +++ b/pootle/apps/pootle_app/views/admin/projects.py @@ -6,12 +6,21 @@ # 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 (requires_permission_class, + set_permissions) 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 (PermissionSet, + check_user_permission, + get_pootle_permission) from pootle_language.models import Language from pootle_project.models import PROJECT_CHECKERS, Project @@ -19,8 +28,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 +52,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 +65,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("create_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) 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..598f69ce912 --- /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 (requires_permission_class, + set_permissions) +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("create_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..52e89c4e6e6 --- /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 ProjectAPIView, ProjectUserView + + +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'), +] 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/apps/pootle_project/migrations/0017_add_permission_add_project.py b/pootle/apps/pootle_project/migrations/0017_add_permission_add_project.py new file mode 100644 index 00000000000..735830e211d --- /dev/null +++ b/pootle/apps/pootle_project/migrations/0017_add_permission_add_project.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-10-06 13:51 +from __future__ import unicode_literals + +from django.contrib.contenttypes.management import update_contenttypes +from django.db import migrations + +def create_permission_add_project(apps, schema_editor): + update_contenttypes(apps.app_configs['pootle_project']) + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + try: + pootle_project_model = ContentType.objects.get( + app_label="pootle_app", + model="directory" + ) + try: + add_project_permission = Permission.objects.get( + codename="create_project", + content_type=pootle_project_model + ) + except Permission.DoesNotExist: + pootle_project_model = ContentType.objects.get( + app_label="pootle_app", + model="directory" + ) + add_project_permission = Permission( + content_type=pootle_project_model, + name="Can create a project", + codename="create_project" + ) + add_project_permission.save() + except ContentType.DoesNotExist: + # this means the content types has not been + # created yet by the migration tool, thus the migration + # is starting from beginning (?). That's why, this migration + # is not required and the permission will be created when + # populating the database with initdb script. + pass + + + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0001_initial'), + ('auth', '0001_initial'), + ('pootle_project', '0016_change_treestyle_choices_label'), + ] + + operations = [ + migrations.RunPython(create_permission_add_project), + ] diff --git a/pootle/apps/pootle_project/templatetags/__init__.py b/pootle/apps/pootle_project/templatetags/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pootle/apps/pootle_project/templatetags/check_permissions.py b/pootle/apps/pootle_project/templatetags/check_permissions.py new file mode 100644 index 00000000000..15c5301e3c6 --- /dev/null +++ b/pootle/apps/pootle_project/templatetags/check_permissions.py @@ -0,0 +1,24 @@ +# -*- 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 import template + +from pootle.core.views.decorators import check_directory_permission +from pootle_app.models import Directory + + +register = template.Library() + + +@register.filter('can_create_project') +def can_create_project(request): + return check_directory_permission( + "create_project", + request, + Directory.objects.root + ) diff --git a/pootle/core/initdb.py b/pootle/core/initdb.py index c5266b27a3f..f9f92450934 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 create a project"), + 'codename': "create_project", + }, ] criteria = { diff --git a/pootle/core/views/decorators.py b/pootle/core/views/decorators.py index c5a7b86ae71..e5824f7bba2 100644 --- a/pootle/core/views/decorators.py +++ b/pootle/core/views/decorators.py @@ -11,7 +11,8 @@ 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 (check_permission, + get_matching_permissions) def check_directory_permission(permission_codename, request, directory): @@ -71,3 +72,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 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/pootle/templates/layout.html b/pootle/templates/layout.html index 0122151b77b..162c23bc03c 100644 --- a/pootle/templates/layout.html +++ b/pootle/templates/layout.html @@ -1,4 +1,4 @@ -{% load core i18n assets locale profile_tags static statici18n %} +{% load check_permissions core i18n assets locale profile_tags static statici18n %} {% get_current_language as LANGUAGE_CODE %} @@ -130,6 +130,9 @@