diff --git a/api/dashboard/organisation/organisation_views.py b/api/dashboard/organisation/organisation_views.py index 0a734603..6b402de1 100644 --- a/api/dashboard/organisation/organisation_views.py +++ b/api/dashboard/organisation/organisation_views.py @@ -17,7 +17,9 @@ AffiliationSerializer, DepartmentSerializer, InstitutionCreateUpdateSerializer, - InstitutionSerializer, InstitutionPrefillSerializer, + InstitutionSerializer, + InstitutionPrefillSerializer, + OrganizationMergerSerializer, ) @@ -129,19 +131,15 @@ def delete(self, request, org_code): class InstitutionAPI(APIView): def get(self, request, org_type, district_id=None): - if district_id: organisations = Organization.objects.filter( - org_type=org_type, - district_id=district_id + org_type=org_type, district_id=district_id ) else: - organisations = Organization.objects.filter( - org_type=org_type - ) + organisations = Organization.objects.filter(org_type=org_type) org_queryset = organisations.select_related( - 'affiliation', + "affiliation", # 'district', # "district__zone__state__country", # "district__zone__state", @@ -197,9 +195,8 @@ class InstitutionCSVAPI(APIView): @role_required([RoleType.ADMIN.value]) def get(self, request, org_type): organizations = ( - Organization.objects.filter( - org_type=org_type - ).select_related( + Organization.objects.filter(org_type=org_type) + .select_related( "affiliation", # "district__zone__state__country", # "district__zone__state", @@ -216,15 +213,9 @@ def get(self, request, org_type): ) ) - serializer = InstitutionSerializer( - organizations, - many=True - ).data + serializer = InstitutionSerializer(organizations, many=True).data - return CommonUtils.generate_csv( - serializer, - f"{org_type} data" - ) + return CommonUtils.generate_csv(serializer, f"{org_type} data") class InstitutionDetailsAPI(APIView): @@ -287,22 +278,13 @@ class AffiliationGetPostUpdateDeleteAPI(APIView): def get(self, request): affiliation = OrgAffiliation.objects.all() paginated_queryset = CommonUtils.get_paginated_queryset( - affiliation, - request, - [ - "id", - "title" - ] + affiliation, request, ["id", "title"] ) serializer = AffiliationSerializer( - paginated_queryset.get("queryset"), - many=True + paginated_queryset.get("queryset"), many=True ) return CustomResponse().paginated_response( - data=serializer.data, - pagination=paginated_queryset.get( - "pagination" - ) + data=serializer.data, pagination=paginated_queryset.get("pagination") ) @role_required([RoleType.ADMIN.value]) @@ -323,9 +305,7 @@ def post(self, request): general_message=f"{request.data.get('title')} added successfully" ).get_success_response() - return CustomResponse( - general_message=serializer.errors - ).get_failure_response() + return CustomResponse(general_message=serializer.errors).get_failure_response() @role_required([RoleType.ADMIN.value]) def put(self, request, affiliation_id): @@ -432,15 +412,9 @@ def delete(self, request, department_id): class AffiliationListAPI(APIView): @role_required([RoleType.ADMIN.value]) def get(self, request): + affiliation = OrgAffiliation.objects.all().values("id", "title") - affiliation = OrgAffiliation.objects.all().values( - 'id', - 'title' - ) - - return CustomResponse( - response=affiliation - ).get_success_response() + return CustomResponse(response=affiliation).get_success_response() class InstitutionPrefillAPI(APIView): @@ -450,14 +424,29 @@ class InstitutionPrefillAPI(APIView): ] ) def get(self, request, org_code): - organization = Organization.objects.filter(code=org_code).first() - serializer = InstitutionPrefillSerializer( - organization, - many=False - ).data + serializer = InstitutionPrefillSerializer(organization, many=False).data - return CustomResponse( - response=serializer - ).get_success_response() + return CustomResponse(response=serializer).get_success_response() + + +class OrganizationMergerView(APIView): + def patch(self, request, organisation_id): + try: + destination = Organization.objects.get(pk=organisation_id) + serializer = OrganizationMergerSerializer(destination, data=request.data) + if not serializer.is_valid(): + return CustomResponse( + general_message=serializer.errors + ).get_failure_response() + serializer.save() + + return CustomResponse( + general_message=f"Organizations merged successfully into {destination.title}." + ).get_success_response() + + except Organization.DoesNotExist as e: + return CustomResponse( + general_message="Organisation id that was given to merge into does not exist" + ).get_failure_response() diff --git a/api/dashboard/organisation/serializers.py b/api/dashboard/organisation/serializers.py index f90171b1..6a9f122f 100644 --- a/api/dashboard/organisation/serializers.py +++ b/api/dashboard/organisation/serializers.py @@ -2,10 +2,20 @@ from django.db.models import Count from rest_framework import serializers - -from db.organization import Organization, District, Zone, State, OrgAffiliation, Department +from django.db import models + +from db.organization import ( + Organization, + District, + Zone, + State, + OrgAffiliation, + Department, +) from utils.permission import JWTUtils from utils.types import OrganizationType +from django.db import transaction +from django.forms.models import model_to_dict class InstitutionSerializer(serializers.ModelSerializer): @@ -27,15 +37,11 @@ class Meta: "zone", "state", "country", - "user_count" + "user_count", ] def get_user_count(self, obj): - return obj.user_organization_link_org.annotate( - user_count=Count( - 'user' - ) - ).count() + return obj.user_organization_link_org.annotate(user_count=Count("user")).count() # class InstitutionSerializer(serializers.ModelSerializer): @@ -142,15 +148,12 @@ def validate_affiliation(self, affiliation_id): class AffiliationSerializer(serializers.ModelSerializer): - label = serializers.ReadOnlyField(source='title') - value = serializers.ReadOnlyField(source='id') + label = serializers.ReadOnlyField(source="title") + value = serializers.ReadOnlyField(source="id") class Meta: model = OrgAffiliation - fields = [ - "value", - "label" - ] + fields = ["value", "label"] class AffiliationCreateUpdateSerializer(serializers.ModelSerializer): @@ -174,14 +177,10 @@ def update(self, instance, validated_data): return instance def validate_title(self, title): - org_affiliation = OrgAffiliation.objects.filter( - title=title - ).first() + org_affiliation = OrgAffiliation.objects.filter(title=title).first() if org_affiliation: - raise serializers.ValidationError( - "Affiliation already exist" - ) + raise serializers.ValidationError("Affiliation already exist") return title @@ -212,16 +211,20 @@ def update(self, instance, validated_data): class InstitutionPrefillSerializer(serializers.ModelSerializer): - affiliation_id = serializers.CharField(source='affiliation.id', allow_null=True) - affiliation_name = serializers.CharField(source='affiliation.name', allow_null=True) - district_id = serializers.CharField(source='district.id', allow_null=True) - district_name = serializers.CharField(source='district.name', allow_null=True) - zone_id = serializers.CharField(source='district.zone.id', allow_null=True) - zone_name = serializers.CharField(source='district.zone.name', allow_null=True) - state_id = serializers.CharField(source='district.state.id', allow_null=True) - state_name = serializers.CharField(source='district.state.name', allow_null=True) - country_id = serializers.CharField(source='district.state.country.id', allow_null=True) - country_name = serializers.CharField(source='district.state.country.name', allow_null=True) + affiliation_id = serializers.CharField(source="affiliation.id", allow_null=True) + affiliation_name = serializers.CharField(source="affiliation.name", allow_null=True) + district_id = serializers.CharField(source="district.id", allow_null=True) + district_name = serializers.CharField(source="district.name", allow_null=True) + zone_id = serializers.CharField(source="district.zone.id", allow_null=True) + zone_name = serializers.CharField(source="district.zone.name", allow_null=True) + state_id = serializers.CharField(source="district.state.id", allow_null=True) + state_name = serializers.CharField(source="district.state.name", allow_null=True) + country_id = serializers.CharField( + source="district.state.country.id", allow_null=True + ) + country_name = serializers.CharField( + source="district.state.country.name", allow_null=True + ) class Meta: model = Organization @@ -239,4 +242,66 @@ class Meta: "state_name", "country_id", "country_name", - ] \ No newline at end of file + ] + + +class OrganizationMergerSerializer(serializers.Serializer): + remove_code = serializers.SlugRelatedField( + slug_field="code", queryset=Organization.objects.all() + ) + + def validate(self, attrs): + if self.instance.code == attrs.get("remove_code"): + raise serializers.ValidationError( + "Keep code and remove code should not be the same." + ) + return super().validate(attrs) + + def update(self, instance, validated_data): + with transaction.atomic(): + remove_org = validated_data["remove_code"] + + # Fetch and iterate over all relations to the Organization model + for relation in Organization._meta.related_objects: + # We're interested in ForeignKey relations only + if isinstance( + relation, + (models.ForeignKey, models.OneToOneField, models.ManyToManyField), + ): + related_model = relation.related_model + related_field_name = relation.field.name + elif isinstance( + relation, + (models.ManyToOneRel, models.ManyToManyRel, models.OneToOneRel), + ): + related_model = relation.related_model + related_field_name = None + for field in related_model._meta.fields: + if ( + isinstance(field, models.ForeignKey) + and field.related_model == Organization + ): + related_field_name = field.name + break + + if ( + not related_field_name + ): # If the related field is not found, skip + continue + else: + continue # Skip other types of relations + + # Update the ForeignKey in the related model + filter_kwargs = {related_field_name: remove_org} + update_kwargs = {related_field_name: instance} + relation_instance = related_model.objects.filter(**filter_kwargs) + if isinstance(relation, (models.OneToOneField, models.OneToOneRel)): + if existing_college := related_model.objects.filter( + **{related_field_name: instance} + ): + existing_college.delete() + + relation_instance.update(**update_kwargs) + remove_org.delete() + + return instance diff --git a/api/dashboard/organisation/urls.py b/api/dashboard/organisation/urls.py index abb57f7a..5c9f9694 100644 --- a/api/dashboard/organisation/urls.py +++ b/api/dashboard/organisation/urls.py @@ -21,4 +21,5 @@ path('departments/edit//', organisation_views.DepartmentAPI.as_view()), path('departments/delete//', organisation_views.DepartmentAPI.as_view()), path('affiliation/list/', organisation_views.AffiliationListAPI.as_view()), + path('merge_organizations//', organisation_views.OrganizationMergerView.as_view()) ] diff --git a/db/organization.py b/db/organization.py index 70f45ba7..f78433d7 100644 --- a/db/organization.py +++ b/db/organization.py @@ -117,7 +117,7 @@ class Meta: class College(models.Model): id = models.CharField(primary_key=True, max_length=36) level = models.IntegerField() - org = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='college_org') + org = models.OneToOneField(Organization, on_delete=models.CASCADE, related_name='college_org', unique=True) updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='updated_by', related_name='college_updated_by') updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by', related_name='college_created_by') @@ -132,7 +132,7 @@ class Meta: class OrgDiscordLink(models.Model): id = models.CharField(primary_key=True, max_length=36) discord_id = models.CharField(unique=True, max_length=36) - org = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='org_discord_link_org_id') + org = models.OneToOneField(Organization, on_delete=models.CASCADE, related_name='org_discord_link_org_id') updated_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='updated_by', related_name='org_discord_link_updated_by') updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey(User, on_delete=models.CASCADE, db_column='created_by', related_name='org_discord_link_created_by')