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