From d0ca68149a4ae1ee6ac8309741361d2ed22c1b71 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 27 Dec 2023 18:36:52 +0800 Subject: [PATCH] Improved serializer for transaction and added available equipment viewset --- equipment_tracker/equipments/admin.py | 4 +- equipment_tracker/equipments/models.py | 19 ++- equipment_tracker/equipments/urls.py | 4 + equipment_tracker/equipments/views.py | 27 ++++ equipment_tracker/schema.yml | 34 +++++ equipment_tracker/transactions/serializers.py | 126 ++++++++++++------ 6 files changed, 167 insertions(+), 47 deletions(-) diff --git a/equipment_tracker/equipments/admin.py b/equipment_tracker/equipments/admin.py index 561282d..6e3dcea 100644 --- a/equipment_tracker/equipments/admin.py +++ b/equipment_tracker/equipments/admin.py @@ -6,11 +6,11 @@ from .models import Equipment, EquipmentInstance @admin.register(Equipment) class EquipmentAdmin(SimpleHistoryAdmin): readonly_fields = ('date_added', 'last_updated') - list_display = ('name', 'date_added', 'last_updated') + list_display = ('id', 'name', 'date_added', 'last_updated') @admin.register(EquipmentInstance) class EquipmentInstanceAdmin(SimpleHistoryAdmin): readonly_fields = ('date_added', 'last_updated') - list_display = ('equipment', 'status', 'remarks', + list_display = ('id', 'equipment', 'status', 'remarks', 'date_added', 'last_updated') diff --git a/equipment_tracker/equipments/models.py b/equipment_tracker/equipments/models.py index 47a8950..94d77d5 100644 --- a/equipment_tracker/equipments/models.py +++ b/equipment_tracker/equipments/models.py @@ -19,12 +19,13 @@ class Equipment(models.Model): history = HistoricalRecords() def __str__(self): - return f'{self.name} ID:{self.id}' + return f'{self.name}' class EquipmentInstance(models.Model): EQUIPMENT_INSTANCE_STATUS_CHOICES = ( ('Available', 'Available'), + ('Pending', 'Pending'), ('Broken', 'Broken'), ('Borrowed', 'Borrowed'), ) @@ -37,7 +38,7 @@ class EquipmentInstance(models.Model): history = HistoricalRecords() def __str__(self): - return f'{self.equipment.name} ID:{self.id}' + return f'{self.equipment.name}' @receiver(post_migrate) @@ -48,19 +49,25 @@ def create_superuser(sender, **kwargs): 'name': 'Pyrex Beaker', 'description': '', 'category': 'Glassware', - 'remarks': 'First beaker of equipment tracker!' + 'remarks': 'A beaker for storing fluids' }, { 'name': 'Bunsen Burner', 'description': '', 'category': 'Miscellaneous', - 'remarks': 'First bunsen burner of equipment tracker!' + 'remarks': 'A burner for heating things' }, { 'name': 'Microscope', 'description': '', 'category': 'Miscellaneous', - 'remarks': 'First microscope of equipment tracker!' + 'remarks': 'A microscope for zooming into tiny objects' + }, + { + 'name': 'Petri Dish', + 'description': '', + 'category': 'Glassware', + 'remarks': 'A petri dish' } ] @@ -72,6 +79,8 @@ def create_superuser(sender, **kwargs): ) if (CREATED): print('Created Equipment: ' + data['name']) + print( + 'Generating 3 Equipment Instances for Equipment: ' + data['name']) # Generate 3 equipment instances per SKU for x in range(3): EQUIPMENT_INSTANCE = EquipmentInstance.objects.create( diff --git a/equipment_tracker/equipments/urls.py b/equipment_tracker/equipments/urls.py index 9547051..c322c8e 100644 --- a/equipment_tracker/equipments/urls.py +++ b/equipment_tracker/equipments/urls.py @@ -18,6 +18,10 @@ urlpatterns = [ views.EquipmentLogViewSet.as_view({'get': 'list'})), # Last changed equipment path('equipments/latest', views.LastUpdatedEquipmentViewSet.as_view()), + # List of equipment instances that are available for borrowing (those not belonging to a non-finalized transaction) + path('equipment_instances/available', + views.AvailableEquipmentInstanceViewSet.as_view()), + # Logs for each equipment instance # Logs for all equipment instances path('equipment_instances/logs', views.EquipmentInstancesLogsViewSet.as_view()), # Logs for each equipment instance diff --git a/equipment_tracker/equipments/views.py b/equipment_tracker/equipments/views.py index 3888d5c..cd30e07 100644 --- a/equipment_tracker/equipments/views.py +++ b/equipment_tracker/equipments/views.py @@ -1,9 +1,11 @@ from rest_framework.permissions import IsAuthenticated from rest_framework import viewsets, generics from .models import Equipment, EquipmentInstance +from django.db.models import Q from . import serializers from config.settings import DEBUG from accounts.permissions import IsTechnician +from transactions.models import Transaction # -- Equipment Viewsets @@ -56,6 +58,31 @@ class EquipmentInstanceViewSet(viewsets.ModelViewSet): serializer_class = serializers.EquipmentInstanceSerializer queryset = EquipmentInstance.objects.all().order_by('id') + +class AvailableEquipmentInstanceViewSet(generics.ListAPIView): + if (not DEBUG): + permission_classes = [IsAuthenticated] + serializer_class = serializers.EquipmentInstanceSerializer + + def get_queryset(self): + """ + This view should return a list of all the equipment instances + that are not associated with a non-finalized transaction. + """ + # Get all non-finalized transactions + non_finalized_transactions = Transaction.objects.filter( + ~Q(transaction_status__in=['Finalized', 'Cancelled'])) + + # Get all equipment instances associated with non-finalized transactions + non_finalized_equipments = EquipmentInstance.objects.filter( + transaction__in=non_finalized_transactions) + + # Get all equipment instances which are not associated with non-finalized transactions + queryset = EquipmentInstance.objects.exclude( + id__in=non_finalized_equipments.values_list('id', flat=True)).order_by('id') + + return queryset + # For viewing all equipment instance logs diff --git a/equipment_tracker/schema.yml b/equipment_tracker/schema.yml index b5fd661..6ac770c 100644 --- a/equipment_tracker/schema.yml +++ b/equipment_tracker/schema.yml @@ -766,6 +766,23 @@ paths: responses: '204': description: No response body + /api/v1/equipments/equipment_instances/available: + get: + operationId: api_v1_equipments_equipment_instances_available_list + tags: + - api + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EquipmentInstance' + description: '' /api/v1/equipments/equipment_instances/latest: get: operationId: api_v1_equipments_equipment_instances_latest_list @@ -1187,6 +1204,9 @@ components: CustomUser: type: object properties: + id: + type: integer + readOnly: true username: type: string readOnly: true @@ -1213,6 +1233,7 @@ components: required: - avatar - first_name + - id - last_name - username Equipment: @@ -1454,6 +1475,9 @@ components: PatchedCustomUser: type: object properties: + id: + type: integer + readOnly: true username: type: string readOnly: true @@ -1545,6 +1569,10 @@ components: type: array items: type: integer + remarks: + type: string + nullable: true + maxLength: 512 transaction_status: $ref: '#/components/schemas/TransactionStatusEnum' timestamp: @@ -1587,11 +1615,13 @@ components: StatusEnum: enum: - Available + - Pending - Broken - Borrowed type: string description: |- * `Available` - Available + * `Pending` - Pending * `Broken` - Broken * `Borrowed` - Borrowed TokenObtainPair: @@ -1648,6 +1678,10 @@ components: type: array items: type: integer + remarks: + type: string + nullable: true + maxLength: 512 transaction_status: $ref: '#/components/schemas/TransactionStatusEnum' timestamp: diff --git a/equipment_tracker/transactions/serializers.py b/equipment_tracker/transactions/serializers.py index 21c4448..54c954e 100644 --- a/equipment_tracker/transactions/serializers.py +++ b/equipment_tracker/transactions/serializers.py @@ -106,57 +106,106 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): raise serializers.ValidationError( f"Cannot add Equipment ID:{equipment.id}. It is still part of a non-finalized transaction") - return super().create(validated_data) + # Create the transaction if there are no issues + transaction = super().create(validated_data) + + # Get the equipments from the newly created transaction + equipments = transaction.equipments.all() + + # Iterate through each of those equipment instances and change their status field to "Pending" + # This updates the status field of all equipment instances in a single query + EquipmentInstance.objects.filter( + id__in=[equipment.id for equipment in equipments]).update(status='Pending') + + return transaction def update(self, instance, validated_data): user = self.context['request'].user - # If user is not a teacher or a technician, forbid them from changing the status of a transaction - if not user.is_teacher and not user.is_technician and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.transaction_status: + # User Validation + + # If user is not a teacher or a technician (ie a student), forbid them from changing the status of a transaction + if not user.is_teacher and not user.is_technician and 'transaction_status' in validated_data and validated_data.get('transaction_status') != instance.transaction_status: raise serializers.ValidationError( "You are not a teacher or technician. You do not have permission to change the status of transactions" ) - # If user is not a teacher, forbid them from changing the status of a transaction - if not user.is_teacher and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.transaction_status: + # If the user is a teacher but is not assigned to the current transaction, forbid them from changing anything in the transaction + if user.is_teacher and instance.teacher != user: raise serializers.ValidationError( - "You do not have permission to change the status of a transaction" + "You are not the assigned teacher for this transaction" ) + + # If the user is a teacher and is updating a transaction with values other than Approved or Rejected, forbid them from doing so (Only technicians can update to other statuses) + if user.is_teacher and not (validated_data.get('transaction_status') == 'Rejected' or validated_data.get('transaction_status') == 'Approved'): + raise serializers.ValidationError( + "Teachers can only mark assigned transactions as Approved or Rejected. Please consult with a Technician should you wish to process this request" + ) + + # Equipment Instances Validation # Do not allow changes to equipments on created transactions - if 'equipments' in validated_data and instance.equipments != validated_data['equipments']: + if 'equipments' in validated_data: raise serializers.ValidationError( "You cannot change the equipments of an already created transaction" ) - # For already finalized/done transactions (Rejected or Finalized ones) - # Do not allow any changes to already finalized transactions - if instance.transaction_status in ['Rejected', 'Finalized']: - raise serializers.ValidationError( - "Unable to update rejected or finalized transaction. Please create a new one" - ) - + # Transaction Status Validation # Check if the update involves the transaction status if 'transaction_status' in validated_data: - # For Pending transactions + # For already finalized/done transactions (Rejected or Finalized ones) + # Do not allow any changes to already finalized transactions + if instance.transaction_status in ['Rejected', 'Finalized']: + raise serializers.ValidationError( + "Unable to update rejected or finalized transaction. Please create a new one" + ) + + # For Pending Approval transactions # If not changing to Approved or Rejected, throw an error - if instance.transaction_status == "Pending" and (validated_data['transaction_status'] != "Approved" or validated_data['transaction_status'] != "Rejected"): + if instance.transaction_status == "Pending Approval" and not (validated_data.get('transaction_status') == "Approved" or validated_data.get('transaction_status') == "Rejected"): raise serializers.ValidationError( "A pending transaction can only change to Approved or Rejected" ) + # If a transaction goes from Pending Approval to Rejected, reset the status of related equipment instances so that they can be included in new transactions + if instance.transaction_status == "Pending Approval" and validated_data.get('transaction_status') == "Rejected": + equipments = instance.equipments.all() + # Iterate through each of those equipment instances and change their status field to "Available" + # This updates the status field of all equipment instances in a single query + EquipmentInstance.objects.filter( + id__in=[equipment.id for equipment in equipments]).update(status='Available') + return super().update(instance, validated_data) + # For Approved transactions, # If not changing to Borrowed or Cancelled, throw an error # Already approved transactions can only be moved to Borrowed or Cancelled - if instance.transaction_status == "Approved" and (validated_data['transaction_status'] != "Borrowed" or validated_data != "Cancelled"): + if instance.transaction_status == "Approved" and not (validated_data.get('transaction_status') == "Borrowed" or validated_data.get('transaction_status') == "Cancelled"): raise serializers.ValidationError( - "An already approved transaction can only changed to Borrowed (On borrow) or Cancelled" + "An already approved transaction can only changed to Borrowed or Cancelled" ) + # If the transaction somehow gets Cancelled after being Approved, label the selected equipment's statuses as Available again + if instance.transaction_status == "Approved" and validated_data.get('transaction_status') == "Cancelled": + equipments = instance.equipments.all() + # Iterate through each of those equipment instances and change their status field to "Available" + # This updates the status field of all equipment instances in a single query + EquipmentInstance.objects.filter( + id__in=[equipment.id for equipment in equipments]).update(status='Available') + return super().update(instance, validated_data) + + # If there are no issues and a transaction changes from Approved to Borrowed, label the selected equipment's statuses as Borrowed + if instance.transaction_status == "Approved" and validated_data.get('transaction_status') == "Borrowed": + equipments = instance.equipments.all() + # Iterate through each of those equipment instances and change their status field to "Borrowed" + # This updates the status field of all equipment instances in a single query + EquipmentInstance.objects.filter( + id__in=[equipment.id for equipment in equipments]).update(status='Borrowed') + return super().update(instance, validated_data) + # For Borrowed transactions, - # If not changing to returned, throw an error - # Borrowed transactions that can only be changed to returned, pending checking for broken items - if instance.transaction_status == "Borrowed" and (validated_data['transaction_status'] != "Finalized" or validated_data != "With Breakages: Pending Resolution"): + # If not changing to Returned: Pending Checking, throw an error + # Borrowed transactions that are Returned must be checked first upon returning (thus needing Returned: Pending Checking) + if instance.transaction_status == "Borrowed" and not validated_data.get('transaction_status') == "Returned: Pending Checking": raise serializers.ValidationError( "A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution" ) @@ -164,12 +213,12 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): # For Return: Pending Checking transactions, # If not changing to With Breakages: Pending Resolution or Finalized, throw an error # Returned transactions can only be finalized without any issues or be marked as with breakages - if instance.transaction_status == "Returned: Pending Checking" and (validated_data['transaction_status'] != "Finalized" or validated_data != "With Breakages: Pending Resolution"): + if instance.transaction_status == "Returned: Pending Checking" and not (validated_data.get('transaction_status') == "Finalized" or validated_data.get('transaction_status') == "With Breakages: Pending Resolution"): raise serializers.ValidationError( "A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution" ) - # For transactions with pending breakage resolutions, + # For transactions with pending breakage resolutions # Do not allow updating of status. It should be updated within its respective breakage report # If it has been resolved there, this field will automatically update to Finalized if instance.transaction_status == "With Breakages: Pending Resolution": @@ -177,27 +226,24 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): "A transaction with pending breakage resolutions must be updated or resolved in its respective breakage report" ) - # If there are no issues and a transaction changes from Approved to Borrowed, label the selected equipment's statuses as Borrowed - if instance.transaction_status == "Approved" and validated_data['transaction_status'] == "Borrowed": - equipments = validated_data.get('equipments', []) - for equipment in equipments: - equipment.transaction_status = 'Borrowed' - equipment.save() - return super().update(validated_data) # If the transaction changes from Borrowed to Finalized and there are no breakages, label the selected equipment's statuses as Available again from Borrowed - if instance.transaction_status == "Borrowed" and validated_data['transaction_status'] == "Finalized": - equipments = validated_data.get('equipments', []) - for equipment in equipments: - equipment.transaction_status = 'Available' - equipment.save() - return super().update(validated_data) - # If the transaction changes from Borrowed to With Breakages, we create a Breakage Report instance - if instance.transaction_status == "Borrowed" and validated_data['transaction_status'] == "Finalized": - BreakageReport.objects.create( + if instance.transaction_status == "Borrowed" and validated_data.get('transaction_status') == "Finalized": + equipments = instance.equipments.all() + # Iterate through each of those equipment instances and change their status field to "Available" + # This updates the status field of all equipment instances in a single query + EquipmentInstance.objects.filter( + id__in=[equipment.id for equipment in equipments]).update(status='Available') + return super().update(instance, validated_data) + + # If the transaction changes from Returned: Pending Checking to With Breakages, we create a Breakage Report instance + if instance.transaction_status == "Returned: Pending Checking" and validated_data.get('transaction_status') == "With Breakages: Pending Resolution": + equipments = instance.equipments.all() + report = BreakageReport.objects.create( transaction=instance, - equipments=instance.equipments.all(), resolved=False ) + report.equipments.set(equipments) + return super().update(instance, validated_data) # Changing equipment status of broken items when there are breakages is handled in breakage reports return super().update(instance, validated_data)