diff --git a/core/common/views.py b/core/common/views.py index 56d697210..7d6e7dc3f 100644 --- a/core/common/views.py +++ b/core/common/views.py @@ -77,7 +77,7 @@ def has_parent_scope(self): return self.has_owner_scope() and self.has_concept_container_scope() def _should_exclude_retired_from_search_results(self): - if self.is_owner_document_model() or 'expansion' in self.kwargs: + if self.is_owner_document_model() or 'expansion' in self.kwargs or self.is_url_registry_document(): return False params = get(self, 'params') or self.request.query_params.dict() @@ -88,7 +88,8 @@ def should_include_inactive(self): return self.request.query_params.get(INCLUDE_INACTIVE) in TRUTHY def _should_include_private(self): - return self.is_user_document() or self.request.user.is_staff or self.is_user_scope() + return (self.is_user_document() or self.request.user.is_staff or + self.is_user_scope() or self.is_url_registry_document()) def is_verbose(self): return self.request.query_params.get(VERBOSE_PARAM, False) in TRUTHY @@ -468,6 +469,10 @@ def is_user_document(self): from core.users.documents import UserProfileDocument return self.document_model == UserProfileDocument + def is_url_registry_document(self): + from core.url_registry.documents import URLRegistryDocument + return self.document_model == URLRegistryDocument + def is_concept_document(self): from core.concepts.documents import ConceptDocument return self.document_model == ConceptDocument diff --git a/core/integration_tests/tests_url_registries.py b/core/integration_tests/tests_url_registries.py new file mode 100644 index 000000000..56e6ba1cf --- /dev/null +++ b/core/integration_tests/tests_url_registries.py @@ -0,0 +1,302 @@ +from rest_framework.exceptions import ErrorDetail + +from core.common.tests import OCLAPITestCase +from core.orgs.tests.factories import OrganizationFactory +from core.url_registry.factories import GlobalURLRegistryFactory, OrganizationURLRegistryFactory, UserURLRegistryFactory +from core.url_registry.models import URLRegistry +from core.users.tests.factories import UserProfileFactory + + +class URLRegistriesViewTest(OCLAPITestCase): + def test_post_global_registry(self): + user = UserProfileFactory() + response = self.client.post( + '/url-registry/', + { + 'name': 'GlobalRegistry', + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, organization__isnull=True, user__isnull=True, url='https://foo.bar.com', + namespace__isnull=True + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + # duplicate entry + response = self.client.post( + '/url-registry/', + { + 'name': 'GlobalRegistry', + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {'non_fields_error': [ErrorDetail(string='This entry already exists.', code='invalid')]} + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + # entry with same namespace and url but different name is duplicate + response = self.client.post( + '/url-registry/', + { + 'name': 'GlobalRegistry2', + 'namespace': '/registry-1/', + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {'non_fields_error': [ErrorDetail(string='This entry already exists.', code='invalid')]} + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + # entry with same name and namespace but different url is not duplicate + response = self.client.post( + '/url-registry/', + { + 'name': 'GlobalRegistry1', + 'namespace': '/registry-1/', + 'url': 'https://foo.bar.1.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, organization__isnull=True, user__isnull=True, url='https://foo.bar.1.com', + namespace__isnull=True + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 2) + + def test_post_org_registry(self): + org = OrganizationFactory() + user = org.created_by + response = self.client.post( + org.uri + 'url-registry/', + { + 'name': 'GlobalRegistry', + 'namespace': 'Foobar', # will be set correctly + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertEqual(response.data['namespace'], org.uri) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, organization=org, url='https://foo.bar.com' + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + # duplicate entry + response = self.client.post( + org.uri + 'url-registry/', + { + 'name': 'GlobalRegistry', + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {'non_fields_error': [ErrorDetail(string='This entry already exists.', code='invalid')]} + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + response = self.client.post( + org.uri + 'url-registry/', + { + 'name': 'GlobalRegistry', + 'url': 'https://foo.bar.1.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertEqual(response.data['namespace'], org.uri) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, organization=org, url='https://foo.bar.1.com' + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 2) + + response = self.client.post( + '/url-registry/', + { + 'name': 'GlobalRegistry', + 'namespace': org.uri, + 'url': 'https://foo.bar.2.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertEqual(response.data['namespace'], org.uri) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, organization=org, url='https://foo.bar.2.com' + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 3) + + def test_post_user_registry(self): + user = UserProfileFactory() + response = self.client.post( + user.uri + 'url-registry/', + { + 'name': 'GlobalRegistry', + 'namespace': 'Foobar', # will be set correctly + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertEqual(response.data['namespace'], user.uri) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, user=user, url='https://foo.bar.com' + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + # duplicate entry + response = self.client.post( + user.uri + 'url-registry/', + { + 'name': 'GlobalRegistry', + 'url': 'https://foo.bar.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {'non_fields_error': [ErrorDetail(string='This entry already exists.', code='invalid')]} + ) + self.assertEqual(URLRegistry.objects.count(), 1) + + response = self.client.post( + user.uri + 'url-registry/', + { + 'name': 'GlobalRegistry', + 'url': 'https://foo.bar.1.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertEqual(response.data['namespace'], user.uri) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, user=user, url='https://foo.bar.1.com' + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 2) + + response = self.client.post( + '/url-registry/', + { + 'name': 'GlobalRegistry', + 'namespace': user.uri, + 'url': 'https://foo.bar.2.com', + }, + HTTP_AUTHORIZATION=f"Token {user.get_token()}", + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.data['id']) + self.assertEqual(response.data['namespace'], user.uri) + self.assertTrue( + URLRegistry.objects.filter( + is_active=True, user=user, url='https://foo.bar.2.com' + ).exists() + ) + self.assertEqual(URLRegistry.objects.count(), 3) + + def test_get(self): + global_registry = GlobalURLRegistryFactory(name='global') + org_registry = OrganizationURLRegistryFactory(name='org') + user_registry = UserURLRegistryFactory(name='user') + + response = self.client.get('/url-registry/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'global') + self.assertEqual(response.data[0]['id'], global_registry.id) + + response = self.client.get(org_registry.owner.uri + 'url-registry/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'org') + self.assertEqual(response.data[0]['id'], org_registry.id) + + response = self.client.get(user_registry.owner.uri + 'url-registry/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'user') + self.assertEqual(response.data[0]['id'], user_registry.id) + + response = self.client.get(user_registry.owner.uri + 'orgs/url-registry/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 0) + + org_registry.organization.members.add(user_registry.user) + + response = self.client.get(user_registry.owner.uri + 'orgs/url-registry/') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'org') + self.assertEqual(response.data[0]['id'], org_registry.id) + + response = self.client.get( + '/user/orgs/url-registry/', + HTTP_AUTHORIZATION=f"Token {user_registry.user.get_token()}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'org') + self.assertEqual(response.data[0]['id'], org_registry.id) diff --git a/core/orgs/urls.py b/core/orgs/urls.py index 2557ec9b8..eba5da2b9 100644 --- a/core/orgs/urls.py +++ b/core/orgs/urls.py @@ -51,6 +51,9 @@ views.OrganizationMemberView.as_view(), name='organization-member-detail' ), + re_path( + r'^(?P' + NAMESPACE_PATTERN + ')/url-registry/', include('core.url_registry.urls'), + name='org-url-registry-urls'), re_path(r'^(?P' + NAMESPACE_PATTERN + ')/repos/', include('core.repos.urls'), name='org-repos-urls'), re_path(r'^(?P' + NAMESPACE_PATTERN + ')/sources/', include('core.sources.urls')), re_path(r'^(?P' + NAMESPACE_PATTERN + ')/CodeSystem/', include('core.code_systems.urls'), diff --git a/core/settings.py b/core/settings.py index 2dbb040b4..7a203e980 100644 --- a/core/settings.py +++ b/core/settings.py @@ -100,6 +100,7 @@ 'core.tasks', 'core.toggles', 'core.repos', + 'core.url_registry', ] REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( diff --git a/core/url_registry/__init__.py b/core/url_registry/__init__.py new file mode 100644 index 000000000..7224579be --- /dev/null +++ b/core/url_registry/__init__.py @@ -0,0 +1,4 @@ +from django.conf import settings + + +__version__ = settings.VERSION diff --git a/core/url_registry/documents.py b/core/url_registry/documents.py new file mode 100644 index 000000000..5f69ba25e --- /dev/null +++ b/core/url_registry/documents.py @@ -0,0 +1,86 @@ +import json + +from django_elasticsearch_dsl import Document, fields +from django_elasticsearch_dsl.registries import registry + +from core.common.utils import jsonify_safe, flatten_dict +from core.orgs.models import Organization +from core.url_registry.models import URLRegistry + + +@registry.register_document +class URLRegistryDocument(Document): + class Index: + name = 'url_registries' + settings = {'number_of_shards': 1, 'number_of_replicas': 0} + + namespace = fields.TextField(attr='namespace') + url = fields.TextField(attr='url') + _url = fields.KeywordField(attr='url') + name = fields.TextField(attr='name') + _name = fields.KeywordField(attr='name', normalizer='lowercase') + extras = fields.ObjectField(dynamic=True) + last_update = fields.DateField(attr='updated_at') + updated_by = fields.KeywordField(attr='updated_by.username') + + class Django: + model = URLRegistry + + @staticmethod + def get_match_phrase_attrs(): + return ['_url', '_name', 'namespace'] + + @staticmethod + def get_exact_match_attrs(): + return { + 'url': { + 'boost': 4, + }, + 'namespace': { + 'boost': 3.5, + }, + 'name': { + 'boost': 3, + } + } + + @staticmethod + def get_wildcard_search_attrs(): + return { + 'url': { + 'boost': 1, + 'wildcard': True, + 'lower': True + }, + 'namespace': { + 'boost': 0.8, + 'wildcard': True, + 'lower': True + }, + 'name': { + 'boost': 0.6, + 'wildcard': True, + 'lower': True + } + } + + @staticmethod + def get_fuzzy_search_attrs(): + return { + 'namespace': { + 'boost': 0.8, + } + } + + @staticmethod + def prepare_extras(instance): + value = {} + + if instance.extras: + value = jsonify_safe(instance.extras) + if isinstance(value, dict): + value = flatten_dict(value) + + if value: + value = json.loads(json.dumps(value)) + return value or {} diff --git a/core/url_registry/factories.py b/core/url_registry/factories.py new file mode 100644 index 000000000..3bfaa37bd --- /dev/null +++ b/core/url_registry/factories.py @@ -0,0 +1,32 @@ +import factory +from factory import Sequence, SubFactory + +from core.orgs.tests.factories import OrganizationFactory +from core.url_registry.models import URLRegistry +from core.users.tests.factories import UserProfileFactory + + +class GlobalURLRegistryFactory(factory.django.DjangoModelFactory): + class Meta: + model = URLRegistry + + name = Sequence("GlobalRegistry-{}".format) # pylint: disable=consider-using-f-string + url = 'https://foo.bar.com' + + +class OrganizationURLRegistryFactory(factory.django.DjangoModelFactory): + class Meta: + model = URLRegistry + + name = Sequence("OrgRegistry-{}".format) # pylint: disable=consider-using-f-string + url = 'https://foo.bar.com' + organization = SubFactory(OrganizationFactory) + + +class UserURLRegistryFactory(factory.django.DjangoModelFactory): + class Meta: + model = URLRegistry + + name = Sequence("UserRegistry-{}".format) # pylint: disable=consider-using-f-string + url = 'https://foo.bar.com' + user = SubFactory(UserProfileFactory) diff --git a/core/url_registry/migrations/0001_initial.py b/core/url_registry/migrations/0001_initial.py new file mode 100644 index 000000000..2cd3eee35 --- /dev/null +++ b/core/url_registry/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.4 on 2024-01-11 09:40 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('orgs', '0021_organization_checksums'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='URLRegistry', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_active', models.BooleanField(default=True)), + ('extras', models.JSONField(blank=True, default=dict, null=True)), + ('url', models.URLField()), + ('name', models.TextField(blank=True, null=True)), + ('namespace', models.CharField(blank=True, max_length=300, null=True)), + ('created_by', models.ForeignKey(default=1, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_related_created_by', related_query_name='%(app_label)s_%(class)ss_created_by', to=settings.AUTH_USER_MODEL)), + ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='url_registries', to='orgs.organization')), + ('updated_by', models.ForeignKey(default=1, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_related_updated_by', related_query_name='%(app_label)s_%(class)ss_updated_by', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='url_registries', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'url_registries', + }, + ), + migrations.AddConstraint( + model_name='urlregistry', + constraint=models.UniqueConstraint(condition=models.Q(('is_active', True), ('user__isnull', False)), fields=('user', 'url'), name='user_url_unique'), + ), + migrations.AddConstraint( + model_name='urlregistry', + constraint=models.UniqueConstraint(condition=models.Q(('is_active', True), ('organization__isnull', False)), fields=('organization', 'url'), name='org_url_unique'), + ), + migrations.AddConstraint( + model_name='urlregistry', + constraint=models.UniqueConstraint(condition=models.Q(('is_active', True), ('namespace__isnull', True)), fields=('url',), name='global_url_unique'), + ), + ] diff --git a/core/url_registry/migrations/__init__.py b/core/url_registry/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/core/url_registry/models.py b/core/url_registry/models.py new file mode 100644 index 000000000..f7ee5a330 --- /dev/null +++ b/core/url_registry/models.py @@ -0,0 +1,87 @@ +from django.db import models + +from core.common.models import BaseModel + + +class URLRegistry(BaseModel): + url = models.URLField() + name = models.TextField(null=True, blank=True) + namespace = models.CharField(max_length=300, null=True, blank=True) + organization = models.ForeignKey( + 'orgs.Organization', on_delete=models.CASCADE, null=True, blank=True, + related_name='url_registries' + ) + user = models.ForeignKey( + 'users.UserProfile', on_delete=models.CASCADE, null=True, blank=True, + related_name='url_registries' + ) + public_access = None + uri = None + + es_fields = { + 'name': {'sortable': False, 'filterable': True, 'exact': True}, + '_name': {'sortable': True, 'filterable': False, 'exact': False}, + 'namespace': {'sortable': False, 'filterable': True, 'exact': True}, + '_namespace': {'sortable': True, 'filterable': False, 'exact': False}, + 'url': {'sortable': False, 'filterable': True, 'exact': True}, + '_url': {'sortable': True, 'filterable': False, 'exact': False}, + 'last_update': {'sortable': True, 'default': 'desc', 'filterable': False}, + 'updated_by': {'sortable': False, 'filterable': False, 'facet': True} + } + + class Meta: + db_table = 'url_registries' + constraints = [ + models.UniqueConstraint( + condition=models.Q(is_active=True, user__isnull=False), + fields=('user', 'url'), name='user_url_unique' + ), + models.UniqueConstraint( + condition=models.Q(is_active=True, organization__isnull=False), + fields=('organization', 'url'), name='org_url_unique' + ), + models.UniqueConstraint( + condition=models.Q(is_active=True, namespace__isnull=True), + fields=('url',), name='global_url_unique' + ), + ] + + def _set_owner_from_uri(self): + if '/orgs/' in self.namespace: + from core.orgs.models import Organization + self.organization = Organization.objects.filter(uri=self.namespace).first() + elif '/users/' in self.namespace: + from core.users.models import UserProfile + self.user = UserProfile.objects.filter(uri=self.namespace).first() + + def clean(self): + owner = self.owner + if owner: + self.namespace = owner.uri + if not owner and self.namespace: + self._set_owner_from_uri() + if not self.owner: + self.namespace = None + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + @property + def owner(self): + return self.organization or self.user + + def is_uniq(self): + return not self.get_active_entries().filter(url=self.url).exists() + + def get_active_entries(self): + queryset = URLRegistry.objects.filter(is_active=True) + + if self.organization: + queryset = queryset.filter(organization_id=self.organization) + elif self.user: + queryset = queryset.filter(user=self.user) + else: + queryset = queryset.filter(namespace__isnull=True) + + return queryset diff --git a/core/url_registry/serializers.py b/core/url_registry/serializers.py new file mode 100644 index 000000000..6a8f773b7 --- /dev/null +++ b/core/url_registry/serializers.py @@ -0,0 +1,47 @@ +from rest_framework.fields import CharField, BooleanField, DateTimeField +from rest_framework.serializers import ModelSerializer + +from core.url_registry.models import URLRegistry + + +class URLRegistryBaseSerializer(ModelSerializer): + class Meta: + model = URLRegistry + fields = ['id', 'name', 'url', 'namespace'] + + +class URLRegistryDetailSerializer(URLRegistryBaseSerializer): + created_by = CharField(source='created_by.username', read_only=True) + updated_by = CharField(source='updated_by.username', read_only=True) + is_active = BooleanField(read_only=True) + created_at = DateTimeField(read_only=True) + updated_at = DateTimeField(read_only=True) + + class Meta: + model = URLRegistry + fields = URLRegistryBaseSerializer.Meta.fields + [ + 'created_by', 'updated_by', 'created_at', 'updated_at', 'is_active', 'extras'] + + def prepare_object(self, validated_data): + user = self.context['request'].user + view = self.context['view'] + + url_registry = URLRegistry( + name=validated_data.get('name'), + namespace=validated_data.get('namespace'), + url=validated_data.get('url'), + created_by=user, + updated_by=user, + extras=validated_data.get('extras', {}), + ) + if view.parent_resource_type and view.parent_resource: + setattr(url_registry, view.parent_resource_type, view.parent_resource) + return url_registry + + def create(self, validated_data): + url_registry = self.prepare_object(validated_data) + if not url_registry.is_uniq(): + self._errors['non_fields_error'] = ['This entry already exists.'] + if not self._errors: + url_registry.save() + return url_registry diff --git a/core/url_registry/tests.py b/core/url_registry/tests.py new file mode 100644 index 000000000..e69de29bb diff --git a/core/url_registry/urls.py b/core/url_registry/urls.py new file mode 100644 index 000000000..6a46f6237 --- /dev/null +++ b/core/url_registry/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from core.url_registry import views + +urlpatterns = [ + path('', views.URLRegistriesView.as_view(), name='url-registries'), +] diff --git a/core/url_registry/views.py b/core/url_registry/views.py new file mode 100644 index 000000000..f20b270f4 --- /dev/null +++ b/core/url_registry/views.py @@ -0,0 +1,79 @@ +from django.http import Http404 +from drf_yasg.utils import swagger_auto_schema +from rest_framework.generics import CreateAPIView +from rest_framework.permissions import IsAuthenticatedOrReadOnly + +from core.common.mixins import ListWithHeadersMixin +from core.common.swagger_parameters import q_param, limit_param, sort_desc_param, sort_asc_param, page_param +from core.common.views import BaseAPIView +from core.url_registry.documents import URLRegistryDocument +from core.url_registry.models import URLRegistry +from core.url_registry.serializers import URLRegistryDetailSerializer + + +class URLRegistriesView(BaseAPIView, ListWithHeadersMixin, CreateAPIView): + permission_classes = (IsAuthenticatedOrReadOnly,) + serializer_class = URLRegistryDetailSerializer + queryset = URLRegistry.objects.filter(is_active=True) + parent_resource = None + parent_resource_type = None + + document_model = URLRegistryDocument + es_fields = URLRegistry.es_fields + is_searchable = True + + def set_parent_resource(self): + from core.orgs.models import Organization + from core.users.models import UserProfile + org = self.kwargs.get('org', None) + user = self.kwargs.get('user', None) + if not user and self.user_is_self: + user = self.request.user.username + if org: + self.parent_resource = Organization.objects.filter(mnemonic=org).first() + self.parent_resource_type = 'organization' + elif user: + self.parent_resource = UserProfile.objects.filter(username=user).first() + self.parent_resource_type = 'user' + + self.kwargs['parent_resource'] = self.parent_resource + self.kwargs['parent_resource_type'] = self.parent_resource_type + + def get_queryset(self): + self.set_parent_resource() + + queryset = self.queryset + if self.parent_resource_type: + queryset = queryset.filter( + **{f"{self.parent_resource_type}__{self.parent_resource.mnemonic_attr}": self.parent_resource.mnemonic} + ) + else: + queryset = queryset.filter(organization__isnull=True, user__isnull=True) + + return queryset + + @swagger_auto_schema( + manual_parameters=[ + q_param, limit_param, sort_desc_param, sort_asc_param, page_param, + ] + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + self.set_parent_resource() + return super().create(request, *args, **kwargs) + + def perform_create(self, serializer): + serializer.save() + serializer.is_valid(raise_exception=True) + + +class UserOrgURLRegistriesView(URLRegistriesView): + def get_queryset(self): + self.set_parent_resource() + + if not self.parent_resource: + raise Http404() + + return self.queryset.filter(organization__members__username=self.parent_resource.username) \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 623f31bae..7116acd42 100644 --- a/core/urls.py +++ b/core/urls.py @@ -65,6 +65,7 @@ path('orgs/', include('core.orgs.urls'), name='orgs_url'), path('sources/', include('core.sources.urls'), name='sources_url'), path('repos/', include('core.repos.urls'), name='repos_url'), + path('url-registry/', include('core.url_registry.urls'), name='url_registry_url'), #TODO: require FHIR subdomain path('fhir/CodeSystem/', include('core.code_systems.urls'), name='code_systems_urls'), path('fhir/ValueSet/', include('core.value_sets.urls'), name='value_sets_urls'), diff --git a/core/users/urls.py b/core/users/urls.py index 816b37d62..f5188dc16 100644 --- a/core/users/urls.py +++ b/core/users/urls.py @@ -4,6 +4,7 @@ from core.orgs import views as org_views from . import views from ..repos.views import OrganizationRepoListView +from ..url_registry.views import UserOrgURLRegistriesView urlpatterns = [ re_path(r'^$', views.UserListView.as_view(), name='userprofile-list'), @@ -72,12 +73,18 @@ OrganizationRepoListView.as_view(), name='userprofile-organization-repo-list', ), + re_path( + r'^(?P' + NAMESPACE_PATTERN + ')/orgs/url-registry/$', + UserOrgURLRegistriesView.as_view(), + name='userprofile-organization-url-registry-list', + ), re_path( r"^(?P{pattern})/extras/(?P{pattern})/$".format(pattern=NAMESPACE_PATTERN), views.UserExtraRetrieveUpdateDestroyView.as_view(), name='user-extra' ), re_path(r'^(?P' + NAMESPACE_PATTERN + ')/repos/', include('core.repos.urls')), + re_path(r'^(?P' + NAMESPACE_PATTERN + ')/url-registry/', include('core.url_registry.urls')), re_path(r'^(?P' + NAMESPACE_PATTERN + ')/sources/', include('core.sources.urls')), #TODO: require FHIR subdomain re_path(r'^(?P' + NAMESPACE_PATTERN + ')/CodeSystem/', include('core.code_systems.urls'), diff --git a/core/users/user_urls.py b/core/users/user_urls.py index 3af22cd5a..380bb8a00 100644 --- a/core/users/user_urls.py +++ b/core/users/user_urls.py @@ -3,6 +3,7 @@ from core.common.constants import NAMESPACE_PATTERN from core.orgs import views as orgs_views from core.repos.views import OrganizationRepoListView +from core.url_registry.views import UserOrgURLRegistriesView from core.users import views extra_kwargs = {'user_is_self': True} @@ -36,8 +37,15 @@ re_path( r'^orgs/repos/$', OrganizationRepoListView.as_view(), + extra_kwargs, name='user-organization-repo-list', ), + re_path( + r'^orgs/url-registry/$', + UserOrgURLRegistriesView.as_view(), + extra_kwargs, + name='user-organization-url-registry-list', + ), re_path( fr"^extras/(?P{NAMESPACE_PATTERN})/$", views.UserExtraRetrieveUpdateDestroyView.as_view(), @@ -47,5 +55,6 @@ re_path(r'^sources/', include('core.sources.urls'), extra_kwargs), re_path(r'^collections/', include('core.collections.urls'), extra_kwargs), re_path(r'^repos/', include('core.repos.urls'), extra_kwargs), + re_path(r'^url-registry/', include('core.url_registry.urls'), extra_kwargs), re_path(r'^pins/', include('core.pins.urls'), extra_kwargs) ]