Improved serializer for transaction and added available equipment viewset

This commit is contained in:
Keannu Bernasol 2023-12-27 18:36:52 +08:00
parent 94ba018c9e
commit d0ca68149a
6 changed files with 167 additions and 47 deletions

View file

@ -6,11 +6,11 @@ from .models import Equipment, EquipmentInstance
@admin.register(Equipment) @admin.register(Equipment)
class EquipmentAdmin(SimpleHistoryAdmin): class EquipmentAdmin(SimpleHistoryAdmin):
readonly_fields = ('date_added', 'last_updated') readonly_fields = ('date_added', 'last_updated')
list_display = ('name', 'date_added', 'last_updated') list_display = ('id', 'name', 'date_added', 'last_updated')
@admin.register(EquipmentInstance) @admin.register(EquipmentInstance)
class EquipmentInstanceAdmin(SimpleHistoryAdmin): class EquipmentInstanceAdmin(SimpleHistoryAdmin):
readonly_fields = ('date_added', 'last_updated') readonly_fields = ('date_added', 'last_updated')
list_display = ('equipment', 'status', 'remarks', list_display = ('id', 'equipment', 'status', 'remarks',
'date_added', 'last_updated') 'date_added', 'last_updated')

View file

@ -19,12 +19,13 @@ class Equipment(models.Model):
history = HistoricalRecords() history = HistoricalRecords()
def __str__(self): def __str__(self):
return f'{self.name} ID:{self.id}' return f'{self.name}'
class EquipmentInstance(models.Model): class EquipmentInstance(models.Model):
EQUIPMENT_INSTANCE_STATUS_CHOICES = ( EQUIPMENT_INSTANCE_STATUS_CHOICES = (
('Available', 'Available'), ('Available', 'Available'),
('Pending', 'Pending'),
('Broken', 'Broken'), ('Broken', 'Broken'),
('Borrowed', 'Borrowed'), ('Borrowed', 'Borrowed'),
) )
@ -37,7 +38,7 @@ class EquipmentInstance(models.Model):
history = HistoricalRecords() history = HistoricalRecords()
def __str__(self): def __str__(self):
return f'{self.equipment.name} ID:{self.id}' return f'{self.equipment.name}'
@receiver(post_migrate) @receiver(post_migrate)
@ -48,19 +49,25 @@ def create_superuser(sender, **kwargs):
'name': 'Pyrex Beaker', 'name': 'Pyrex Beaker',
'description': '', 'description': '',
'category': 'Glassware', 'category': 'Glassware',
'remarks': 'First beaker of equipment tracker!' 'remarks': 'A beaker for storing fluids'
}, },
{ {
'name': 'Bunsen Burner', 'name': 'Bunsen Burner',
'description': '', 'description': '',
'category': 'Miscellaneous', 'category': 'Miscellaneous',
'remarks': 'First bunsen burner of equipment tracker!' 'remarks': 'A burner for heating things'
}, },
{ {
'name': 'Microscope', 'name': 'Microscope',
'description': '', 'description': '',
'category': 'Miscellaneous', '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): if (CREATED):
print('Created Equipment: ' + data['name']) print('Created Equipment: ' + data['name'])
print(
'Generating 3 Equipment Instances for Equipment: ' + data['name'])
# Generate 3 equipment instances per SKU # Generate 3 equipment instances per SKU
for x in range(3): for x in range(3):
EQUIPMENT_INSTANCE = EquipmentInstance.objects.create( EQUIPMENT_INSTANCE = EquipmentInstance.objects.create(

View file

@ -18,6 +18,10 @@ urlpatterns = [
views.EquipmentLogViewSet.as_view({'get': 'list'})), views.EquipmentLogViewSet.as_view({'get': 'list'})),
# Last changed equipment # Last changed equipment
path('equipments/latest', views.LastUpdatedEquipmentViewSet.as_view()), 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 # Logs for all equipment instances
path('equipment_instances/logs', views.EquipmentInstancesLogsViewSet.as_view()), path('equipment_instances/logs', views.EquipmentInstancesLogsViewSet.as_view()),
# Logs for each equipment instance # Logs for each equipment instance

View file

@ -1,9 +1,11 @@
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets, generics from rest_framework import viewsets, generics
from .models import Equipment, EquipmentInstance from .models import Equipment, EquipmentInstance
from django.db.models import Q
from . import serializers from . import serializers
from config.settings import DEBUG from config.settings import DEBUG
from accounts.permissions import IsTechnician from accounts.permissions import IsTechnician
from transactions.models import Transaction
# -- Equipment Viewsets # -- Equipment Viewsets
@ -56,6 +58,31 @@ class EquipmentInstanceViewSet(viewsets.ModelViewSet):
serializer_class = serializers.EquipmentInstanceSerializer serializer_class = serializers.EquipmentInstanceSerializer
queryset = EquipmentInstance.objects.all().order_by('id') 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 # For viewing all equipment instance logs

View file

@ -766,6 +766,23 @@ paths:
responses: responses:
'204': '204':
description: No response body 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: /api/v1/equipments/equipment_instances/latest:
get: get:
operationId: api_v1_equipments_equipment_instances_latest_list operationId: api_v1_equipments_equipment_instances_latest_list
@ -1187,6 +1204,9 @@ components:
CustomUser: CustomUser:
type: object type: object
properties: properties:
id:
type: integer
readOnly: true
username: username:
type: string type: string
readOnly: true readOnly: true
@ -1213,6 +1233,7 @@ components:
required: required:
- avatar - avatar
- first_name - first_name
- id
- last_name - last_name
- username - username
Equipment: Equipment:
@ -1454,6 +1475,9 @@ components:
PatchedCustomUser: PatchedCustomUser:
type: object type: object
properties: properties:
id:
type: integer
readOnly: true
username: username:
type: string type: string
readOnly: true readOnly: true
@ -1545,6 +1569,10 @@ components:
type: array type: array
items: items:
type: integer type: integer
remarks:
type: string
nullable: true
maxLength: 512
transaction_status: transaction_status:
$ref: '#/components/schemas/TransactionStatusEnum' $ref: '#/components/schemas/TransactionStatusEnum'
timestamp: timestamp:
@ -1587,11 +1615,13 @@ components:
StatusEnum: StatusEnum:
enum: enum:
- Available - Available
- Pending
- Broken - Broken
- Borrowed - Borrowed
type: string type: string
description: |- description: |-
* `Available` - Available * `Available` - Available
* `Pending` - Pending
* `Broken` - Broken * `Broken` - Broken
* `Borrowed` - Borrowed * `Borrowed` - Borrowed
TokenObtainPair: TokenObtainPair:
@ -1648,6 +1678,10 @@ components:
type: array type: array
items: items:
type: integer type: integer
remarks:
type: string
nullable: true
maxLength: 512
transaction_status: transaction_status:
$ref: '#/components/schemas/TransactionStatusEnum' $ref: '#/components/schemas/TransactionStatusEnum'
timestamp: timestamp:

View file

@ -106,57 +106,106 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer):
raise serializers.ValidationError( raise serializers.ValidationError(
f"Cannot add Equipment ID:{equipment.id}. It is still part of a non-finalized transaction") 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): def update(self, instance, validated_data):
user = self.context['request'].user user = self.context['request'].user
# If user is not a teacher or a technician, forbid them from changing the status of a transaction # User Validation
if not user.is_teacher and not user.is_technician and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.transaction_status:
# 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( raise serializers.ValidationError(
"You are not a teacher or technician. You do not have permission to change the status of transactions" "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 the user is a teacher but is not assigned to the current transaction, forbid them from changing anything in the transaction
if not user.is_teacher and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.transaction_status: if user.is_teacher and instance.teacher != user:
raise serializers.ValidationError( 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 # 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( raise serializers.ValidationError(
"You cannot change the equipments of an already created transaction" "You cannot change the equipments of an already created transaction"
) )
# For already finalized/done transactions (Rejected or Finalized ones) # Transaction Status Validation
# 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"
)
# Check if the update involves the transaction status # Check if the update involves the transaction status
if 'transaction_status' in validated_data: 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 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( raise serializers.ValidationError(
"A pending transaction can only change to Approved or Rejected" "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, # For Approved transactions,
# If not changing to Borrowed or Cancelled, throw an error # If not changing to Borrowed or Cancelled, throw an error
# Already approved transactions can only be moved to Borrowed or Cancelled # 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( 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, # For Borrowed transactions,
# If not changing to returned, throw an error # If not changing to Returned: Pending Checking, throw an error
# Borrowed transactions that can only be changed to returned, pending checking for broken items # Borrowed transactions that are Returned must be checked first upon returning (thus needing Returned: Pending Checking)
if instance.transaction_status == "Borrowed" and (validated_data['transaction_status'] != "Finalized" or validated_data != "With Breakages: Pending Resolution"): if instance.transaction_status == "Borrowed" and not validated_data.get('transaction_status') == "Returned: Pending Checking":
raise serializers.ValidationError( raise serializers.ValidationError(
"A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution" "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, # For Return: Pending Checking transactions,
# If not changing to With Breakages: Pending Resolution or Finalized, throw an error # 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 # 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( raise serializers.ValidationError(
"A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution" "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 # 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 it has been resolved there, this field will automatically update to Finalized
if instance.transaction_status == "With Breakages: Pending Resolution": 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" "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 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": if instance.transaction_status == "Borrowed" and validated_data.get('transaction_status') == "Finalized":
equipments = validated_data.get('equipments', []) equipments = instance.equipments.all()
for equipment in equipments: # Iterate through each of those equipment instances and change their status field to "Available"
equipment.transaction_status = 'Available' # This updates the status field of all equipment instances in a single query
equipment.save() EquipmentInstance.objects.filter(
return super().update(validated_data) id__in=[equipment.id for equipment in equipments]).update(status='Available')
# If the transaction changes from Borrowed to With Breakages, we create a Breakage Report instance return super().update(instance, validated_data)
if instance.transaction_status == "Borrowed" and validated_data['transaction_status'] == "Finalized":
BreakageReport.objects.create( # 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, transaction=instance,
equipments=instance.equipments.all(),
resolved=False 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 # Changing equipment status of broken items when there are breakages is handled in breakage reports
return super().update(instance, validated_data) return super().update(instance, validated_data)