"""REST API view tests for the projectroles app"""
import base64
import json
import pytz

from django.conf import settings
from django.core import mail
from django.forms.models import model_to_dict
from django.test import override_settings
from django.urls import reverse
from django.utils import timezone

from knox.models import AuthToken

from test_plus.test import APITestCase

from projectroles import views_api
from projectroles.models import (
    Project,
    Role,
    RoleAssignment,
    ProjectInvite,
    SODAR_CONSTANTS,
)
from projectroles.plugins import change_plugin_status, get_backend_api
from projectroles.remote_projects import RemoteProjectAPI
from projectroles.tests.test_models import (
    ProjectMixin,
    RoleAssignmentMixin,
    ProjectInviteMixin,
    RemoteSiteMixin,
    RemoteProjectMixin,
    AppSettingMixin,
)
from projectroles.tests.test_views import (
    TestViewsBase,
    PROJECT_TYPE_CATEGORY,
    PROJECT_TYPE_PROJECT,
    PROJECT_ROLE_OWNER,
    PROJECT_ROLE_DELEGATE,
    PROJECT_ROLE_CONTRIBUTOR,
    PROJECT_ROLE_GUEST,
    REMOTE_SITE_NAME,
    REMOTE_SITE_URL,
    SITE_MODE_SOURCE,
    SITE_MODE_TARGET,
    REMOTE_SITE_DESC,
    REMOTE_SITE_SECRET,
)
from projectroles.utils import build_secret


CORE_API_MEDIA_TYPE_INVALID = 'application/vnd.bihealth.invalid'
CORE_API_VERSION_INVALID = '9.9.9'

INVALID_UUID = '11111111-1111-1111-1111-111111111111'
NEW_CATEGORY_TITLE = 'New Category'
NEW_PROJECT_TITLE = 'New Project'
UPDATED_TITLE = 'Updated Title'
UPDATED_DESC = 'Updated description'
UPDATED_README = 'Updated readme'

INVITE_USER_EMAIL = 'new1@example.com'
INVITE_USER2_EMAIL = 'new2@example.com'
INVITE_MESSAGE = 'Message'


# Base Classes -----------------------------------------------------------------


class SODARAPIViewTestMixin:
    """
    Mixin for SODAR and SODAR Core API views with accept headers, knox token
    authorization and general helper methods.
    """

    # Default API header parameters are for external SODAR site APIs
    # Override these for testing SODAR Core API views
    media_type = settings.SODAR_API_MEDIA_TYPE
    api_version = settings.SODAR_API_DEFAULT_VERSION

    # Copied from Knox tests
    @classmethod
    def _get_basic_auth_header(cls, username, password):
        return (
            'Basic %s'
            % base64.b64encode(
                ('%s:%s' % (username, password)).encode('ascii')
            ).decode()
        )

    @classmethod
    def get_token(cls, user, full_result=False):
        """
        Get or create a knox token for a user.

        :param user: User object
        :param full_result: Return full result of AuthToken creation if True
        :return: Token string or AuthToken creation tuple
        """
        result = AuthToken.objects.create(user=user)
        return result if full_result else result[1]

    @classmethod
    def get_serialized_user(cls, user):
        """
        Return serialization for a user.

        :param user: User object
        :return: Dict
        """
        return {
            'email': user.email,
            'name': user.name,
            'sodar_uuid': str(user.sodar_uuid),
            'username': user.username,
        }

    @classmethod
    def get_drf_datetime(cls, obj_dt):
        """
        Return datetime in DRF compatible format.

        :param obj_dt: Object DateTime field
        :return: String
        """
        return timezone.localtime(
            obj_dt, pytz.timezone(settings.TIME_ZONE)
        ).isoformat()

    @classmethod
    def get_accept_header(
        cls,
        media_type=None,
        version=None,
    ):
        """
        Return version accept header based on the media type and version string.

        :param media_type: String (default = cls.media_type)
        :param version: String (default = cls.api_version)
        :return: Dict
        """
        if not media_type:
            media_type = cls.media_type

        if not version:
            version = cls.api_version

        return {'HTTP_ACCEPT': '{}; version={}'.format(media_type, version)}

    @classmethod
    def get_token_header(cls, token):
        """
        Return auth header based on token.

        :param token: Token string
        :return: Dict
        """
        return {'HTTP_AUTHORIZATION': 'token {}'.format(token)}

    def request_knox(
        self,
        url,
        method='GET',
        format='json',
        data=None,
        token=None,
        media_type=None,
        version=None,
        header=None,
    ):
        """
        Perform a HTTP request with Knox token auth.

        :param url: URL for the request
        :param method: Request method (string, default="GET")
        :param format: Request format (string, default="json")
        :param data: Optional data for request (dict)
        :param token: Knox token string (if None, use self.knox_token)
        :param media_type: String (default = cls.media_type)
        :param version: String (default = cls.api_version)
        :return: Response object
        """
        if not token:
            token = self.knox_token

        req_kwargs = {
            'format': format,
            **self.get_accept_header(media_type, version),
            **self.get_token_header(token),
        }

        if data:
            req_kwargs['data'] = data

        if header:
            req_kwargs.update(header)

        req_method = getattr(self.client, method.lower(), None)

        if not req_method:
            raise ValueError('Unsupported method "{}"'.format(method))

        return req_method(url, **req_kwargs)


class TestAPIViewsBase(
    ProjectMixin, RoleAssignmentMixin, SODARAPIViewTestMixin, APITestCase
):
    """Base API test view with knox authentication"""

    def setUp(self):
        # Show complete diff in case of failure
        self.maxDiff = None
        # Force disabling of taskflow plugin if it's available
        if get_backend_api('taskflow'):
            change_plugin_status(
                name='taskflow', status=1, plugin_type='backend'  # 0 = Disabled
            )

        # Init roles
        self.role_owner = Role.objects.get_or_create(name=PROJECT_ROLE_OWNER)[0]
        self.role_delegate = Role.objects.get_or_create(
            name=PROJECT_ROLE_DELEGATE
        )[0]
        self.role_contributor = Role.objects.get_or_create(
            name=PROJECT_ROLE_CONTRIBUTOR
        )[0]
        self.role_guest = Role.objects.get_or_create(name=PROJECT_ROLE_GUEST)[0]

        # Init superuser
        self.user = self.make_user('superuser')
        self.user.is_staff = True
        self.user.is_superuser = True
        self.user.save()

        # Set up category and project with owner role assignments
        self.category = self._make_project(
            'TestCategory', PROJECT_TYPE_CATEGORY, None
        )
        self.cat_owner_as = self._make_assignment(
            self.category, self.user, self.role_owner
        )
        self.project = self._make_project(
            'TestProject', PROJECT_TYPE_PROJECT, self.category
        )
        self.owner_as = self._make_assignment(
            self.project, self.user, self.role_owner
        )

        # Get knox token for self.user
        self.knox_token = self.get_token(self.user)


class TestCoreAPIViewsBase(TestAPIViewsBase):
    """Override of TestAPIViewsBase to be used with SODAR Core API views"""

    media_type = views_api.CORE_API_MEDIA_TYPE
    api_version = views_api.CORE_API_DEFAULT_VERSION


# Tests ------------------------------------------------------------------------


class TestProjectListAPIView(TestCoreAPIViewsBase):
    """Tests for ProjectListAPIView"""

    def test_get(self):
        """Test ProjectListAPIView get() as project owner"""
        url = reverse('projectroles:api_project_list')
        response = self.request_knox(url)

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 2)
        expected = [
            {
                'title': self.category.title,
                'type': self.category.type,
                'parent': None,
                'description': self.category.description,
                'readme': '',
                'public_guest_access': False,
                'submit_status': self.category.submit_status,
                'roles': {
                    str(self.cat_owner_as.sodar_uuid): {
                        'user': {
                            'username': self.user.username,
                            'name': self.user.name,
                            'email': self.user.email,
                            'sodar_uuid': str(self.user.sodar_uuid),
                        },
                        'role': PROJECT_ROLE_OWNER,
                        'sodar_uuid': str(self.cat_owner_as.sodar_uuid),
                    }
                },
                'sodar_uuid': str(self.category.sodar_uuid),
            },
            {
                'title': self.project.title,
                'type': self.project.type,
                'parent': str(self.category.sodar_uuid),
                'description': self.project.description,
                'readme': '',
                'public_guest_access': False,
                'submit_status': self.project.submit_status,
                'roles': {
                    str(self.owner_as.sodar_uuid): {
                        'user': {
                            'username': self.user.username,
                            'name': self.user.name,
                            'email': self.user.email,
                            'sodar_uuid': str(self.user.sodar_uuid),
                        },
                        'role': PROJECT_ROLE_OWNER,
                        'sodar_uuid': str(self.owner_as.sodar_uuid),
                    }
                },
                'sodar_uuid': str(self.project.sodar_uuid),
            },
        ]
        self.assertEqual(response_data, expected)

    def test_get_no_roles(self):
        """Test ProjectListAPIView get() without roles"""
        user_no_roles = self.make_user('user_no_roles')
        url = reverse('projectroles:api_project_list')
        response = self.request_knox(url, token=self.get_token(user_no_roles))

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 0)

    def test_get_limited_roles(self):
        """Test ProjectListAPIView get() with only one role"""
        user_no_roles = self.make_user('user_no_roles')
        self._make_assignment(
            self.project, user_no_roles, self.role_contributor
        )
        url = reverse('projectroles:api_project_list')
        response = self.request_knox(url, token=self.get_token(user_no_roles))

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 1)


class TestProjectRetrieveAPIView(AppSettingMixin, TestCoreAPIViewsBase):
    """Tests for ProjectRetrieveAPIView"""

    def test_get_category(self):
        """Test ProjectRetrieveAPIView get() with a category"""
        url = reverse(
            'projectroles:api_project_retrieve',
            kwargs={'project': self.category.sodar_uuid},
        )
        response = self.request_knox(url)

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        expected = {
            'title': self.category.title,
            'type': self.category.type,
            'parent': None,
            'description': self.category.description,
            'readme': '',
            'public_guest_access': False,
            'submit_status': self.category.submit_status,
            'roles': {
                str(self.cat_owner_as.sodar_uuid): {
                    'user': {
                        'username': self.user.username,
                        'name': self.user.name,
                        'email': self.user.email,
                        'sodar_uuid': str(self.user.sodar_uuid),
                    },
                    'role': PROJECT_ROLE_OWNER,
                    'sodar_uuid': str(self.cat_owner_as.sodar_uuid),
                }
            },
            'sodar_uuid': str(self.category.sodar_uuid),
        }
        self.assertEqual(response_data, expected)

    def test_get_project(self):
        """Test ProjectRetrieveAPIView get() with a project"""
        url = reverse(
            'projectroles:api_project_retrieve',
            kwargs={'project': self.project.sodar_uuid},
        )
        response = self.request_knox(url)

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        expected = {
            'title': self.project.title,
            'type': self.project.type,
            'parent': str(self.category.sodar_uuid),
            'description': self.project.description,
            'readme': '',
            'public_guest_access': False,
            'submit_status': self.project.submit_status,
            'roles': {
                str(self.owner_as.sodar_uuid): {
                    'user': {
                        'username': self.user.username,
                        'name': self.user.name,
                        'email': self.user.email,
                        'sodar_uuid': str(self.user.sodar_uuid),
                    },
                    'role': PROJECT_ROLE_OWNER,
                    'sodar_uuid': str(self.owner_as.sodar_uuid),
                }
            },
            'sodar_uuid': str(self.project.sodar_uuid),
        }
        self.assertEqual(response_data, expected)

    def test_get_not_found(self):
        """Test ProjectRetrieveAPIView get() with an invalid UUID"""
        url = reverse(
            'projectroles:api_project_retrieve',
            kwargs={'project': INVALID_UUID},
        )
        response = self.request_knox(url)
        self.assertEqual(response.status_code, 404)


class TestIPAllowing(AppSettingMixin, TestCoreAPIViewsBase):
    """Tests for IP allowing settings using ProjectRetrieveAPIView"""

    def _setup_ip_allowing(self, ip_list, role_suffix):
        """Setup users and roles for IP allowing test"""
        # Create new user
        user = self.make_user(role_suffix)
        # Remove previously assigned owner role for superuser.
        superuser_as_owner_role = RoleAssignment.objects.get(
            project=self.project, user=self.user, role=self.role_owner
        )
        superuser_as_owner_role.delete()
        # Assign requested role to user
        user_as = self._make_assignment(
            self.project, user, getattr(self, 'role_' + role_suffix)
        )

        user_cat_as = RoleAssignment.objects.get(
            project=self.category, user=self.user, role=self.role_owner
        )
        if role_suffix == 'owner':
            user_cat_as.delete()
            user_cat_as = self._make_assignment(
                self.category, user, getattr(self, 'role_' + role_suffix)
            )

        # Init IP restrict setting
        self._make_setting(
            app_name='projectroles',
            name='ip_restrict',
            setting_type='BOOLEAN',
            value=True,
            project=self.project,
        )

        # Init IP allowlist setting
        self._make_setting(
            app_name='projectroles',
            name='ip_allowlist',
            setting_type='JSON',
            value=None,
            value_json=ip_list,
            project=self.project,
        )

        return user, user_as, user_cat_as

    def _get_project_ip_allowing(
        self, username, http_attribute, ip_list, blocked=None
    ):
        """Helper for IP allowing tests"""
        if blocked is None:
            raise Exception('Please set "blocked" argument (True/False)')

        user, user_as, user_cat_as = self._setup_ip_allowing(ip_list, username)
        url = reverse(
            'projectroles:api_project_retrieve',
            kwargs={'project': self.project.sodar_uuid},
        )
        header = {http_attribute: '192.168.1.1'}
        response = self.request_knox(
            url, token=self.get_token(user), header=header
        )

        if username == 'owner':
            role = PROJECT_ROLE_OWNER
        elif username == 'delegate':
            role = PROJECT_ROLE_DELEGATE
        elif username == 'contributor':
            role = PROJECT_ROLE_CONTRIBUTOR
        else:
            role = PROJECT_ROLE_GUEST

        if blocked:
            self.assertEqual(response.status_code, 403)
        else:
            expected = {
                'title': self.project.title,
                'type': self.project.type,
                'parent': str(self.category.sodar_uuid),
                'description': self.project.description,
                'readme': '',
                'public_guest_access': False,
                'submit_status': self.project.submit_status,
                'roles': {
                    str(user_as.sodar_uuid): {
                        'user': {
                            'username': user.username,
                            'name': user.name,
                            'email': user.email,
                            'sodar_uuid': str(user.sodar_uuid),
                        },
                        'role': role,
                        'sodar_uuid': str(user_as.sodar_uuid),
                    }
                },
                'sodar_uuid': str(self.project.sodar_uuid),
            }

            # Assert response
            self.assertEqual(response.status_code, 200)
            response_data = json.loads(response.content)
            self.assertEqual(response_data, expected)

    def test_http_x_forwarded_for_block_all_owner(self):
        self._get_project_ip_allowing(
            'owner', 'HTTP_X_FORWARDED_FOR', [], blocked=False
        )

    def test_http_x_forwarded_for_allow_ip_owner(self):
        self._get_project_ip_allowing(
            'owner', 'HTTP_X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_http_x_forwarded_for_block_all_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'HTTP_X_FORWARDED_FOR', [], blocked=False
        )

    def test_http_x_forwarded_for_allow_ip_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'HTTP_X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_http_x_forwarded_for_block_all_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'HTTP_X_FORWARDED_FOR', [], blocked=True
        )

    def test_http_x_forwarded_for_allow_ip_contributor(self):
        self._get_project_ip_allowing(
            'contributor',
            'HTTP_X_FORWARDED_FOR',
            ['192.168.1.1'],
            blocked=False,
        )

    def test_http_x_forwarded_for_block_all_guest(self):
        self._get_project_ip_allowing(
            'guest', 'HTTP_X_FORWARDED_FOR', [], blocked=True
        )

    def test_http_x_forwarded_for_allow_ip_guest(self):
        self._get_project_ip_allowing(
            'guest', 'HTTP_X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_x_forwarded_for_block_all_owner(self):
        self._get_project_ip_allowing(
            'owner', 'X_FORWARDED_FOR', [], blocked=False
        )

    def test_x_forwarded_for_allow_ip_owner(self):
        self._get_project_ip_allowing(
            'owner', 'X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_x_forwarded_for_block_all_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'X_FORWARDED_FOR', [], blocked=False
        )

    def test_x_forwarded_for_allow_ip_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_x_forwarded_for_block_all_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'X_FORWARDED_FOR', [], blocked=True
        )

    def test_forwarded_for_allow_ip_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_forwarded_for_block_all_guest(self):
        self._get_project_ip_allowing(
            'guest', 'X_FORWARDED_FOR', [], blocked=True
        )

    def test_forwarded_for_allow_ip_guest(self):
        self._get_project_ip_allowing(
            'guest', 'X_FORWARDED_FOR', ['192.168.1.1'], blocked=False
        )

    def test_forwarded_block_all_owner(self):
        self._get_project_ip_allowing('owner', 'FORWARDED', [], blocked=False)

    def test_forwarded_allow_ip_owner(self):
        self._get_project_ip_allowing(
            'owner', 'FORWARDED', ['192.168.1.1'], blocked=False
        )

    def test_forwarded_block_all_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'FORWARDED', [], blocked=False
        )

    def test_forwarded_allow_ip_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'FORWARDED', ['192.168.1.1'], blocked=False
        )

    def test_forwarded_block_all_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'FORWARDED', [], blocked=True
        )

    def test_forwarded_allow_ip_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'FORWARDED', ['192.168.1.1'], blocked=False
        )

    def test_forwarded_block_all_guest(self):
        self._get_project_ip_allowing('guest', 'FORWARDED', [], blocked=True)

    def test_forwarded_allow_ip_guest(self):
        self._get_project_ip_allowing(
            'guest', 'FORWARDED', ['192.168.1.1'], blocked=False
        )

    def test_remote_addr_block_all_owner(self):
        self._get_project_ip_allowing('owner', 'REMOTE_ADDR', [], blocked=False)

    def test_remote_addr_allow_ip_owner(self):
        self._get_project_ip_allowing(
            'owner', 'REMOTE_ADDR', ['192.168.1.1'], blocked=False
        )

    def test_remote_addr_block_all_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'REMOTE_ADDR', [], blocked=False
        )

    def test_remote_addr_allow_ip_delegate(self):
        self._get_project_ip_allowing(
            'delegate', 'REMOTE_ADDR', ['192.168.1.1'], blocked=False
        )

    def test_remote_addr_block_all_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'REMOTE_ADDR', [], blocked=True
        )

    def test_remote_addr_allow_ip_contributor(self):
        self._get_project_ip_allowing(
            'contributor', 'REMOTE_ADDR', ['192.168.1.1'], blocked=False
        )

    def test_remote_addr_block_all_guest(self):
        self._get_project_ip_allowing('guest', 'REMOTE_ADDR', [], blocked=True)

    def test_remote_addr_allow_ip_guest(self):
        self._get_project_ip_allowing(
            'guest', 'REMOTE_ADDR', ['192.168.1.1'], blocked=False
        )

    def test_remote_addr_allow_network_guest(self):
        self._get_project_ip_allowing(
            'guest', 'REMOTE_ADDR', ['192.168.1.0/24'], blocked=False
        )

    def test_remote_addr_block_not_in_allowlist_ip_guest(self):
        self._get_project_ip_allowing(
            'guest', 'REMOTE_ADDR', ['192.168.1.2'], blocked=True
        )

    def test_remote_addr_block_not_in_allowlist_network_guest(
        self,
    ):
        self._get_project_ip_allowing(
            'guest', 'REMOTE_ADDR', ['192.168.2.0/24'], blocked=True
        )


class TestProjectCreateAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for ProjectCreateAPIView"""

    def test_create_category(self):
        """Test creating a root category"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_CATEGORY_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': '',
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(Project.objects.count(), 3)

        # Assert object content
        new_category = Project.objects.get(title=NEW_CATEGORY_TITLE)
        model_dict = model_to_dict(new_category)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': new_category.pk,
            'title': new_category.title,
            'type': new_category.type,
            'parent': None,
            'description': new_category.description,
            'readme': new_category.readme.raw,
            'public_guest_access': False,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': new_category.title,
            'sodar_uuid': new_category.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert role assignment
        self.assertEqual(
            RoleAssignment.objects.filter(
                project=new_category, user=self.user, role=self.role_owner
            ).count(),
            1,
        )

        # Assert API response
        expected = {
            'title': NEW_CATEGORY_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': None,
            'description': new_category.description,
            'readme': new_category.readme.raw,
            'public_guest_access': False,
            'sodar_uuid': str(new_category.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_create_category_nested(self):
        """Test creating a category under an existing category"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_CATEGORY_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(Project.objects.count(), 3)

        # Assert object content
        new_category = Project.objects.get(title=NEW_CATEGORY_TITLE)
        model_dict = model_to_dict(new_category)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': new_category.pk,
            'title': new_category.title,
            'type': new_category.type,
            'parent': self.category.pk,
            'description': new_category.description,
            'readme': new_category.readme.raw,
            'public_guest_access': False,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': self.category.title + ' / ' + new_category.title,
            'sodar_uuid': new_category.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert role assignment
        self.assertEqual(
            RoleAssignment.objects.filter(
                project=new_category, user=self.user, role=self.role_owner
            ).count(),
            1,
        )

        # Assert API response
        expected = {
            'title': NEW_CATEGORY_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': str(self.category.sodar_uuid),
            'description': new_category.description,
            'readme': new_category.readme.raw,
            'public_guest_access': False,
            'sodar_uuid': str(new_category.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_create_project(self):
        """Test creating a project under an existing category"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(Project.objects.count(), 3)

        # Assert object content
        new_project = Project.objects.get(title=NEW_PROJECT_TITLE)
        model_dict = model_to_dict(new_project)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': new_project.pk,
            'title': new_project.title,
            'type': new_project.type,
            'parent': self.category.pk,
            'description': new_project.description,
            'readme': new_project.readme.raw,
            'public_guest_access': False,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': self.category.title + ' / ' + new_project.title,
            'sodar_uuid': new_project.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert role assignment
        self.assertEqual(
            RoleAssignment.objects.filter(
                project=new_project, user=self.user, role=self.role_owner
            ).count(),
            1,
        )

        # Assert API response
        expected = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': new_project.description,
            'readme': new_project.readme.raw,
            'public_guest_access': False,
            'sodar_uuid': str(new_project.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_create_project_root(self):
        """Test creating a project in root (should fail)"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': None,
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400)
        self.assertEqual(Project.objects.count(), 2)

    @override_settings(PROJECTROLES_DISABLE_CATEGORIES=True)
    def test_create_project_disable_categories(self):
        """Test creating a project in root with disabled categories"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': '',
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(Project.objects.count(), 3)

    def test_create_project_duplicate_title(self):
        """Test creating a project with a title already in category (should fail)"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': self.project.title,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400)
        self.assertEqual(Project.objects.count(), 2)

    def test_create_project_unknown_user(self):
        """Test creating a project with a non-existent user (should fail)"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': INVALID_UUID,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400)
        self.assertEqual(Project.objects.count(), 2)

    def test_create_project_unknown_parent(self):
        """Test creating a project with a non-existent parent category (should fail)"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': INVALID_UUID,
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400)
        self.assertEqual(Project.objects.count(), 2)

    def test_create_project_invalid_parent(self):
        """Test creating a project with a project as parent (should fail)"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.project.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400)
        self.assertEqual(Project.objects.count(), 2)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_create_project_target_enabled(self):
        """Test creating a project as TARGET with target creation allowed"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(Project.objects.count(), 3)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_create_project_target_remote(self):
        """Test creating a project as TARGET under a remote category (should fail)"""

        # Create source site
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )

        # Make category remote
        self._make_remote_project(
            project_uuid=self.category.sodar_uuid,
            project=self.category,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 403, msg=response.content)
        self.assertEqual(Project.objects.count(), 2)

    @override_settings(
        PROJECTROLES_SITE_MODE=SITE_MODE_TARGET,
        PROJECTROLES_TARGET_CREATE=False,
    )
    def test_create_project_target_disabled(self):
        """Test creating a project as TARGET with target creation disallowed (should fail)"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse('projectroles:api_project_create')
        post_data = {
            'title': NEW_PROJECT_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': 'description',
            'readme': 'readme',
            'public_guest_access': False,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 403, msg=response.content)
        self.assertEqual(Project.objects.count(), 2)


class TestProjectUpdateAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for ProjectUpdateAPIView"""

    def test_put_category(self):
        """Test put() for category updating"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.category.sodar_uuid},
        )
        put_data = {
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': '',
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='PUT', data=put_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(Project.objects.count(), 2)

        # Assert object content
        self.category.refresh_from_db()
        model_dict = model_to_dict(self.category)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': self.category.pk,
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': None,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': UPDATED_TITLE,
            'sodar_uuid': self.category.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert API response
        expected = {
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': None,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'roles': {
                str(self.category.get_owner().sodar_uuid): {
                    'role': PROJECT_ROLE_OWNER,
                    'user': self.get_serialized_user(self.user),
                    'sodar_uuid': str(self.category.get_owner().sodar_uuid),
                }
            },
            'sodar_uuid': str(self.category.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_put_project(self):
        """Test put() for project updating"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        put_data = {
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'owner': str(self.user.sodar_uuid),
        }
        response = self.request_knox(url, method='PUT', data=put_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(Project.objects.count(), 2)

        # Assert object content
        self.project.refresh_from_db()
        model_dict = model_to_dict(self.project)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': self.project.pk,
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': self.category.pk,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': self.category.title + ' / ' + UPDATED_TITLE,
            'sodar_uuid': self.project.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert API response
        expected = {
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'roles': {
                str(self.project.get_owner().sodar_uuid): {
                    'role': PROJECT_ROLE_OWNER,
                    'user': self.get_serialized_user(self.user),
                    'sodar_uuid': str(self.project.get_owner().sodar_uuid),
                }
            },
            'sodar_uuid': str(self.project.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_patch_category(self):
        """Test patch() for updating category metadata"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.category.sodar_uuid},
        )
        patch_data = {
            'title': UPDATED_TITLE,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
        }
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(Project.objects.count(), 2)

        # Assert object content
        self.category.refresh_from_db()
        model_dict = model_to_dict(self.category)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': self.category.pk,
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': None,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': False,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': UPDATED_TITLE,
            'sodar_uuid': self.category.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert role assignment
        self.assertEqual(self.category.get_owner().user, self.user)

        # Assert API response
        expected = {
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_CATEGORY,
            'parent': None,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': False,
            'roles': {
                str(self.category.get_owner().sodar_uuid): {
                    'role': PROJECT_ROLE_OWNER,
                    'user': self.get_serialized_user(self.user),
                    'sodar_uuid': str(self.category.get_owner().sodar_uuid),
                }
            },
            'sodar_uuid': str(self.category.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_patch_project(self):
        """Test patch() for updating project metadata"""

        # Assert preconditions
        self.assertEqual(Project.objects.count(), 2)

        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {
            'title': UPDATED_TITLE,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
        }
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(Project.objects.count(), 2)

        # Assert object content
        self.project.refresh_from_db()
        model_dict = model_to_dict(self.project)
        model_dict['readme'] = model_dict['readme'].raw
        expected = {
            'id': self.project.pk,
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': self.category.pk,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'full_title': self.category.title + ' / ' + UPDATED_TITLE,
            'sodar_uuid': self.project.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert role assignment
        self.assertEqual(self.project.get_owner().user, self.user)

        # Assert API response
        expected = {
            'title': UPDATED_TITLE,
            'type': PROJECT_TYPE_PROJECT,
            'parent': str(self.category.sodar_uuid),
            'submit_status': SODAR_CONSTANTS['SUBMIT_STATUS_OK'],
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
            'public_guest_access': True,
            'roles': {
                str(self.project.get_owner().sodar_uuid): {
                    'role': PROJECT_ROLE_OWNER,
                    'user': self.get_serialized_user(self.user),
                    'sodar_uuid': str(self.project.get_owner().sodar_uuid),
                }
            },
            'sodar_uuid': str(self.project.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_patch_project_owner(self):
        """Test patch() for updating project owner (should fail)"""
        new_owner = self.make_user('new_owner')

        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {'owner': str(new_owner.sodar_uuid)}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_patch_project_move(self):
        """Test patch() for moving project under a different category"""

        # Assert preconditions
        self.assertEqual(
            self.project.full_title,
            self.category.title + ' / ' + self.project.title,
        )

        new_category = self._make_project(
            'NewCategory', PROJECT_TYPE_CATEGORY, None
        )
        self._make_assignment(new_category, self.user, self.role_owner)
        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {'parent': str(new_category.sodar_uuid)}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 200, msg=response.content)

        # Assert object content
        self.project.refresh_from_db()
        model_dict = model_to_dict(self.project)
        self.assertEqual(model_dict['parent'], new_category.pk)

        # Assert role assignment
        self.assertEqual(self.project.get_owner().user, self.user)

        # Assert child project full title update
        self.assertEqual(
            self.project.full_title,
            new_category.title + ' / ' + self.project.title,
        )

        # Assert API response
        self.assertEqual(
            json.loads(response.content)['parent'], str(new_category.sodar_uuid)
        )

    def test_patch_project_move_unallowed(self):
        """Test patch() for moving project without permissions (should fail)"""

        new_category = self._make_project(
            'NewCategory', PROJECT_TYPE_CATEGORY, None
        )
        new_owner = self.make_user('new_owner')
        self._make_assignment(new_category, new_owner, self.role_owner)
        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {'parent': str(new_category.sodar_uuid)}
        # Disable superuser status from self.user and perform request
        self.user.is_superuser = False
        self.user.save()
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 403, msg=response.content)

    def test_patch_project_move_root(self):
        """Test patch() for moving project without permissions (should fail)"""

        new_category = self._make_project(
            'NewCategory', PROJECT_TYPE_CATEGORY, None
        )
        new_owner = self.make_user('new_owner')
        self._make_assignment(new_category, new_owner, self.role_owner)
        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {'parent': ''}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 200, msg=response.content)

    def test_patch_project_move_root_unallowed(self):
        """Test patch() for moving project to root without permissions (should fail)"""

        new_category = self._make_project(
            'NewCategory', PROJECT_TYPE_CATEGORY, None
        )
        new_owner = self.make_user('new_owner')
        self._make_assignment(new_category, new_owner, self.role_owner)
        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {'parent': ''}
        # Disable superuser status from self.user and perform request
        self.user.is_superuser = False
        self.user.save()
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 403, msg=response.content)

    def test_patch_project_move_child(self):
        """Test patch() for moving a category inside its child (should fail)"""

        new_category = self._make_project(
            'NewCategory', PROJECT_TYPE_CATEGORY, self.category
        )
        self._make_assignment(new_category, self.user, self.role_owner)
        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.category.sodar_uuid},
        )
        patch_data = {'parent': str(new_category.sodar_uuid)}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_patch_project_type_change(self):
        """Test patch() with a changed project type (should fail)"""
        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {'type': PROJECT_TYPE_CATEGORY}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_patch_project_remote(self):
        """Test patch() for updating remote project metadata (should fail)"""

        # Create source site and remote project
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )
        self._make_remote_project(
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        url = reverse(
            'projectroles:api_project_update',
            kwargs={'project': self.project.sodar_uuid},
        )
        patch_data = {
            'title': UPDATED_TITLE,
            'description': UPDATED_DESC,
            'readme': UPDATED_README,
        }
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400, msg=response.content)


class TestRoleAssignmentCreateAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for RoleAssignmentCreateAPIView"""

    def setUp(self):
        super().setUp()
        self.assign_user = self.make_user('assign_user')

    def test_create_contributor(self):
        """Test creating a contributor role for user"""

        # Assert preconditions
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 1
        )

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 2
        )

        # Assert object
        role_as = RoleAssignment.objects.filter(
            project=self.project,
            role=self.role_contributor,
            user=self.assign_user,
        ).first()
        self.assertIsNotNone(role_as)

        # Assert API response
        expected = {
            'project': str(self.project.sodar_uuid),
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'user': str(self.assign_user.sodar_uuid),
            'sodar_uuid': str(role_as.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_create_owner(self):
        """Test creating an owner role (should fail)"""

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_OWNER,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_create_delegate(self):
        """Test creating a delegate role for user as owner"""

        # Disable superuser status from self.user
        self.user.is_superuser = False
        self.user.save()

        # Assert preconditions
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 1
        )

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 2
        )

        # Assert object
        role_as = RoleAssignment.objects.filter(
            project=self.project, role=self.role_delegate, user=self.assign_user
        ).first()
        self.assertIsNotNone(role_as)

    def test_create_delegate_unauthorized(self):
        """Test creating a delegate role without authorization (should fail)"""

        # Create new user and grant delegate role
        new_user = self.make_user('new_user')
        self._make_assignment(self.project, new_user, self.role_contributor)
        new_user_token = self.get_token(new_user)

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(
            url, method='POST', data=post_data, token=new_user_token
        )

        # Assert response
        self.assertEqual(response.status_code, 403, msg=response.content)

    def test_create_delegate_limit(self):
        """Test creating a delegate role with limit reached (should fail)"""

        # Create new user and grant delegate role
        new_user = self.make_user('new_user')
        self._make_assignment(self.project, new_user, self.role_delegate)

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
        }

        # NOTE: Post as owner
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_create_delegate_limit_inherit(self):
        """Test creating a delegate role existing role for inherited owner"""

        # Set up category owner
        new_user = self.make_user('new_user')
        self.cat_owner_as.user = new_user

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
        }
        # NOTE: Post as owner
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 2
        )
        # Assert object
        role_as = RoleAssignment.objects.filter(
            project=self.project, role=self.role_delegate, user=self.assign_user
        ).first()
        self.assertIsNotNone(role_as)

    def test_create_delegate_category(self):
        """Test creating a non-owner role for category"""

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.category.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response
        self.assertEqual(response.status_code, 201, msg=response.content)

    def test_create_role_existing(self):
        """Test creating a role for user already in the project"""

        # Assert preconditions
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 1
        )

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 2
        )

        # Post again
        post_data = {
            'role': PROJECT_ROLE_GUEST,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 2
        )

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_create_remote(self):
        """Test creating a role for a remote project (should fail)"""

        # Create source site and remote project
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )
        self._make_remote_project(
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        # Assert preconditions
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 1
        )

        url = reverse(
            'projectroles:api_role_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            RoleAssignment.objects.filter(project=self.project).count(), 1
        )


class TestRoleAssignmentUpdateAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for RoleAssignmentUpdateAPIView"""

    def setUp(self):
        super().setUp()
        self.assign_user = self.make_user('assign_user')
        self.update_as = self._make_assignment(
            self.project, self.assign_user, self.role_contributor
        )

    def test_put_role(self):
        """Test put() for role assignment updating"""

        # Assert preconditions
        self.assertEqual(RoleAssignment.objects.count(), 3)

        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        put_data = {
            'role': PROJECT_ROLE_GUEST,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='PUT', data=put_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(RoleAssignment.objects.count(), 3)

        # Assert object content
        self.update_as.refresh_from_db()
        model_dict = model_to_dict(self.update_as)
        expected = {
            'id': self.update_as.pk,
            'project': self.project.pk,
            'role': self.role_guest.pk,
            'user': self.assign_user.pk,
            'sodar_uuid': self.update_as.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert API response
        expected = {
            'project': str(self.project.sodar_uuid),
            'role': PROJECT_ROLE_GUEST,
            'user': str(self.assign_user.sodar_uuid),
            'sodar_uuid': str(self.update_as.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_put_delegate(self):
        """Test put() for delegate role assignment"""
        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        put_data = {
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='PUT', data=put_data)

        # Assert response
        self.assertEqual(response.status_code, 200, msg=response.content)

        # Assert object content
        self.update_as.refresh_from_db()
        model_dict = model_to_dict(self.update_as)
        expected = {
            'id': self.update_as.pk,
            'project': self.project.pk,
            'role': self.role_delegate.pk,
            'user': self.assign_user.pk,
            'sodar_uuid': self.update_as.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert API response
        expected = {
            'project': str(self.project.sodar_uuid),
            'role': PROJECT_ROLE_DELEGATE,
            'user': str(self.assign_user.sodar_uuid),
            'sodar_uuid': str(self.update_as.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_put_owner(self):
        """Test put() for owner role assignment (should fail)"""
        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        put_data = {
            'role': PROJECT_ROLE_OWNER,
            'user': str(self.assign_user.sodar_uuid),
        }
        response = self.request_knox(url, method='PUT', data=put_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_put_change_user(self):
        """Test put() with a different user (should fail)"""
        new_user = self.make_user('new_user')

        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        put_data = {
            'role': PROJECT_ROLE_GUEST,
            'user': str(new_user.sodar_uuid),
        }
        response = self.request_knox(url, method='PUT', data=put_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_patch_role(self):
        """Test patch() for role assignment updating"""

        # Assert preconditions
        self.assertEqual(RoleAssignment.objects.count(), 3)

        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        patch_data = {'role': PROJECT_ROLE_GUEST}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(RoleAssignment.objects.count(), 3)

        # Assert object content
        self.update_as.refresh_from_db()
        model_dict = model_to_dict(self.update_as)
        expected = {
            'id': self.update_as.pk,
            'project': self.project.pk,
            'role': self.role_guest.pk,
            'user': self.assign_user.pk,
            'sodar_uuid': self.update_as.sodar_uuid,
        }
        self.assertEqual(model_dict, expected)

        # Assert API response
        expected = {
            'project': str(self.project.sodar_uuid),
            'role': PROJECT_ROLE_GUEST,
            'user': str(self.assign_user.sodar_uuid),
            'sodar_uuid': str(self.update_as.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)

    def test_patch_change_user(self):
        """Test patch() with a different user (should fail)"""
        new_user = self.make_user('new_user')

        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        patch_data = {'user': str(new_user.sodar_uuid)}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response
        self.assertEqual(response.status_code, 400, msg=response.content)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_patch_role_remote(self):
        """Test patch() for updating a role in a remote project (should fail)"""

        # Create source site and remote project
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )
        self._make_remote_project(
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        url = reverse(
            'projectroles:api_role_update',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        patch_data = {'role': PROJECT_ROLE_GUEST}
        response = self.request_knox(url, method='PATCH', data=patch_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 400, msg=response.content)


class TestRoleAssignmentDestroyAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for RoleAssignmentDestroyAPIView"""

    def setUp(self):
        super().setUp()
        self.assign_user = self.make_user('assign_user')

        self.update_as = self._make_assignment(
            self.project, self.assign_user, self.role_contributor
        )

    def test_delete_role(self):
        """Test delete for role assignment deletion"""

        # Assert preconditions
        self.assertEqual(RoleAssignment.objects.count(), 3)

        url = reverse(
            'projectroles:api_role_destroy',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        response = self.request_knox(url, method='DELETE')

        # Assert response and role status
        self.assertEqual(response.status_code, 204, msg=response.content)
        self.assertEqual(RoleAssignment.objects.count(), 2)
        self.assertEqual(
            RoleAssignment.objects.filter(
                project=self.project, user=self.assign_user
            ).count(),
            0,
        )

    def test_delete_delegate_unauthorized(self):
        """Test delete for delegate deletion without perms (should fail)"""
        new_user = self.make_user('new_user')
        delegate_as = self._make_assignment(
            self.project, new_user, self.role_delegate
        )

        # Assert preconditions
        self.assertEqual(RoleAssignment.objects.count(), 4)

        url = reverse(
            'projectroles:api_role_destroy',
            kwargs={'roleassignment': delegate_as.sodar_uuid},
        )
        # NOTE: Perform record as contributor user
        token = self.get_token(self.assign_user)
        response = self.request_knox(url, method='DELETE', token=token)

        # Assert response and role status
        self.assertEqual(response.status_code, 403, msg=response.content)
        self.assertEqual(RoleAssignment.objects.count(), 4)

    def test_delete_owner(self):
        """Test delete for owner deletion (should fail)"""

        # Assert preconditions
        self.assertEqual(RoleAssignment.objects.count(), 3)

        url = reverse(
            'projectroles:api_role_destroy',
            kwargs={'roleassignment': self.owner_as.sodar_uuid},
        )
        response = self.request_knox(url, method='DELETE')

        # Assert response and project status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(RoleAssignment.objects.count(), 3)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_delete_remote(self):
        """Test delete for a remote project (should fail)"""

        # Create source site and remote project
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )
        self._make_remote_project(
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        # Assert preconditions
        self.assertEqual(RoleAssignment.objects.count(), 3)

        url = reverse(
            'projectroles:api_role_destroy',
            kwargs={'roleassignment': self.update_as.sodar_uuid},
        )
        response = self.request_knox(url, method='DELETE')

        # Assert response and role status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(RoleAssignment.objects.count(), 3)


class TestRoleAssignmentOwnerTransferAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for RoleAssignmentOwnerTransferAPIView"""

    def setUp(self):
        super().setUp()
        self.assign_user = self.make_user('assign_user')

    def test_transfer_owner(self):
        """Test transferring ownership for a project"""

        # Assign role to new user
        self._make_assignment(
            self.project, self.assign_user, self.role_contributor
        )

        # Assert preconditions
        self.assertEqual(self.project.get_owner().user, self.user)

        url = reverse(
            'projectroles:api_role_owner_transfer',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'new_owner': self.assign_user.username,
            'old_owner_role': self.role_contributor.name,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(self.project.get_owner().user, self.assign_user)

    def test_transfer_owner_category(self):
        """Test transferring ownership for a category"""

        # Assign role to new user
        self._make_assignment(
            self.category, self.assign_user, self.role_contributor
        )

        # Assert preconditions
        self.assertEqual(self.category.get_owner().user, self.user)

        url = reverse(
            'projectroles:api_role_owner_transfer',
            kwargs={'project': self.category.sodar_uuid},
        )
        post_data = {
            'new_owner': self.assign_user.username,
            'old_owner_role': self.role_contributor.name,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(self.category.get_owner().user, self.assign_user)

    def test_transfer_owner_inherit(self):
        """Test transferring ownership to an inherited owner"""

        # Assign role to new user
        self._make_assignment(
            self.project, self.assign_user, self.role_contributor
        )

        # Set alt owner to current project, make self.user inherited owner
        alt_owner = self.make_user('alt_owner')
        self.owner_as.user = alt_owner
        self.owner_as.save()

        # Assert preconditions
        self.assertEqual(self.project.get_owner().user, alt_owner)
        self.assertEqual(
            self.project.get_owners(inherited_only=True)[0].user, self.user
        )

        url = reverse(
            'projectroles:api_role_owner_transfer',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'new_owner': self.user.username,
            'old_owner_role': self.role_contributor.name,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(self.project.get_owner().user, self.user)
        old_owner_as = RoleAssignment.objects.get(
            project=self.project, user=alt_owner
        )
        self.assertEqual(old_owner_as.role, self.role_contributor)

    def test_transfer_owner_no_roles(self):
        """Test transferring ownership to user with no existing roles (should fail)"""

        # NOTE: No role given to user

        url = reverse(
            'projectroles:api_role_owner_transfer',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'new_owner': self.assign_user.username,
            'old_owner_role': self.role_contributor.name,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400, msg=response.content)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_transfer_remote(self):
        """Test transferring ownership for a remote project (should fail)"""

        # Create source site and remote project
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )
        self._make_remote_project(
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        # Assign role to new user
        self._make_assignment(
            self.project, self.assign_user, self.role_contributor
        )

        # Assert preconditions
        self.assertEqual(self.project.get_owner().user, self.user)

        url = reverse(
            'projectroles:api_role_owner_transfer',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'new_owner': self.assign_user.username,
            'old_owner_role': self.role_contributor.name,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and project status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(self.project.get_owner().user, self.user)


class TestProjectInviteListAPIView(ProjectInviteMixin, TestCoreAPIViewsBase):
    """Tests for ProjectInviteListAPIView"""

    def setUp(self):
        super().setUp()
        # Create invites
        self.invite = self._make_invite(
            email=INVITE_USER_EMAIL,
            project=self.project,
            role=self.role_guest,
            issuer=self.user,
            message='',
            secret=build_secret(),
        )
        self.invite2 = self._make_invite(
            email=INVITE_USER2_EMAIL,
            project=self.project,
            role=self.role_contributor,
            issuer=self.user,
            message=INVITE_MESSAGE,
            secret=build_secret(),
        )

    def test_get(self):
        """Test ProjectInviteListAPIView get()"""
        url = reverse(
            'projectroles:api_invite_list',
            kwargs={'project': self.project.sodar_uuid},
        )
        response = self.request_knox(url, token=self.get_token(self.user))

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 2)
        expected = [
            {
                'email': INVITE_USER_EMAIL,
                'project': str(self.project.sodar_uuid),
                'role': self.role_guest.name,
                'issuer': self.get_serialized_user(self.user),
                'date_created': self.get_drf_datetime(self.invite.date_created),
                'date_expire': self.get_drf_datetime(self.invite.date_expire),
                'message': '',
                'sodar_uuid': str(self.invite.sodar_uuid),
            },
            {
                'email': INVITE_USER2_EMAIL,
                'project': str(self.project.sodar_uuid),
                'role': self.role_contributor.name,
                'issuer': self.get_serialized_user(self.user),
                'date_created': self.get_drf_datetime(
                    self.invite2.date_created
                ),
                'date_expire': self.get_drf_datetime(self.invite2.date_expire),
                'message': INVITE_MESSAGE,
                'sodar_uuid': str(self.invite2.sodar_uuid),
            },
        ]
        self.assertEqual(response_data, expected)

    def test_get_inactive(self):
        """Test get() with an inactive invite"""
        self.invite.active = False
        self.invite.save()

        url = reverse(
            'projectroles:api_invite_list',
            kwargs={'project': self.project.sodar_uuid},
        )
        response = self.request_knox(url, token=self.get_token(self.user))

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 1)
        expected = [
            {
                'email': INVITE_USER2_EMAIL,
                'project': str(self.project.sodar_uuid),
                'role': self.role_contributor.name,
                'issuer': self.get_serialized_user(self.user),
                'date_created': self.get_drf_datetime(
                    self.invite2.date_created
                ),
                'date_expire': self.get_drf_datetime(self.invite2.date_expire),
                'message': INVITE_MESSAGE,
                'sodar_uuid': str(self.invite2.sodar_uuid),
            },
        ]
        self.assertEqual(response_data, expected)


class TestProjectInviteCreateAPIView(
    RemoteSiteMixin, RemoteProjectMixin, TestCoreAPIViewsBase
):
    """Tests for ProjectInviteCreateAPIView"""

    def test_create(self):
        """Test creating a contributor invite for user"""

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 1
        )

        # Assert data
        invite = ProjectInvite.objects.first()
        self.assertEqual(invite.email, INVITE_USER_EMAIL)
        self.assertEqual(invite.role, self.role_contributor)
        self.assertEqual(invite.issuer, self.user)
        self.assertEqual(invite.message, INVITE_MESSAGE)

        # Assert response
        expected = {
            'email': INVITE_USER_EMAIL,
            'project': str(self.project.sodar_uuid),
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'issuer': self.get_serialized_user(self.user),
            'date_created': self.get_drf_datetime(invite.date_created),
            'date_expire': self.get_drf_datetime(invite.date_expire),
            'message': invite.message,
            'sodar_uuid': str(invite.sodar_uuid),
        }
        self.assertEqual(json.loads(response.content), expected)
        self.assertEqual(len(mail.outbox), 1)

    def test_create_owner(self):
        """Test creating an invite for an owner role (should fail)"""

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_OWNER,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and data
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )
        self.assertEqual(len(mail.outbox), 0)

    def test_create_delegate(self):
        """Test creating an invite for an delegate role"""

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_DELEGATE,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and data
        self.assertEqual(response.status_code, 201, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 1
        )
        invite = ProjectInvite.objects.first()
        self.assertEqual(invite.role, self.role_delegate)
        self.assertEqual(len(mail.outbox), 1)

    @override_settings(PROJECTROLES_DELEGATE_LIMIT=2)
    def test_create_delegate_no_perms(self):
        """Test creating an delegate invite without perms (should fail)"""
        del_user = self.make_user('delegate')
        self._make_assignment(self.project, del_user, self.role_delegate)

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_DELEGATE,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(
            url, method='POST', data=post_data, token=self.get_token(del_user)
        )

        # Assert response and data
        self.assertEqual(response.status_code, 403, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )
        self.assertEqual(len(mail.outbox), 0)

    def test_create_delegate_limit(self):
        """Test creating an delegate invite with exceeded limit (should fail)"""
        del_user = self.make_user('delegate')
        self._make_assignment(self.project, del_user, self.role_delegate)

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_DELEGATE,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and data
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )
        self.assertEqual(len(mail.outbox), 0)

    def test_create_invalid_email(self):
        """Test creating an invite with invalid email (should fail)"""

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': 'NOT_AN_EMAIL!',
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and data
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )
        self.assertEqual(len(mail.outbox), 0)

    def test_create_existing_user(self):
        """Test creating an invite for an existing user (should fail)"""
        user = self.make_user('new_user')
        user.email = INVITE_USER_EMAIL
        user.save()

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )
        self.assertEqual(len(mail.outbox), 0)

    @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET)
    def test_create_remote(self):
        """Test creating an invite for a remote project (should fail)"""
        # Set up remote site and project
        source_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_SOURCE,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )
        self._make_remote_project(
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            site=source_site,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES'],
        )

        # Assert preconditions
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )

        url = reverse(
            'projectroles:api_invite_create',
            kwargs={'project': self.project.sodar_uuid},
        )
        post_data = {
            'email': INVITE_USER_EMAIL,
            'role': PROJECT_ROLE_CONTRIBUTOR,
            'message': INVITE_MESSAGE,
        }
        response = self.request_knox(url, method='POST', data=post_data)

        # Assert response and role status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(
            ProjectInvite.objects.filter(project=self.project).count(), 0
        )
        self.assertEqual(len(mail.outbox), 0)


class TestProjectInviteRevokeAPIView(ProjectInviteMixin, TestCoreAPIViewsBase):
    """Tests for ProjectInviteRevokeAPIView"""

    def setUp(self):
        super().setUp()

        # Create invite
        self.invite = self._make_invite(
            email=INVITE_USER_EMAIL,
            project=self.project,
            role=self.role_contributor,
            issuer=self.user,
        )

    def test_revoke(self):
        """Test revoking an invite"""

        # Assert preconditions
        self.assertEqual(self.invite.active, True)

        url = reverse(
            'projectroles:api_invite_revoke',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(url, method='POST')

        # Assert response and invite status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.invite.refresh_from_db()
        self.assertEqual(self.invite.active, False)

    def test_revoke_inactive(self):
        """Test revoking an already inactive invite (should fail)"""
        self.invite.active = False
        self.invite.save()

        url = reverse(
            'projectroles:api_invite_revoke',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(url, method='POST')

        # Assert response and invite status
        self.assertEqual(response.status_code, 400, msg=response.content)

    def test_revoke_delegate(self):
        """Test revoking a delegate invite with sufficient perms"""
        self.invite.role = self.role_delegate
        self.invite.save()

        url = reverse(
            'projectroles:api_invite_revoke',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(url, method='POST')

        # Assert response and invite status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.invite.refresh_from_db()
        self.assertEqual(self.invite.active, False)

    def test_revoke_delegate_no_perms(self):
        """Test revoking a delegate invite without perms (should fail)"""
        self.invite.role = self.role_delegate
        self.invite.save()
        delegate = self.make_user('delegate')
        self._make_assignment(self.project, delegate, self.role_delegate)

        url = reverse(
            'projectroles:api_invite_revoke',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(
            url, method='POST', token=self.get_token(delegate)
        )

        # Assert response and invite status
        self.assertEqual(response.status_code, 403, msg=response.content)
        self.invite.refresh_from_db()
        self.assertEqual(self.invite.active, True)

    def test_revoke_not_found(self):
        """Test revoking invite with invalid UUID"""
        url = reverse(
            'projectroles:api_invite_revoke',
            kwargs={'projectinvite': INVALID_UUID},
        )
        response = self.request_knox(url, method='POST')
        self.assertEqual(response.status_code, 404)


class TestProjectInviteResendAPIView(ProjectInviteMixin, TestCoreAPIViewsBase):
    """Tests for ProjectInviteResendAPIView"""

    def setUp(self):
        super().setUp()

        # Create invite
        self.invite = self._make_invite(
            email=INVITE_USER_EMAIL,
            project=self.project,
            role=self.role_contributor,
            issuer=self.user,
        )

    def test_resend(self):
        """Test resending an invite"""

        # Assert preconditions
        self.assertEqual(len(mail.outbox), 0)

        url = reverse(
            'projectroles:api_invite_resend',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(url, method='POST')

        # Assert response and mail status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(len(mail.outbox), 1)

    def test_resend_inactive(self):
        """Test resending an inactive invite (should fail)"""
        self.invite.active = False
        self.invite.save()

        url = reverse(
            'projectroles:api_invite_resend',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(url, method='POST')

        # Assert response and mail status
        self.assertEqual(response.status_code, 400, msg=response.content)
        self.assertEqual(len(mail.outbox), 0)

    def test_resend_delegate(self):
        """Test resending a delegate invite with sufficient perms"""
        self.invite.role = self.role_delegate
        self.invite.save()

        url = reverse(
            'projectroles:api_invite_resend',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(url, method='POST')

        # Assert response and mail status
        self.assertEqual(response.status_code, 200, msg=response.content)
        self.assertEqual(len(mail.outbox), 1)

    def test_resend_delegate_no_perms(self):
        """Test resending a delegate invite without perms (should fail)"""
        self.invite.role = self.role_delegate
        self.invite.save()
        delegate = self.make_user('delegate')
        self._make_assignment(self.project, delegate, self.role_delegate)

        url = reverse(
            'projectroles:api_invite_resend',
            kwargs={'projectinvite': self.invite.sodar_uuid},
        )
        response = self.request_knox(
            url, method='POST', token=self.get_token(delegate)
        )

        # Assert response and mail status
        self.assertEqual(response.status_code, 403, msg=response.content)
        self.assertEqual(len(mail.outbox), 0)

    def test_resend_not_found(self):
        """Test resending invite with invalid UUID"""
        url = reverse(
            'projectroles:api_invite_resend',
            kwargs={'projectinvite': INVALID_UUID},
        )
        response = self.request_knox(url, method='POST')
        self.assertEqual(response.status_code, 404)


class TestUserListAPIView(TestCoreAPIViewsBase):
    """Tests for UserListAPIView"""

    def setUp(self):
        super().setUp()
        # Create additional users
        self.domain_user = self.make_user('domain_user@domain')

    def test_get(self):
        """Test UserListAPIView get() as a regular user"""
        url = reverse('projectroles:api_user_list')
        response = self.request_knox(
            url, token=self.get_token(self.domain_user)
        )

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 1)  # System users not returned
        expected = [
            {
                'username': self.domain_user.username,
                'name': self.domain_user.name,
                'email': self.domain_user.email,
                'sodar_uuid': str(self.domain_user.sodar_uuid),
            }
        ]
        self.assertEqual(response_data, expected)

    def test_get_superuser(self):
        """Test UserListAPIView get() as a superuser"""
        url = reverse('projectroles:api_user_list')
        response = self.request_knox(url)  # Default token is for superuser

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        self.assertEqual(len(response_data), 2)
        expected = [
            {
                'username': self.user.username,
                'name': self.user.name,
                'email': self.user.email,
                'sodar_uuid': str(self.user.sodar_uuid),
            },
            {
                'username': self.domain_user.username,
                'name': self.domain_user.name,
                'email': self.domain_user.email,
                'sodar_uuid': str(self.domain_user.sodar_uuid),
            },
        ]
        self.assertEqual(response_data, expected)


class TestCurrentUserRetrieveAPIView(TestCoreAPIViewsBase):
    """Tests for CurrentUserRetrieveAPIView"""

    def setUp(self):
        super().setUp()
        # Create additional users
        self.domain_user = self.make_user('domain_user@domain')

    def test_get(self):
        """Test CurrentUserRetrieveAPIView get() as a regular user"""
        url = reverse('projectroles:api_user_current')
        response = self.request_knox(
            url, token=self.get_token(self.domain_user)
        )

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        expected = {
            'username': self.domain_user.username,
            'name': self.domain_user.name,
            'email': self.domain_user.email,
            'sodar_uuid': str(self.domain_user.sodar_uuid),
        }
        self.assertEqual(response_data, expected)

    def test_get_superuser(self):
        """Test CurrentUserRetrieveAPIView get() as superuser"""
        url = reverse('projectroles:api_user_current')
        response = self.request_knox(url)

        # Assert response
        self.assertEqual(response.status_code, 200)
        response_data = json.loads(response.content)
        expected = {
            'username': self.user.username,
            'name': self.user.name,
            'email': self.user.email,
            'sodar_uuid': str(self.user.sodar_uuid),
        }
        self.assertEqual(response_data, expected)


class TestAPIVersioning(TestCoreAPIViewsBase):
    """Tests for REST API view versioning using ProjectRetrieveAPIView"""

    def setUp(self):
        super().setUp()

        self.url = reverse(
            'projectroles:api_project_retrieve',
            kwargs={'project': self.project.sodar_uuid},
        )

    def test_api_versioning(self):
        """Test SODAR API Access with correct version headers"""
        response = self.request_knox(
            self.url,
            media_type=views_api.CORE_API_MEDIA_TYPE,
            version=views_api.CORE_API_DEFAULT_VERSION,
        )
        self.assertEqual(response.status_code, 200)

    def test_api_versioning_invalid_version(self):
        """Test SODAR API Access with unsupported version (should fail)"""
        response = self.request_knox(
            self.url,
            media_type=views_api.CORE_API_MEDIA_TYPE,
            version=CORE_API_VERSION_INVALID,
        )
        self.assertEqual(response.status_code, 406)

    def test_api_versioning_invalid_media_type(self):
        """Test SODAR API Access with unsupported media type (should fail)"""
        response = self.request_knox(
            self.url,
            media_type=CORE_API_MEDIA_TYPE_INVALID,
            version=views_api.CORE_API_MEDIA_TYPE,
        )
        self.assertEqual(response.status_code, 406)


# TODO: To be updated once the legacy API view is redone for SODAR Core v1.0
class TestRemoteProjectGetAPIView(
    ProjectMixin,
    RoleAssignmentMixin,
    RemoteSiteMixin,
    RemoteProjectMixin,
    SODARAPIViewTestMixin,
    TestViewsBase,
):
    """Tests for remote project getting API view"""

    media_type = views_api.CORE_API_MEDIA_TYPE
    api_version = views_api.CORE_API_DEFAULT_VERSION

    def setUp(self):
        super().setUp()

        # Set up projects
        self.category = self._make_project(
            'TestCategory', PROJECT_TYPE_CATEGORY, None
        )
        self.cat_owner_as = self._make_assignment(
            self.category, self.user, self.role_owner
        )
        self.project = self._make_project(
            'TestProject', PROJECT_TYPE_PROJECT, self.category
        )
        self.project_owner_as = self._make_assignment(
            self.project, self.user, self.role_owner
        )

        # Create target site
        self.target_site = self._make_site(
            name=REMOTE_SITE_NAME,
            url=REMOTE_SITE_URL,
            mode=SITE_MODE_TARGET,
            description=REMOTE_SITE_DESC,
            secret=REMOTE_SITE_SECRET,
        )

        # Create remote project
        self.remote_project = self._make_remote_project(
            site=self.target_site,
            project_uuid=self.project.sodar_uuid,
            project=self.project,
            level=SODAR_CONSTANTS['REMOTE_LEVEL_READ_INFO'],
        )

        self.remote_api = RemoteProjectAPI()

    def test_get(self):
        """Test retrieving project data to the target site"""

        response = self.client.get(
            reverse(
                'projectroles:api_remote_get',
                kwargs={'secret': REMOTE_SITE_SECRET},
            )
        )

        self.assertEqual(response.status_code, 200)

        expected = self.remote_api.get_source_data(self.target_site)
        response_dict = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response_dict, expected)

    def test_get_invalid_secret(self):
        """Test retrieving project data with an invalid secret (should fail)"""

        response = self.client.get(
            reverse(
                'projectroles:api_remote_get', kwargs={'secret': build_secret()}
            )
        )

        self.assertEqual(response.status_code, 401)

[((919, 5, 919, 60), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((1030, 5, 1030, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((1053, 5, 1053, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((1093, 5, 1096, 5), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((1521, 5, 1521, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((1782, 5, 1782, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((1996, 5, 1996, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((2099, 5, 2099, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((2246, 5, 2246, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((2486, 5, 2486, 53), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((2600, 5, 2600, 63), 'django.test.override_settings', 'override_settings', (), '', False, 'from django.test import override_settings\n'), ((101, 17, 101, 52), 'knox.models.AuthToken.objects.create', 'AuthToken.objects.create', (), '', False, 'from knox.models import AuthToken\n'), ((217, 11, 217, 38), 'projectroles.plugins.get_backend_api', 'get_backend_api', ({(217, 27, 217, 37): '"""taskflow"""'}, {}), "('taskflow')", False, 'from projectroles.plugins import change_plugin_status, get_backend_api\n'), ((271, 14, 271, 54), 'django.urls.reverse', 'reverse', ({(271, 22, 271, 53): '"""projectroles:api_project_list"""'}, {}), "('projectroles:api_project_list')", False, 'from django.urls import reverse\n'), ((276, 24, 276, 52), 'json.loads', 'json.loads', ({(276, 35, 276, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((329, 14, 329, 54), 'django.urls.reverse', 'reverse', ({(329, 22, 329, 53): '"""projectroles:api_project_list"""'}, {}), "('projectroles:api_project_list')", False, 'from django.urls import reverse\n'), ((334, 24, 334, 52), 'json.loads', 'json.loads', ({(334, 35, 334, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((343, 14, 343, 54), 'django.urls.reverse', 'reverse', ({(343, 22, 343, 53): '"""projectroles:api_project_list"""'}, {}), "('projectroles:api_project_list')", False, 'from django.urls import reverse\n'), ((348, 24, 348, 52), 'json.loads', 'json.loads', ({(348, 35, 348, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((357, 14, 360, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((365, 24, 365, 52), 'json.loads', 'json.loads', ({(365, 35, 365, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((392, 14, 395, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((400, 24, 400, 52), 'json.loads', 'json.loads', ({(400, 35, 400, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((427, 14, 430, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((443, 34, 445, 9), 'projectroles.models.RoleAssignment.objects.get', 'RoleAssignment.objects.get', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((452, 22, 454, 9), 'projectroles.models.RoleAssignment.objects.get', 'RoleAssignment.objects.get', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((490, 14, 493, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((723, 14, 723, 56), 'django.urls.reverse', 'reverse', ({(723, 22, 723, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((740, 23, 740, 68), 'projectroles.models.Project.objects.get', 'Project.objects.get', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((741, 21, 741, 48), 'django.forms.models.model_to_dict', 'model_to_dict', ({(741, 35, 741, 47): 'new_category'}, {}), '(new_category)', False, 'from django.forms.models import model_to_dict\n'), ((783, 14, 783, 56), 'django.urls.reverse', 'reverse', ({(783, 22, 783, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((800, 23, 800, 68), 'projectroles.models.Project.objects.get', 'Project.objects.get', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((801, 21, 801, 48), 'django.forms.models.model_to_dict', 'model_to_dict', ({(801, 35, 801, 47): 'new_category'}, {}), '(new_category)', False, 'from django.forms.models import model_to_dict\n'), ((843, 14, 843, 56), 'django.urls.reverse', 'reverse', ({(843, 22, 843, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((860, 22, 860, 66), 'projectroles.models.Project.objects.get', 'Project.objects.get', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((861, 21, 861, 47), 'django.forms.models.model_to_dict', 'model_to_dict', ({(861, 35, 861, 46): 'new_project'}, {}), '(new_project)', False, 'from django.forms.models import model_to_dict\n'), ((903, 14, 903, 56), 'django.urls.reverse', 'reverse', ({(903, 22, 903, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((926, 14, 926, 56), 'django.urls.reverse', 'reverse', ({(926, 22, 926, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((948, 14, 948, 56), 'django.urls.reverse', 'reverse', ({(948, 22, 948, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((970, 14, 970, 56), 'django.urls.reverse', 'reverse', ({(970, 22, 970, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((992, 14, 992, 56), 'django.urls.reverse', 'reverse', ({(992, 22, 992, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((1014, 14, 1014, 56), 'django.urls.reverse', 'reverse', ({(1014, 22, 1014, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((1037, 14, 1037, 56), 'django.urls.reverse', 'reverse', ({(1037, 22, 1037, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((1077, 14, 1077, 56), 'django.urls.reverse', 'reverse', ({(1077, 22, 1077, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((1103, 14, 1103, 56), 'django.urls.reverse', 'reverse', ({(1103, 22, 1103, 55): '"""projectroles:api_project_create"""'}, {}), "('projectroles:api_project_create')", False, 'from django.urls import reverse\n'), ((1131, 14, 1134, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1152, 21, 1152, 49), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1152, 35, 1152, 48): 'self.category'}, {}), '(self.category)', False, 'from django.forms.models import model_to_dict\n'), ((1194, 14, 1197, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1215, 21, 1215, 48), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1215, 35, 1215, 47): 'self.project'}, {}), '(self.project)', False, 'from django.forms.models import model_to_dict\n'), ((1257, 14, 1260, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1274, 21, 1274, 49), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1274, 35, 1274, 48): 'self.category'}, {}), '(self.category)', False, 'from django.forms.models import model_to_dict\n'), ((1319, 14, 1322, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1337, 21, 1337, 48), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1337, 35, 1337, 47): 'self.project'}, {}), '(self.project)', False, 'from django.forms.models import model_to_dict\n'), ((1380, 14, 1383, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1403, 14, 1406, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1415, 21, 1415, 48), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1415, 35, 1415, 47): 'self.project'}, {}), '(self.project)', False, 'from django.forms.models import model_to_dict\n'), ((1440, 14, 1443, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1461, 14, 1464, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1479, 14, 1482, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1499, 14, 1502, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1511, 14, 1514, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1540, 14, 1543, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1572, 14, 1575, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1608, 14, 1611, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1633, 14, 1636, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1663, 14, 1666, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1685, 14, 1688, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1707, 14, 1710, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1732, 14, 1735, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1753, 14, 1756, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1806, 14, 1809, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1841, 14, 1844, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1857, 21, 1857, 50), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1857, 35, 1857, 49): 'self.update_as'}, {}), '(self.update_as)', False, 'from django.forms.models import model_to_dict\n'), ((1878, 14, 1881, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1893, 21, 1893, 50), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1893, 35, 1893, 49): 'self.update_as'}, {}), '(self.update_as)', False, 'from django.forms.models import model_to_dict\n'), ((1914, 14, 1917, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1931, 14, 1934, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1950, 14, 1953, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1963, 21, 1963, 50), 'django.forms.models.model_to_dict', 'model_to_dict', ({(1963, 35, 1963, 49): 'self.update_as'}, {}), '(self.update_as)', False, 'from django.forms.models import model_to_dict\n'), ((1986, 14, 1989, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2015, 14, 2018, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2045, 14, 2048, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2071, 14, 2074, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2089, 14, 2092, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2121, 14, 2124, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2152, 14, 2155, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2177, 14, 2180, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2210, 14, 2213, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2223, 23, 2225, 9), 'projectroles.models.RoleAssignment.objects.get', 'RoleAssignment.objects.get', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2233, 14, 2236, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2273, 14, 2276, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2313, 14, 2316, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2321, 24, 2321, 52), 'json.loads', 'json.loads', ({(2321, 35, 2321, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2354, 14, 2357, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2362, 24, 2362, 52), 'json.loads', 'json.loads', ({(2362, 35, 2362, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2394, 14, 2397, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2412, 17, 2412, 46), 'projectroles.models.ProjectInvite.objects.first', 'ProjectInvite.objects.first', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2440, 14, 2443, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2466, 14, 2469, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2482, 17, 2482, 46), 'projectroles.models.ProjectInvite.objects.first', 'ProjectInvite.objects.first', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2497, 14, 2500, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2527, 14, 2530, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2553, 14, 2556, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2582, 14, 2585, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2623, 14, 2626, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2662, 14, 2665, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2678, 14, 2681, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2692, 14, 2695, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2710, 14, 2713, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2725, 14, 2728, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2753, 14, 2756, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2768, 14, 2771, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2783, 14, 2786, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2800, 14, 2803, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2814, 14, 2817, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((2832, 14, 2832, 51), 'django.urls.reverse', 'reverse', ({(2832, 22, 2832, 50): '"""projectroles:api_user_list"""'}, {}), "('projectroles:api_user_list')", False, 'from django.urls import reverse\n'), ((2839, 24, 2839, 52), 'json.loads', 'json.loads', ({(2839, 35, 2839, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2853, 14, 2853, 51), 'django.urls.reverse', 'reverse', ({(2853, 22, 2853, 50): '"""projectroles:api_user_list"""'}, {}), "('projectroles:api_user_list')", False, 'from django.urls import reverse\n'), ((2858, 24, 2858, 52), 'json.loads', 'json.loads', ({(2858, 35, 2858, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2887, 14, 2887, 54), 'django.urls.reverse', 'reverse', ({(2887, 22, 2887, 53): '"""projectroles:api_user_current"""'}, {}), "('projectroles:api_user_current')", False, 'from django.urls import reverse\n'), ((2894, 24, 2894, 52), 'json.loads', 'json.loads', ({(2894, 35, 2894, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2905, 14, 2905, 54), 'django.urls.reverse', 'reverse', ({(2905, 22, 2905, 53): '"""projectroles:api_user_current"""'}, {}), "('projectroles:api_user_current')", False, 'from django.urls import reverse\n'), ((2910, 24, 2910, 52), 'json.loads', 'json.loads', ({(2910, 35, 2910, 51): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2926, 19, 2929, 9), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((3007, 26, 3007, 44), 'projectroles.remote_projects.RemoteProjectAPI', 'RemoteProjectAPI', ({}, {}), '()', False, 'from projectroles.remote_projects import RemoteProjectAPI\n'), ((218, 12, 220, 13), 'projectroles.plugins.change_plugin_status', 'change_plugin_status', (), '', False, 'from projectroles.plugins import change_plugin_status, get_backend_api\n'), ((223, 26, 223, 77), 'projectroles.models.Role.objects.get_or_create', 'Role.objects.get_or_create', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((224, 29, 226, 9), 'projectroles.models.Role.objects.get_or_create', 'Role.objects.get_or_create', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((227, 32, 229, 9), 'projectroles.models.Role.objects.get_or_create', 'Role.objects.get_or_create', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((230, 26, 230, 77), 'projectroles.models.Role.objects.get_or_create', 'Role.objects.get_or_create', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((536, 28, 536, 56), 'json.loads', 'json.loads', ({(536, 39, 536, 55): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((721, 25, 721, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((737, 25, 737, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((775, 25, 775, 53), 'json.loads', 'json.loads', ({(775, 36, 775, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((781, 25, 781, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((797, 25, 797, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((835, 25, 835, 53), 'json.loads', 'json.loads', ({(835, 36, 835, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((841, 25, 841, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((857, 25, 857, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((895, 25, 895, 53), 'json.loads', 'json.loads', ({(895, 36, 895, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((901, 25, 901, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((917, 25, 917, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((924, 25, 924, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((940, 25, 940, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((946, 25, 946, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((962, 25, 962, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((968, 25, 968, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((984, 25, 984, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((990, 25, 990, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1006, 25, 1006, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1012, 25, 1012, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1028, 25, 1028, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1035, 25, 1035, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1051, 25, 1051, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1075, 25, 1075, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1091, 25, 1091, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1101, 25, 1101, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1117, 25, 1117, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1129, 25, 1129, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1148, 25, 1148, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1186, 25, 1186, 53), 'json.loads', 'json.loads', ({(1186, 36, 1186, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1192, 25, 1192, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1211, 25, 1211, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1249, 25, 1249, 53), 'json.loads', 'json.loads', ({(1249, 36, 1249, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1255, 25, 1255, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1270, 25, 1270, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1311, 25, 1311, 53), 'json.loads', 'json.loads', ({(1311, 36, 1311, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1317, 25, 1317, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1333, 25, 1333, 48), 'projectroles.models.Project.objects.count', 'Project.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1374, 25, 1374, 53), 'json.loads', 'json.loads', ({(1374, 36, 1374, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1603, 25, 1603, 53), 'json.loads', 'json.loads', ({(1603, 36, 1603, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1839, 25, 1839, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1853, 25, 1853, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1874, 25, 1874, 53), 'json.loads', 'json.loads', ({(1874, 36, 1874, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1910, 25, 1910, 53), 'json.loads', 'json.loads', ({(1910, 36, 1910, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1948, 25, 1948, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1959, 25, 1959, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1980, 25, 1980, 53), 'json.loads', 'json.loads', ({(1980, 36, 1980, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((2043, 25, 2043, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2053, 25, 2053, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2069, 25, 2069, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2081, 25, 2081, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2087, 25, 2087, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2097, 25, 2097, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2119, 25, 2119, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2129, 25, 2129, 55), 'projectroles.models.RoleAssignment.objects.count', 'RoleAssignment.objects.count', ({}, {}), '()', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2429, 25, 2429, 53), 'json.loads', 'json.loads', ({(2429, 36, 2429, 52): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((3013, 12, 3016, 13), 'django.urls.reverse', 'reverse', (), '', False, 'from django.urls import reverse\n'), ((1429, 12, 1429, 40), 'json.loads', 'json.loads', ({(1429, 23, 1429, 39): 'response.content'}, {}), '(response.content)', False, 'import json\n'), ((1589, 18, 1593, 9), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1650, 18, 1652, 9), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1724, 18, 1726, 9), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2300, 19, 2300, 33), 'projectroles.utils.build_secret', 'build_secret', ({}, {}), '()', False, 'from projectroles.utils import build_secret\n'), ((2308, 19, 2308, 33), 'projectroles.utils.build_secret', 'build_secret', ({}, {}), '()', False, 'from projectroles.utils import build_secret\n'), ((128, 20, 128, 53), 'pytz.timezone', 'pytz.timezone', ({(128, 34, 128, 52): 'settings.TIME_ZONE'}, {}), '(settings.TIME_ZONE)', False, 'import pytz\n'), ((759, 12, 761, 13), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((819, 12, 821, 13), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((879, 12, 881, 13), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1569, 12, 1569, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1585, 12, 1585, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1630, 12, 1630, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1646, 12, 1646, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1721, 12, 1721, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1750, 12, 1750, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1766, 12, 1766, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1779, 12, 1779, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1803, 12, 1803, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((1819, 12, 1819, 63), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2055, 12, 2057, 13), 'projectroles.models.RoleAssignment.objects.filter', 'RoleAssignment.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2391, 12, 2391, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2408, 12, 2408, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2437, 12, 2437, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2454, 12, 2454, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2463, 12, 2463, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2480, 12, 2480, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2494, 12, 2494, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2513, 12, 2513, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2524, 12, 2524, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2541, 12, 2541, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2550, 12, 2550, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2567, 12, 2567, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2579, 12, 2579, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2596, 12, 2596, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2620, 12, 2620, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((2637, 12, 2637, 62), 'projectroles.models.ProjectInvite.objects.filter', 'ProjectInvite.objects.filter', (), '', False, 'from projectroles.models import Project, Role, RoleAssignment, ProjectInvite, SODAR_CONSTANTS\n'), ((3031, 65, 3031, 79), 'projectroles.utils.build_secret', 'build_secret', ({}, {}), '()', False, 'from projectroles.utils import build_secret\n')]