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

commit 4e87cde731c5a235e065af05d7c870f9209cb78a
Author: Marcus Christie <[email protected]>
AuthorDate: Fri Mar 11 12:56:06 2022 -0500

    AIRAVATA-3563 DB and REST API for extended user profile fields
---
 ...ld_extendeduserprofilefieldlink_extendeduser.py | 107 ++++++++++++++++++
 django_airavata/apps/auth/models.py                | 109 +++++++++++++++++++
 django_airavata/apps/auth/serializers.py           | 120 +++++++++++++++++++++
 django_airavata/apps/auth/urls.py                  |   1 +
 django_airavata/apps/auth/views.py                 |  17 +++
 5 files changed, 354 insertions(+)

diff --git 
a/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
 
b/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
new file mode 100644
index 0000000..2b5601c
--- /dev/null
+++ 
b/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
@@ -0,0 +1,107 @@
+# Generated by Django 3.2.11 on 2022-03-11 14:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0013_auto_20220118_1650'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ExtendedUserProfileField',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=64)),
+                ('help_text', models.TextField(blank=True)),
+                ('order', models.IntegerField()),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+                ('deleted', models.BooleanField(default=False)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileAgreementField',
+            fields=[
+                ('field_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.extendeduserprofilefield')),
+                ('checkbox_label', models.TextField(blank=True)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceField',
+            fields=[
+                ('field_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.extendeduserprofilefield')),
+                ('other', models.BooleanField(default=False)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileSingleChoiceField',
+            fields=[
+                ('field_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.extendeduserprofilefield')),
+                ('other', models.BooleanField(default=False)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileTextField',
+            fields=[
+                ('field_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.extendeduserprofilefield')),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileInfo',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('id_value', models.BigIntegerField(null=True)),
+                ('text_value', models.CharField(blank=True, max_length=255)),
+                ('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='ExtendedUserProfileFieldLink',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('label', models.TextField()),
+                ('url', models.URLField()),
+                ('order', models.IntegerField()),
+                ('display_link', models.BooleanField(default=True)),
+                ('display_inline', models.BooleanField(default=False)),
+                ('field', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='links', to='django_airavata_auth.extendeduserprofilefield')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileSingleChoiceFieldChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('display_text', models.CharField(max_length=255)),
+                ('order', models.IntegerField()),
+                ('deleted', models.BooleanField(default=False)),
+                ('single_choice_field', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='choices', 
to='django_airavata_auth.extendeduserprofilesinglechoicefield')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceFieldChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('display_text', models.CharField(max_length=255)),
+                ('order', models.IntegerField()),
+                ('deleted', models.BooleanField(default=False)),
+                ('multi_choice_field', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='choices', 
to='django_airavata_auth.extendeduserprofilemultichoicefield')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/django_airavata/apps/auth/models.py 
b/django_airavata/apps/auth/models.py
index 54395e0..a4aa13e 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -149,3 +149,112 @@ class PendingEmailChange(models.Model):
         max_length=36, unique=True, default=uuid.uuid4)
     created_date = models.DateTimeField(auto_now_add=True)
     verified = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileField(models.Model):
+    name = models.CharField(max_length=64)
+    help_text = models.TextField(blank=True)
+    order = models.IntegerField()
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now=True)
+    deleted = models.BooleanField(default=False)
+
+    def __str__(self) -> str:
+        return f"{self.name} ({self.id})"
+
+    @property
+    def field_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:
+            raise Exception("Could not determine field_type")
+
+
+class ExtendedUserProfileTextField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="text")
+
+
+class ExtendedUserProfileSingleChoiceField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="single_choice")
+    other = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileFieldChoice(models.Model):
+    display_text = models.CharField(max_length=255)
+    order = models.IntegerField()
+    deleted = models.BooleanField(default=False)
+
+    class Meta:
+        abstract = True
+
+    def __str__(self) -> str:
+        return f"{self.display_text} ({self.id})"
+
+
+class 
ExtendedUserProfileSingleChoiceFieldChoice(ExtendedUserProfileFieldChoice):
+    single_choice_field = 
models.ForeignKey(ExtendedUserProfileSingleChoiceField, 
on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileMultiChoiceField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="multi_choice")
+    other = models.BooleanField(default=False)
+
+
+class 
ExtendedUserProfileMultiChoiceFieldChoice(ExtendedUserProfileFieldChoice):
+    multi_choice_field = 
models.ForeignKey(ExtendedUserProfileMultiChoiceField, 
on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileAgreementField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="user_agreement")
+    # if no checkbox label, then some default text will be used
+    checkbox_label = models.TextField(blank=True)
+
+
+class ExtendedUserProfileFieldLink(models.Model):
+    label = models.TextField()
+    url = models.URLField()
+    order = models.IntegerField()
+    display_link = models.BooleanField(default=True)
+    display_inline = models.BooleanField(default=False)
+    # Technically any field can have links
+    field = models.ForeignKey(ExtendedUserProfileField, 
on_delete=models.CASCADE, related_name="links")
+
+    def __str__(self) -> str:
+        return f"{self.label} {self.url}"
+
+
+class ExtendedUserProfileInfo(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}"
+        else:
+            return f"{self.ext_user_profile_field.name} {self.text_value}"
diff --git a/django_airavata/apps/auth/serializers.py 
b/django_airavata/apps/auth/serializers.py
index c10b918..6508fae 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -98,3 +98,123 @@ class UserSerializer(serializers.ModelSerializer):
             "url": verification_uri,
         })
         utils.send_email_to_user(models.VERIFY_EMAIL_CHANGE_TEMPLATE, context)
+
+
+class ExtendedUserProfileFieldChoiceSerializer(serializers.Serializer):
+    id = serializers.IntegerField(required=False)
+    display_text = serializers.CharField()
+    order = serializers.IntegerField()
+
+
+class ExtendedUserProfileFieldLinkSerializer(serializers.Serializer):
+    id = serializers.IntegerField(required=False)
+    label = serializers.CharField()
+    url = serializers.URLField()
+    order = serializers.IntegerField()
+    display_link = serializers.BooleanField(default=True)
+    display_inline = serializers.BooleanField(default=False)
+
+
+class ExtendedUserProfileFieldSerializer(serializers.ModelSerializer):
+    field_type = serializers.ChoiceField(choices=["text", "single_choice", 
"multi_choice", "user_agreement"])
+    other = serializers.BooleanField(required=False)
+    choices = ExtendedUserProfileFieldChoiceSerializer(required=False, 
many=True)
+    checkbox_label = serializers.CharField(allow_blank=True, required=False)
+    links = ExtendedUserProfileFieldLinkSerializer(required=False, many=True)
+
+    class Meta:
+        model = models.ExtendedUserProfileField
+        fields = ['id', 'name', 'help_text', 'order', 'created_date',
+                  'updated_date', 'field_type', 'other', 'choices', 
'checkbox_label', 'links']
+        read_only_fields = ('created_date', 'updated_date')
+
+    def to_representation(self, instance):
+        result = super().to_representation(instance)
+        if instance.field_type == 'single_choice':
+            result['other'] = instance.single_choice.other
+            result['choices'] = 
ExtendedUserProfileFieldChoiceSerializer(instance.single_choice.choices.filter(deleted=False).order_by('order'),
 many=True).data
+        if instance.field_type == 'multi_choice':
+            result['other'] = instance.multi_choice.other
+            result['choices'] = 
ExtendedUserProfileFieldChoiceSerializer(instance.multi_choice.choices.filter(deleted=False).order_by('order'),
 many=True).data
+        if instance.field_type == 'user_agreement':
+            result['checkbox_label'] = instance.user_agreement.checkbox_label
+        result['links'] = 
ExtendedUserProfileFieldLinkSerializer(instance.links.order_by('order'), 
many=True).data
+        return result
+
+    def create(self, validated_data):
+        field_type = validated_data.pop('field_type')
+        other = validated_data.pop('other', False)
+        choices = validated_data.pop('choices', [])
+        checkbox_label = validated_data.pop('checkbox_label', '')
+        links = validated_data.pop('links', [])
+        if field_type == 'text':
+            instance = 
models.ExtendedUserProfileTextField.objects.create(**validated_data)
+        elif field_type == 'single_choice':
+            instance = 
models.ExtendedUserProfileSingleChoiceField.objects.create(**validated_data, 
other=other)
+            # add choices
+            for choice in choices:
+                choice.pop('id', None)
+                instance.choices.create(**choice)
+        elif field_type == 'multi_choice':
+            instance = 
models.ExtendedUserProfileMultiChoiceField.objects.create(**validated_data, 
other=other)
+            # add choices
+            for choice in choices:
+                choice.pop('id', None)
+                instance.choices.create(**choice)
+        elif field_type == 'user_agreement':
+            instance = 
models.ExtendedUserProfileAgreementField.objects.create(**validated_data, 
checkbox_label=checkbox_label)
+        else:
+            raise Exception(f"Unrecognized field type: {field_type}")
+        # create links
+        for link in links:
+            link.pop('id', None)
+            instance.links.create(**link)
+        return instance
+
+    def update(self, instance, validated_data):
+        instance.name = validated_data['name']
+        instance.help_text = validated_data['help_text']
+        instance.order = validated_data['order']
+        # logger.debug(f"instance.field_type={instance.field_type}, 
validated_data={validated_data}")
+        if instance.field_type == 'single_choice':
+            instance.single_choice.other = validated_data.get('other', 
instance.single_choice.other)
+            choices = validated_data.pop('choices', None)
+            if choices:
+                choice_ids = [choice['id'] for choice in choices if 'id' in 
choice]
+                # Soft delete any choices that are not in the list
+                
instance.single_choice.choices.exclude(id__in=choice_ids).update(deleted=True)
+                for choice in choices:
+                    choice_id = choice.pop('id', None)
+                    
models.ExtendedUserProfileSingleChoiceFieldChoice.objects.update_or_create(
+                        id=choice_id,
+                        defaults=choice,
+                    )
+        elif instance.field_type == 'multi_choice':
+            instance.multi_choice.other = validated_data.get('other', 
instance.multi_choice.other)
+            choices = validated_data.pop('choices', None)
+            if choices:
+                choice_ids = [choice['id'] for choice in choices if 'id' in 
choice]
+                # Soft delete any choices that are not in the list
+                
instance.multi_choice.choices.exclude(id__in=choice_ids).update(deleted=True)
+                for choice in choices:
+                    choice_id = choice.pop('id', None)
+                    
models.ExtendedUserProfileMultiChoiceFieldChoice.objects.update_or_create(
+                        id=choice_id,
+                        defaults=choice,
+                    )
+        elif instance.field_type == 'user_agreement':
+            instance.user_agreement.checkbox_label = 
validated_data.pop('checkbox_label', instance.user_agreement.checkbox_label)
+
+        # update links
+        links = validated_data.pop('links', [])
+        link_ids = [link['id'] for link in links if 'id' in link]
+        instance.links.exclude(id__in=link_ids).delete()
+        for link in links:
+            link_id = link.pop('id', None)
+            models.ExtendedUserProfileFieldLink.objects.update_or_create(
+                id=link_id,
+                defaults=link,
+            )
+
+        instance.save()
+        return instance
diff --git a/django_airavata/apps/auth/urls.py 
b/django_airavata/apps/auth/urls.py
index 89909a5..5ab9dcf 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -7,6 +7,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')
 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 edf8861..65aa3ec 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -28,6 +28,7 @@ from rest_framework import permissions, viewsets
 from rest_framework.decorators import action
 from rest_framework.response import Response
 
+from django_airavata.apps.api.view_utils import IsInAdminsGroupPermission
 from django_airavata.apps.auth import serializers
 
 from . import forms, iam_admin_client, models, utils
@@ -701,3 +702,19 @@ def get_client_secret(access_token, client_endpoint):
     r = requests.get(client_endpoint + "/client-secret", headers=headers)
     r.raise_for_status()
     return r.json()['value']
+
+
+class ExtendedUserProfileFieldViewset(viewsets.ModelViewSet):
+    serializer_class = serializers.ExtendedUserProfileFieldSerializer
+    queryset = models.ExtendedUserProfileField.objects.all().order_by('order')
+    permission_classes = [IsInAdminsGroupPermission]
+
+    def get_queryset(self):
+        queryset = super().get_queryset()
+        if self.action == 'list':
+            queryset = queryset.filter(deleted=False)
+        return queryset
+
+    def perform_destroy(self, instance):
+        instance.deleted = True
+        instance.save()

Reply via email to