diff --git a/CHANGES.rst b/CHANGES.rst index 77709695b9..611ad5fa58 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,10 @@ Changelog for onadata 1.13.3 (Unreleased) -------------------- -- Nothing changed yet +- Add check if user has permission to add a project to a profile + `Issue 1385 `_ + [ukanga] + 1.13.2 (2018-04-11) -------------------- diff --git a/onadata/apps/api/management/commands/apply_can_add_project_perms.py b/onadata/apps/api/management/commands/apply_can_add_project_perms.py new file mode 100644 index 0000000000..b351c3fdca --- /dev/null +++ b/onadata/apps/api/management/commands/apply_can_add_project_perms.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +Command apply_can_add_project_perms - applys can_add_project permission to all +users who have can_add_xform permission to a user/organization profile. + +This was necessary because we previously (April 2018) did not have +can_add_project permission on the UserProfile and OrganizationProfile classes. +An attempt on doing this in migrations seems not to be a recommended approach. +""" +from django.core.management.base import BaseCommand +from django.utils.translation import gettext as _ +from guardian.shortcuts import assign_perm + +from onadata.apps.api.models import OrganizationProfile +from onadata.apps.main.models import UserProfile + + +def org_can_add_project_permission(): + """ + Set 'can_add_project' permission to all users who have 'can_add_xform' + permission in the organization profile. + """ + organizations = OrganizationProfile.objects.all() + + for organization in organizations.iterator(): + permissions = organization.orgprofileuserobjectpermission_set.filter( + permission__codename='can_add_xform') + for permission in permissions: + assign_perm('can_add_project', permission.user, organization) + + +def user_can_add_project_permission(): + """ + Set 'can_add_project' permission to all users who have 'can_add_xform' + permission in the user profile. + """ + users = UserProfile.objects.all() + + for user in users.iterator(): + permissions = user.userprofileuserobjectpermission_set.filter( + permission__codename='can_add_xform') + for permission in permissions: + assign_perm('can_add_project', permission.user, user) + + +class Command(BaseCommand): + """ + Command apply_can_add_preject_perms - applys can_add_project permission to + all users who have can_add_xform permission to a user/organization profile. + """ + help = _(u"Apply can_add_project permissions") + + def handle(self, *args, **options): + user_can_add_project_permission() + org_can_add_project_permission() diff --git a/onadata/apps/api/migrations/0003_auto_20180425_0754.py b/onadata/apps/api/migrations/0003_auto_20180425_0754.py new file mode 100644 index 0000000000..3835d5ac5f --- /dev/null +++ b/onadata/apps/api/migrations/0003_auto_20180425_0754.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-04-25 11:54 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_auto_20151014_0909'), + ] + + operations = [ + migrations.AlterModelOptions( + name='organizationprofile', + options={ + 'permissions': + (('can_add_project', 'Can add a project to an organization'), + ('can_add_xform', + 'Can add/upload an xform to an organization'), + ('view_organizationprofile', 'Can view organization profile')) + }, ), + ] diff --git a/onadata/apps/api/models/organization_profile.py b/onadata/apps/api/models/organization_profile.py index 0a58173f86..922d78047a 100644 --- a/onadata/apps/api/models/organization_profile.py +++ b/onadata/apps/api/models/organization_profile.py @@ -1,24 +1,36 @@ -from django.db import models -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +OrganizationProfile module. +""" +from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType -from django.db.models.signals import post_save, post_delete -from guardian.shortcuts import get_perms_for_model, assign_perm -from guardian.models import UserObjectPermissionBase -from guardian.models import GroupObjectPermissionBase +from django.db import models +from django.db.models.signals import post_delete, post_save +from django.utils.encoding import python_2_unicode_compatible + +from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase +from guardian.shortcuts import assign_perm, get_perms_for_model from onadata.apps.api.models.team import Team from onadata.apps.main.models import UserProfile -from onadata.libs.utils.cache_tools import safe_delete, IS_ORG +from onadata.libs.utils.cache_tools import IS_ORG, safe_delete +# pylint: disable=invalid-name,unused-argument def org_profile_post_delete_callback(sender, instance, **kwargs): + """ + Signal handler to delete the organization user object. + """ # delete the org_user too instance.user.delete() safe_delete('{}{}'.format(IS_ORG, instance.pk)) def create_owner_team_and_permissions(sender, instance, created, **kwargs): + """ + Signal handler that creates the Owner team and assigns group and user + permissions. + """ if created: team = Team.objects.create( name=Team.OWNER_TEAM_NAME, organization=instance.user, @@ -37,10 +49,11 @@ def create_owner_team_and_permissions(sender, instance, created, **kwargs): if instance.creator: assign_perm(perm.codename, instance.creator, instance) - if instance.created_by: + if instance.created_by and instance.created_by != instance.creator: assign_perm(perm.codename, instance.created_by, instance) +@python_2_unicode_compatible class OrganizationProfile(UserProfile): """Organization: Extends the user profile for organization specific info @@ -57,7 +70,8 @@ class OrganizationProfile(UserProfile): class Meta: app_label = 'api' permissions = ( - ('can_add_xform', "Can add/upload an xform to organization"), + ('can_add_project', "Can add a project to an organization"), + ('can_add_xform', "Can add/upload an xform to an organization"), ('view_organizationprofile', "Can view organization profile"), ) @@ -65,7 +79,10 @@ class Meta: # Other fields here creator = models.ForeignKey(User) - def save(self, *args, **kwargs): + def __str__(self): + return u'%s[%s]' % (self.name, self.user.username) + + def save(self, *args, **kwargs): # pylint: disable=arguments-differ super(OrganizationProfile, self).save(*args, **kwargs) def remove_user_from_organization(self, user): @@ -97,6 +114,7 @@ def is_organization_owner(self, user): dispatch_uid='org_profile_post_delete_callback') +# pylint: disable=model-no-explicit-unicode class OrgProfileUserObjectPermission(UserObjectPermissionBase): """Guardian model to create direct foreign keys.""" content_object = models.ForeignKey(OrganizationProfile) diff --git a/onadata/apps/api/tests/viewsets/test_project_viewset.py b/onadata/apps/api/tests/viewsets/test_project_viewset.py index 2267be2e97..94407ae26e 100644 --- a/onadata/apps/api/tests/viewsets/test_project_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_project_viewset.py @@ -60,7 +60,7 @@ def get_latest_tags(project): class TestProjectViewSet(TestAbstractViewSet): def setUp(self): - super(self.__class__, self).setUp() + super(TestProjectViewSet, self).setUp() self.view = ProjectViewSet.as_view({ 'get': 'list', 'post': 'create' @@ -244,6 +244,44 @@ def test_projects_create(self): self.assertEqual(self.user, project.created_by) self.assertEqual(self.user, project.organization) + def test_project_create_other_account(self): # pylint: disable=C0103 + """ + Test that a user cannot create a project in a different user account + without the right permission. + """ + alice_data = {'username': 'alice', 'email': 'alice@localhost.com'} + alice_profile = self._create_user_profile(alice_data) + bob = self.user + self._login_user_and_profile(alice_data) + data = { + "name": "Example Project", + "owner": "http://testserver/api/v1/users/bob", # Bob + } + + # Alice should not be able to create the form in bobs account. + request = self.factory.post('/projects', data=data, **self.extra) + response = self.view(request) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data, { + 'owner': [u'You do not have permmission to create a project in ' + 'this organization.']}) + self.assertEqual(Project.objects.count(), 0) + + # Give Alice the permission to create a project in Bob's account. + ManagerRole.add(alice_profile.user, bob.profile) + request = self.factory.post('/projects', data=data, **self.extra) + response = self.view(request) + self.assertEqual(response.status_code, 201) + + projects = Project.objects.all() + self.assertEqual(len(projects), 1) + + for project in projects: + # Created by Alice + self.assertEqual(alice_profile.user, project.created_by) + # But under Bob's account + self.assertEqual(bob, project.organization) + def test_create_duplicate_project(self): """ Test creating a project with the same name @@ -1495,8 +1533,16 @@ def test_move_project_owner(self): request = self.factory.patch('/', data=data_patch, **self.extra) response = view(request, pk=projectid) - self.project.refresh_from_db() + # bob cannot move project if he does not have can_add_project project + # permission on alice's account.c + self.assertEqual(response.status_code, 400) + + # Give bob permission. + ManagerRole.add(self.user, alice_profile) + request = self.factory.patch('/', data=data_patch, **self.extra) + response = view(request, pk=projectid) self.assertEqual(response.status_code, 200) + self.project.refresh_from_db() self.assertEquals(self.project.organization, alice) self.assertTrue(OwnerRole.user_has_role(alice, self.project)) diff --git a/onadata/apps/main/migrations/0008_auto_20180425_0754.py b/onadata/apps/main/migrations/0008_auto_20180425_0754.py new file mode 100644 index 0000000000..a782a2e899 --- /dev/null +++ b/onadata/apps/main/migrations/0008_auto_20180425_0754.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-04-25 11:54 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0007_auto_20160418_0525'), + ] + + operations = [ + migrations.AlterModelOptions( + name='userprofile', + options={ + 'permissions': + (('can_add_project', 'Can add a project to an organization'), + ('can_add_xform', 'Can add/upload an xform to user profile'), + ('view_profile', 'Can view user profile')) + }, ), + ] diff --git a/onadata/apps/main/models/user_profile.py b/onadata/apps/main/models/user_profile.py index 173eee13e7..53ebd2deff 100644 --- a/onadata/apps/main/models/user_profile.py +++ b/onadata/apps/main/models/user_profile.py @@ -78,6 +78,7 @@ def save(self, force_insert=False, force_update=False, using=None, class Meta: app_label = 'main' permissions = ( + ('can_add_project', "Can add a project to an organization"), ('can_add_xform', "Can add/upload an xform to user profile"), ('view_profile', "Can view user profile"), ) diff --git a/onadata/libs/permissions.py b/onadata/libs/permissions.py index 897b3aa07b..14fc8a6374 100644 --- a/onadata/libs/permissions.py +++ b/onadata/libs/permissions.py @@ -25,12 +25,14 @@ CAN_ADD_USERPROFILE = 'add_userprofile' CAN_CHANGE_USERPROFILE = 'change_userprofile' CAN_DELETE_USERPROFILE = 'delete_userprofile' +CAN_ADD_PROJECT_TO_PROFILE = 'can_add_project' CAN_ADD_XFORM_TO_PROFILE = 'can_add_xform' CAN_VIEW_PROFILE = 'view_profile' # Organization Permissions CAN_VIEW_ORGANIZATION_PROFILE = 'view_organizationprofile' CAN_ADD_ORGANIZATION_PROFILE = 'add_organizationprofile' +CAN_ADD_ORGANIZATION_PROJECT = 'can_add_project' CAN_ADD_ORGANIZATION_XFORM = 'can_add_xform' CAN_CHANGE_ORGANIZATION_PROFILE = 'change_organizationprofile' CAN_DELETE_ORGANIZATION_PROFILE = 'delete_organizationprofile' @@ -263,14 +265,16 @@ class ManagerRole(Role): class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: - [CAN_ADD_XFORM_TO_PROFILE, CAN_VIEW_ORGANIZATION_PROFILE], + [CAN_ADD_ORGANIZATION_PROJECT, CAN_ADD_ORGANIZATION_XFORM, + CAN_VIEW_ORGANIZATION_PROFILE], Project: [ CAN_ADD_PROJECT, CAN_ADD_PROJECT_XFORM, CAN_ADD_SUBMISSIONS_PROJECT, CAN_CHANGE_PROJECT, CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT, CAN_VIEW_PROJECT_ALL, CAN_VIEW_PROJECT_DATA ], - UserProfile: [CAN_ADD_XFORM_TO_PROFILE, CAN_VIEW_PROFILE], + UserProfile: [CAN_ADD_PROJECT_TO_PROFILE, CAN_ADD_XFORM_TO_PROFILE, + CAN_VIEW_PROFILE], XForm: [ CAN_ADD_SUBMISSIONS, CAN_ADD_XFORM, CAN_CHANGE_XFORM, CAN_DELETE_SUBMISSION, CAN_DELETE_XFORM, CAN_EXPORT_XFORM, @@ -298,7 +302,8 @@ class OwnerRole(Role): ], MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [ - CAN_ADD_XFORM_TO_PROFILE, CAN_ADD_ORGANIZATION_PROFILE, + CAN_ADD_ORGANIZATION_PROJECT, CAN_ADD_ORGANIZATION_XFORM, + CAN_ADD_ORGANIZATION_PROFILE, CAN_ADD_ORGANIZATION_PROJECT, CAN_ADD_ORGANIZATION_XFORM, CAN_CHANGE_ORGANIZATION_PROFILE, CAN_DELETE_ORGANIZATION_PROFILE, CAN_VIEW_ORGANIZATION_PROFILE, IS_ORGANIZATION_OWNER @@ -311,8 +316,9 @@ class OwnerRole(Role): CAN_VIEW_PROJECT_ALL, CAN_VIEW_PROJECT_DATA ], UserProfile: [ - CAN_ADD_XFORM_TO_PROFILE, CAN_ADD_USERPROFILE, - CAN_CHANGE_USERPROFILE, CAN_DELETE_USERPROFILE, CAN_VIEW_PROFILE + CAN_ADD_PROJECT_TO_PROFILE, CAN_ADD_XFORM_TO_PROFILE, + CAN_ADD_USERPROFILE, CAN_CHANGE_USERPROFILE, + CAN_DELETE_USERPROFILE, CAN_VIEW_PROFILE ], XForm: [ CAN_ADD_SUBMISSIONS, CAN_ADD_XFORM, CAN_CHANGE_XFORM, diff --git a/onadata/libs/serializers/project_serializer.py b/onadata/libs/serializers/project_serializer.py index 32ecb87853..04f29e7ac4 100644 --- a/onadata/libs/serializers/project_serializer.py +++ b/onadata/libs/serializers/project_serializer.py @@ -1,124 +1,144 @@ +# -*- coding: utf-8 -*- +""" +Project Serializer module. +""" from future.utils import listvalues -from rest_framework import serializers -from django.db.utils import IntegrityError from django.conf import settings from django.contrib.auth.models import User from django.core.cache import cache +from django.db.utils import IntegrityError from django.utils.translation import ugettext as _ -from onadata.apps.logger.models import Project -from onadata.apps.logger.models import XForm -from onadata.libs.permissions import OwnerRole -from onadata.libs.permissions import ReadOnlyRole -from onadata.libs.permissions import is_organization -from onadata.libs.permissions import get_role +from rest_framework import serializers + +from onadata.apps.api.models import OrganizationProfile +from onadata.apps.api.tools import (get_organization_members_team, + get_organization_owners_team) +from onadata.apps.logger.models import Project, XForm +from onadata.libs.permissions import (OwnerRole, ReadOnlyRole, get_role, + is_organization) +from onadata.libs.serializers.dataview_serializer import \ + DataViewMinimalSerializer from onadata.libs.serializers.fields.json_field import JsonField from onadata.libs.serializers.tag_list_serializer import TagListSerializer -from onadata.libs.serializers.dataview_serializer import ( - DataViewMinimalSerializer -) -from onadata.libs.utils.decorators import check_obj from onadata.libs.utils.cache_tools import ( - PROJ_FORMS_CACHE, PROJ_NUM_DATASET_CACHE, PROJ_PERM_CACHE, - PROJ_SUB_DATE_CACHE, safe_delete, PROJ_TEAM_USERS_CACHE, - PROJECT_LINKED_DATAVIEWS, PROJ_BASE_FORMS_CACHE) -from onadata.apps.api.tools import ( - get_organization_members_team, get_organization_owners_team) + PROJ_BASE_FORMS_CACHE, PROJ_FORMS_CACHE, PROJ_NUM_DATASET_CACHE, + PROJ_PERM_CACHE, PROJ_SUB_DATE_CACHE, PROJ_TEAM_USERS_CACHE, + PROJECT_LINKED_DATAVIEWS, safe_delete) +from onadata.libs.utils.decorators import check_obj -def get_obj_xforms(obj): - return obj.xforms_prefetch if hasattr(obj, 'xforms_prefetch') else\ - obj.xform_set.filter(deleted_at__isnull=True) +def get_project_xforms(project): + """ + Returns an XForm queryset from project. The prefetched + `xforms_prefetch` or `xform_set.filter()` queryset. + """ + return (project.xforms_prefetch if hasattr(project, 'xforms_prefetch') else + project.xform_set.filter(deleted_at__isnull=True)) @check_obj -def get_last_submission_date(obj): +def get_last_submission_date(project): """Return the most recent submission date to any of the projects datasets. - :param obj: The project to find the last submission date for. + :param project: The project to find the last submission date for. """ - last_submission_date = cache.get('{}{}'.format( - PROJ_SUB_DATE_CACHE, obj.pk)) + last_submission_date = cache.get( + '{}{}'.format(PROJ_SUB_DATE_CACHE, project.pk)) if last_submission_date: return last_submission_date - xforms = get_obj_xforms(obj) - dates = [x.last_submission_time for x in xforms - if x.last_submission_time is not None] + xforms = get_project_xforms(project) + dates = [ + x.last_submission_time for x in xforms + if x.last_submission_time is not None + ] dates.sort(reverse=True) - last_submission_date = dates[0] if len(dates) else None + last_submission_date = dates[0] if dates else None - cache.set('{}{}'.format(PROJ_SUB_DATE_CACHE, obj.pk), + cache.set('{}{}'.format(PROJ_SUB_DATE_CACHE, project.pk), last_submission_date) return last_submission_date @check_obj -def get_num_datasets(obj): - """Return the number of datasets attached to the object. +def get_num_datasets(project): + """Return the number of datasets attached to the project. - :param obj: The project to find datasets for. + :param project: The project to find datasets for. """ - count = cache.get('{}{}'.format(PROJ_NUM_DATASET_CACHE, obj.pk)) + count = cache.get('{}{}'.format(PROJ_NUM_DATASET_CACHE, project.pk)) if count: return count - count = len(get_obj_xforms(obj)) - cache.set('{}{}'.format(PROJ_NUM_DATASET_CACHE, obj.pk), count) + count = len(get_project_xforms(project)) + cache.set('{}{}'.format(PROJ_NUM_DATASET_CACHE, project.pk), count) return count -def get_starred(obj, request): - return obj.user_stars.filter(pk=request.user.pk).count() == 1 +def is_starred(project, request): + """ + Return True if the request.user has starred this project. + """ + return project.user_stars.filter(pk=request.user.pk).count() == 1 -def get_team_permissions(team, obj): - return obj.projectgroupobjectpermission_set.filter( - group__pk=team.pk).values_list('permission__codename', flat=True) +def get_team_permissions(team, project): + """ + Return team permissions. + """ + return project.projectgroupobjectpermission_set.filter( + group__pk=team.pk).values_list( + 'permission__codename', flat=True) @check_obj -def get_teams(obj): - teams_users = cache.get('{}{}'.format( - PROJ_TEAM_USERS_CACHE, obj.pk)) +def get_teams(project): + """ + Return the teams with access to the project. + """ + teams_users = cache.get('{}{}'.format(PROJ_TEAM_USERS_CACHE, project.pk)) if teams_users: return teams_users teams_users = [] - teams = obj.organization.team_set.all() + teams = project.organization.team_set.all() for team in teams: # to take advantage of prefetch iterate over user set users = [user.username for user in team.user_set.all()] - perms = get_team_permissions(team, obj) + perms = get_team_permissions(team, project) teams_users.append({ "name": team.name, - "role": get_role(perms, obj), + "role": get_role(perms, project), "users": users }) - cache.set('{}{}'.format(PROJ_TEAM_USERS_CACHE, obj.pk), - teams_users) + cache.set('{}{}'.format(PROJ_TEAM_USERS_CACHE, project.pk), teams_users) return teams_users @check_obj -def get_users(obj, context, all_perms=True): +def get_users(project, context, all_perms=True): + """ + Return a list of users and organizations that have access to the project. + """ if all_perms: - users = cache.get('{}{}'.format(PROJ_PERM_CACHE, obj.pk)) + users = cache.get('{}{}'.format(PROJ_PERM_CACHE, project.pk)) if users: return users data = {} - for perm in obj.projectuserobjectpermission_set.all(): + for perm in project.projectuserobjectpermission_set.all(): if perm.user_id not in data: user = perm.user - if all_perms or user in [context['request'].user, - obj.organization]: + if all_perms or user in [ + context['request'].user, project.organization + ]: data[perm.user_id] = { 'permissions': [], 'is_org': is_organization(user.profile), @@ -132,13 +152,13 @@ def get_users(obj, context, all_perms=True): for k in list(data): data[k]['permissions'].sort() - data[k]['role'] = get_role(data[k]['permissions'], obj) - del(data[k]['permissions']) + data[k]['role'] = get_role(data[k]['permissions'], project) + del data[k]['permissions'] results = listvalues(data) if all_perms: - cache.set('{}{}'.format(PROJ_PERM_CACHE, obj.pk), results) + cache.set('{}{}'.format(PROJ_PERM_CACHE, project.pk), results) return results @@ -149,6 +169,9 @@ def set_owners_permission(user, project): class BaseProjectXFormSerializer(serializers.HyperlinkedModelSerializer): + """ + BaseProjectXFormSerializer class. + """ formid = serializers.ReadOnlyField(source='id') name = serializers.ReadOnlyField(source='title') @@ -158,55 +181,47 @@ class Meta: class ProjectXFormSerializer(serializers.HyperlinkedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='xform-detail', - lookup_field='pk') + """ + ProjectXFormSerializer class - to return project xform info. + """ + url = serializers.HyperlinkedIdentityField( + view_name='xform-detail', lookup_field='pk') formid = serializers.ReadOnlyField(source='id') name = serializers.ReadOnlyField(source='title') published_by_formbuilder = serializers.SerializerMethodField() class Meta: model = XForm - fields = ( - 'name', - 'formid', - 'id_string', - 'num_of_submissions', - 'downloadable', - 'encrypted', - 'published_by_formbuilder', - 'last_submission_time', - 'date_created', - 'url', - 'last_updated_at', - 'is_merged_dataset', - ) - - def get_published_by_formbuilder(self, obj): - md = obj.metadata_set.filter( - data_type='published_by_formbuilder' - ).first() - if md and hasattr(md, 'data_value') and md.data_value: - return True + fields = ('name', 'formid', 'id_string', 'num_of_submissions', + 'downloadable', 'encrypted', 'published_by_formbuilder', + 'last_submission_time', 'date_created', 'url', + 'last_updated_at', 'is_merged_dataset', ) - return False + def get_published_by_formbuilder(self, obj): # pylint: disable=no-self-use + """ + Returns true if the form was published by formbuilder. + """ + metadata = obj.metadata_set.filter( + data_type='published_by_formbuilder').first() + return (metadata and hasattr(metadata, 'data_value') + and metadata.data_value) class BaseProjectSerializer(serializers.HyperlinkedModelSerializer): + """ + BaseProjectSerializer class. + """ projectid = serializers.ReadOnlyField(source='id') url = serializers.HyperlinkedIdentityField( view_name='project-detail', lookup_field='pk') owner = serializers.HyperlinkedRelatedField( - view_name='user-detail', source='organization', + view_name='user-detail', + source='organization', lookup_field='username', queryset=User.objects.exclude( - username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME - ) - ) + username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME)) created_by = serializers.HyperlinkedRelatedField( - view_name='user-detail', - lookup_field='username', - read_only=True - ) + view_name='user-detail', lookup_field='username', read_only=True) metadata = JsonField(required=False) starred = serializers.SerializerMethodField() users = serializers.SerializerMethodField() @@ -219,63 +234,94 @@ class BaseProjectSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Project - fields = ['url', 'projectid', 'owner', 'created_by', 'metadata', - 'starred', 'users', 'forms', 'public', 'tags', - 'num_datasets', 'last_submission_date', 'teams', 'name', - 'date_created', 'date_modified', 'deleted_at'] + fields = [ + 'url', 'projectid', 'owner', 'created_by', 'metadata', 'starred', + 'users', 'forms', 'public', 'tags', 'num_datasets', + 'last_submission_date', 'teams', 'name', 'date_created', + 'date_modified', 'deleted_at' + ] def get_starred(self, obj): - return get_starred(obj, self.context['request']) + """ + Return True if request user has starred this project. + """ + return is_starred(obj, self.context['request']) def get_users(self, obj): + """ + Return a list of users and organizations that have access to the + project. + """ owner_query_param_in_request = 'request' in self.context and\ "owner" in self.context['request'].GET - return get_users(obj, - self.context, - owner_query_param_in_request) + return get_users(obj, self.context, owner_query_param_in_request) @check_obj def get_forms(self, obj): + """ + Return list of xforms in the project. + """ forms = cache.get('{}{}'.format(PROJ_BASE_FORMS_CACHE, obj.pk)) if forms: return forms - xforms = get_obj_xforms(obj) + xforms = get_project_xforms(obj) request = self.context.get('request') serializer = BaseProjectXFormSerializer( - xforms, context={'request': request}, many=True - ) + xforms, context={'request': request}, many=True) forms = list(serializer.data) cache.set('{}{}'.format(PROJ_BASE_FORMS_CACHE, obj.pk), forms) return forms - def get_num_datasets(self, obj): + def get_num_datasets(self, obj): # pylint: disable=no-self-use + """ + Return the number of datasets attached to the project. + """ return get_num_datasets(obj) - def get_last_submission_date(self, obj): + def get_last_submission_date(self, obj): # pylint: disable=no-self-use + """ + Return the most recent submission date to any of the projects datasets. + """ return get_last_submission_date(obj) - def get_teams(self, obj): + def get_teams(self, obj): # pylint: disable=no-self-use + """ + Return the teams with access to the project. + """ return get_teams(obj) +def can_add_project_to_profile(user, organization): + """ + Check if user has permission to add a project to a profile. + """ + perms = 'can_add_project' + if user != organization and \ + not user.has_perm(perms, organization.profile) and \ + not user.has_perm( + perms, OrganizationProfile.objects.get(user=organization)): + return False + + return True + + class ProjectSerializer(serializers.HyperlinkedModelSerializer): + """ + ProjectSerializer class - creates and updates a project. + """ projectid = serializers.ReadOnlyField(source='id') url = serializers.HyperlinkedIdentityField( view_name='project-detail', lookup_field='pk') owner = serializers.HyperlinkedRelatedField( - view_name='user-detail', source='organization', + view_name='user-detail', + source='organization', lookup_field='username', queryset=User.objects.exclude( - username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME - ) - ) + username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME)) created_by = serializers.HyperlinkedRelatedField( - view_name='user-detail', - lookup_field='username', - read_only=True - ) + view_name='user-detail', lookup_field='username', read_only=True) metadata = JsonField(required=False) starred = serializers.SerializerMethodField() users = serializers.SerializerMethodField() @@ -294,15 +340,33 @@ class Meta: def validate(self, attrs): name = attrs.get('name') organization = attrs.get('organization') - if not self.instance and \ - Project.objects.filter(name__iexact=name, - organization=organization): + project_w_same_name = Project.objects.filter( # pylint: disable=E1101 + name__iexact=name, + organization=organization) + if not self.instance and project_w_same_name: raise serializers.ValidationError({ - 'name': _(u"Project {} already exists.".format(name)) + 'name': + _(u"Project {} already exists.".format(name)) + }) + try: + has_perm = can_add_project_to_profile(self.context['request'].user, + organization) + except OrganizationProfile.DoesNotExist: + # most likely when transfering a project to an individual account + # A user does not require permissions to the user's account forms. + has_perm = False + if not has_perm: + raise serializers.ValidationError({ + 'owner': + _("You do not have permmission to create a project " + "in this organization.") }) return attrs - def validate_metadata(self, value): + def validate_metadata(self, value): # pylint: disable=no-self-use + """ + Validate metadaata is a valid JSON value. + """ msg = serializers.ValidationError(_("Invaid value for metadata")) try: json_val = JsonField.to_json(value) @@ -354,13 +418,12 @@ def create(self, validated_data): created_by = self.context['request'].user try: - project = Project.objects.create( + project = Project.objects.create( # pylint: disable=E1101 name=validated_data.get('name'), organization=validated_data.get('organization'), created_by=created_by, shared=validated_data.get('shared', False), - metadata=metadata - ) + metadata=metadata) except IntegrityError: raise serializers.ValidationError( "The fields name, organization must make a unique set.") @@ -370,40 +433,60 @@ def create(self, validated_data): return project - def get_users(self, obj): + def get_users(self, obj): # pylint: disable=no-self-use + """ + Return a list of users and organizations that have access to the + project. + """ return get_users(obj, self.context) @check_obj - def get_forms(self, obj): + def get_forms(self, obj): # pylint: disable=no-self-use + """ + Return list of xforms in the project. + """ forms = cache.get('{}{}'.format(PROJ_FORMS_CACHE, obj.pk)) if forms: return forms - xforms = get_obj_xforms(obj) + xforms = get_project_xforms(obj) request = self.context.get('request') serializer = ProjectXFormSerializer( - xforms, context={'request': request}, many=True - ) + xforms, context={'request': request}, many=True) forms = list(serializer.data) cache.set('{}{}'.format(PROJ_FORMS_CACHE, obj.pk), forms) return forms - def get_num_datasets(self, obj): + def get_num_datasets(self, obj): # pylint: disable=no-self-use + """ + Return the number of datasets attached to the project. + """ return get_num_datasets(obj) - def get_last_submission_date(self, obj): + def get_last_submission_date(self, obj): # pylint: disable=no-self-use + """ + Return the most recent submission date to any of the projects datasets. + """ return get_last_submission_date(obj) - def get_starred(self, obj): - return get_starred(obj, self.context['request']) + def get_starred(self, obj): # pylint: disable=no-self-use + """ + Return True if request user has starred this project. + """ + return is_starred(obj, self.context['request']) - def get_teams(self, obj): + def get_teams(self, obj): # pylint: disable=no-self-use + """ + Return the teams with access to the project. + """ return get_teams(obj) @check_obj def get_data_views(self, obj): - data_views = cache.get( - '{}{}'.format(PROJECT_LINKED_DATAVIEWS, obj.pk)) + """ + Return a list of filtered datasets. + """ + data_views = cache.get('{}{}'.format(PROJECT_LINKED_DATAVIEWS, obj.pk)) if data_views: return data_views @@ -412,12 +495,9 @@ def get_data_views(self, obj): obj.dataview_set.filter(deleted_at__isnull=True) serializer = DataViewMinimalSerializer( - data_views_obj, - many=True, - context=self.context) + data_views_obj, many=True, context=self.context) data_views = list(serializer.data) - cache.set( - '{}{}'.format(PROJECT_LINKED_DATAVIEWS, obj.pk), data_views) + cache.set('{}{}'.format(PROJECT_LINKED_DATAVIEWS, obj.pk), data_views) return data_views