You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2022/03/29 20:09:23 UTC

[airavata-django-portal] branch AIRAVATA-3562 updated: AIRAVATA-3569 REST API and DB model for storing extended user profile values

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3562
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git


The following commit(s) were added to refs/heads/AIRAVATA-3562 by this push:
     new afd7a09  AIRAVATA-3569 REST API and DB model for storing extended user profile values
afd7a09 is described below

commit afd7a0995bcf0f8efe6ba34c634d1861bfba9e0d
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Mar 29 16:06:11 2022 -0400

    AIRAVATA-3569 REST API and DB model for storing extended user profile values
---
 .../auth/migrations/0015_auto_20220329_1708.py     |  72 ++++++++++++
 django_airavata/apps/auth/models.py                |  62 ++++++++--
 django_airavata/apps/auth/serializers.py           | 129 +++++++++++++++++++++
 django_airavata/apps/auth/urls.py                  |   1 +
 django_airavata/apps/auth/views.py                 |  33 +++++-
 5 files changed, 289 insertions(+), 8 deletions(-)

diff --git a/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py b/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
new file mode 100644
index 0000000..49da5c1
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
@@ -0,0 +1,72 @@
+# Generated by Django 3.2.11 on 2022-03-29 17:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceValueChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('value', models.BigIntegerField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileValue',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+                ('ext_user_profile_field', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_airavata_auth.extendeduserprofilefield')),
+                ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extended_profile', to='django_airavata_auth.userprofile')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileAgreementValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='user_agreement', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('agreement_value', models.BooleanField()),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='multi_choice', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('other_value', models.TextField(blank=True)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileSingleChoiceValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='single_choice', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('choice', models.BigIntegerField(null=True)),
+                ('other_value', models.TextField(blank=True)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileTextValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='text', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('text_value', models.TextField()),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.DeleteModel(
+            name='ExtendedUserProfileInfo',
+        ),
+        migrations.AddField(
+            model_name='extendeduserprofilemultichoicevaluechoice',
+            name='multi_choice_value',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilemultichoicevalue'),
+        ),
+    ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index a4aa13e..b465fd4 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -245,16 +245,64 @@ class ExtendedUserProfileFieldLink(models.Model):
         return f"{self.label} {self.url}"
 
 
-class ExtendedUserProfileInfo(models.Model):
+class ExtendedUserProfileValue(models.Model):
     ext_user_profile_field = models.ForeignKey(ExtendedUserProfileField, on_delete=models.SET_NULL, null=True)
-    id_value = models.BigIntegerField(null=True)
-    text_value = models.CharField(max_length=255, blank=True)
     user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name="extended_profile")
     created_date = models.DateTimeField(auto_now_add=True)
     updated_date = models.DateTimeField(auto_now=True)
 
-    def __str__(self) -> str:
-        if self.id_value:
-            return f"{self.ext_user_profile_field.name} {self.id_value}"
+    @property
+    def value_type(self):
+        if hasattr(self, 'text'):
+            return 'text'
+        elif hasattr(self, 'single_choice'):
+            return 'single_choice'
+        elif hasattr(self, 'multi_choice'):
+            return 'multi_choice'
+        elif hasattr(self, 'user_agreement'):
+            return 'user_agreement'
         else:
-            return f"{self.ext_user_profile_field.name} {self.text_value}"
+            raise Exception("Could not determine value_type")
+
+
+class ExtendedUserProfileTextValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="text")
+    text_value = models.TextField()
+
+
+class ExtendedUserProfileSingleChoiceValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="single_choice")
+    # Only one of value or other_value should be populated, not both
+    choice = models.BigIntegerField(null=True)
+    other_value = models.TextField(blank=True)
+
+
+class ExtendedUserProfileMultiChoiceValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="multi_choice")
+    other_value = models.TextField(blank=True)
+
+
+class ExtendedUserProfileMultiChoiceValueChoice(models.Model):
+    value = models.BigIntegerField()
+    multi_choice_value = models.ForeignKey(ExtendedUserProfileMultiChoiceValue, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileAgreementValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="user_agreement")
+    agreement_value = models.BooleanField()
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index 6508fae..66dd54f 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -218,3 +218,132 @@ class ExtendedUserProfileFieldSerializer(serializers.ModelSerializer):
 
         instance.save()
         return instance
+
+
+class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
+    text_value = serializers.CharField(required=False, allow_blank=True)
+    choices = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=False, min_length=1)
+    other_value = serializers.CharField(required=False, allow_blank=True)
+    agreement_value = serializers.BooleanField(required=False)
+
+    class Meta:
+        model = models.ExtendedUserProfileValue
+        fields = ['id', 'value_type', 'ext_user_profile_field', 'text_value',
+                  'choices', 'other_value', 'agreement_value']
+        read_only_fields = ['value_type']
+
+    def to_representation(self, instance):
+        result = super().to_representation(instance)
+        if instance.value_type == 'text':
+            result['text_value'] = instance.text.text_value
+        elif instance.value_type == 'single_choice':
+            choices = []
+            if instance.single_choice.choice is not None:
+                choices.append(instance.single_choice.choice)
+            result['choices'] = choices
+            result['other_value'] = instance.single_choice.other_value
+        elif instance.value_type == 'multi_choice':
+            result['choices'] = list(map(lambda c: c.value, instance.multi_choice.choices.all()))
+            result['other_value'] = instance.multi_choice.other_value
+        elif instance.value_type == 'user_agreement':
+            result['agreement_value'] = instance.user_agreement.agreement_value
+        return result
+
+    def create(self, validated_data):
+        request = self.context['request']
+        user = request.user
+        user_profile = user.user_profile
+
+        ext_user_profile_field = validated_data.pop('ext_user_profile_field')
+        if ext_user_profile_field.field_type == 'text':
+            text_value = validated_data.pop('text_value')
+            return models.ExtendedUserProfileTextValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                text_value=text_value)
+        elif ext_user_profile_field.field_type == 'single_choice':
+            choices = validated_data.pop('choices', [])
+            choice = choices[0] if len(choices) > 0 else None
+            other_value = validated_data.pop('other_value', '')
+            return models.ExtendedUserProfileSingleChoiceValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                choice=choice,
+                other_value=other_value,
+            )
+        elif ext_user_profile_field.field_type == 'multi_choice':
+            choices = validated_data.pop('choices', [])
+            other_value = validated_data.pop('other_value', '')
+            value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                other_value=other_value,
+            )
+            for choice in choices:
+                models.ExtendedUserProfileMultiChoiceValueChoice.objects.create(
+                    value=choice,
+                    multi_choice_value=value
+                )
+            return value
+        elif ext_user_profile_field.field_type == 'user_agreement':
+            agreement_value = validated_data.get('agreement_value')
+            return models.ExtendedUserProfileAgreementValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                agreement_value=agreement_value
+            )
+
+    def update(self, instance, validated_data):
+        if instance.value_type == 'text':
+            text_value = validated_data.pop('text_value')
+            instance.text.text_value = text_value
+            instance.text.save()
+        elif instance.value_type == 'single_choice':
+            choices = validated_data.pop('choices', [])
+            choice = choices[0] if len(choices) > 0 else None
+            other_value = validated_data.pop('other_value', '')
+            instance.single_choice.choice = choice
+            instance.single_choice.other_value = other_value
+            instance.single_choice.save()
+        elif instance.value_type == 'multi_choice':
+            choices = validated_data.pop('choices', [])
+            other_value = validated_data.pop('other_value', '')
+            # Delete any that are no longer in the set
+            instance.multi_choice.choices.exclude(value__in=choices).delete()
+            # Create records as needed for new entries
+            for choice in choices:
+                models.ExtendedUserProfileMultiChoiceValueChoice.objects.update_or_create(
+                    value=choice, multi_choice_value=instance.multi_choice)
+            instance.multi_choice.other_value = other_value
+            instance.multi_choice.save()
+        elif instance.value_type == 'user_agreement':
+            agreement_value = validated_data.pop('agreement_value')
+            instance.user_agreement.agreement_value = agreement_value
+            instance.user_agreement.save()
+        instance.save()
+        return instance
+
+    def validate(self, attrs):
+        ext_user_profile_field = attrs['ext_user_profile_field']
+        # validate that id_value is only provided for choice fields, and 'text_value' only for the others
+        if ext_user_profile_field.field_type == 'single_choice':
+            choices = attrs.get('choices', [])
+            other_value = attrs.get('other_value', '')
+            # Check that choices are valid
+            for choice in choices:
+                if not ext_user_profile_field.single_choice.choices.filter(id=choice, deleted=False).exists():
+                    raise serializers.ValidationError({'choices': 'Invalid choice.'})
+            if len(choices) > 1:
+                raise serializers.ValidationError({'choices': "Must specify only a single choice."})
+            if len(choices) == 1 and other_value != '':
+                raise serializers.ValidationError("Must specify only a single choice or the other choice, but not both.")
+            if len(choices) == 0 and other_value == '':
+                raise serializers.ValidationError("Must specify one of a single choice or the other choice (but not both).")
+        elif ext_user_profile_field.field_type == 'multi_choice':
+            choices = attrs.get('choices', [])
+            other_value = attrs.get('other_value', '')
+            # Check that choices are valid
+            for choice in choices:
+                if not ext_user_profile_field.multi_choice.choices.filter(id=choice, deleted=False).exists():
+                    raise serializers.ValidationError({'choices': 'Invalid choice.'})
+        return attrs
diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py
index 5ab9dcf..8ca2f53 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -8,6 +8,7 @@ from . import views
 router = routers.DefaultRouter()
 router.register(r'users', views.UserViewSet, basename='user')
 router.register(r'extended-user-profile-fields', views.ExtendedUserProfileFieldViewset, basename='extend-user-profile-field')
+router.register(r'extended-user-profile-values', views.ExtendedUserProfileValueViewset, basename='extend-user-profile-value')
 app_name = 'django_airavata_auth'
 urlpatterns = [
     re_path(r'^', include(router.urls)),
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index 65aa3ec..6948901 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -24,7 +24,7 @@ from django.template.loader import render_to_string
 from django.urls import reverse
 from django.views.decorators.debug import sensitive_variables
 from requests_oauthlib import OAuth2Session
-from rest_framework import permissions, viewsets
+from rest_framework import mixins, permissions, viewsets
 from rest_framework.decorators import action
 from rest_framework.response import Response
 
@@ -718,3 +718,34 @@ class ExtendedUserProfileFieldViewset(viewsets.ModelViewSet):
     def perform_destroy(self, instance):
         instance.deleted = True
         instance.save()
+
+
+class IsExtendedUserProfileOwnerOrReadOnlyForAdmins(permissions.BasePermission):
+    def has_permission(self, request, view):
+        return request.user.is_authenticated
+
+    def has_object_permission(self, request, view, obj):
+        if (request.method in permissions.SAFE_METHODS and
+                request.is_gateway_admin):
+            return True
+        return obj.user_profile.user == request.user
+
+
+class ExtendedUserProfileValueViewset(mixins.CreateModelMixin,
+                                      mixins.RetrieveModelMixin,
+                                      mixins.UpdateModelMixin,
+                                      mixins.ListModelMixin,
+                                      viewsets.GenericViewSet):
+    serializer_class = serializers.ExtendedUserProfileValueSerializer
+    permission_classes = [IsExtendedUserProfileOwnerOrReadOnlyForAdmins]
+
+    def get_queryset(self):
+        user = self.request.user
+        if self.request.is_gateway_admin:
+            queryset = models.ExtendedUserProfileValue.objects.all()
+            user = self.request.query_params.get('user')
+            if user is not None:
+                queryset = queryset.filter(user_profile__user_id=user)
+        else:
+            queryset = user.user_profile.extended_profile.all()
+        return queryset