Added transactions and updated models to reflect transactions

This commit is contained in:
Keannu Bernasol 2023-12-08 23:00:15 +08:00
parent d915852632
commit 2c8cc87cbe
39 changed files with 635 additions and 291 deletions

View file

@ -6,9 +6,9 @@ from .models import CustomUser
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
model = CustomUser model = CustomUser
list_display = UserAdmin.list_display + ('is_technician',) list_display = UserAdmin.list_display + ('is_technician', 'is_teacher')
fieldsets = UserAdmin.fieldsets + ( fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('is_technician',)}), (None, {'fields': ('is_technician', 'is_teacher')}),
) )

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-12-02 12:25 # Generated by Django 4.2.7 on 2023-12-08 14:41
import accounts.models import accounts.models
import django.contrib.auth.models import django.contrib.auth.models
@ -31,6 +31,7 @@ class Migration(migrations.Migration):
('last_name', models.CharField(max_length=100)), ('last_name', models.CharField(max_length=100)),
('is_active', models.BooleanField(default=False)), ('is_active', models.BooleanField(default=False)),
('is_technician', models.BooleanField(default=False)), ('is_technician', models.BooleanField(default=False)),
('is_teacher', models.BooleanField(default=False)),
('avatar', models.ImageField(null=True, upload_to=accounts.models.CustomUser._get_upload_to)), ('avatar', models.ImageField(null=True, upload_to=accounts.models.CustomUser._get_upload_to)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),

View file

@ -38,6 +38,7 @@ class CustomUser(AbstractUser):
# is_admin inherited from base user class # is_admin inherited from base user class
is_active = models.BooleanField(default=False) is_active = models.BooleanField(default=False)
is_technician = models.BooleanField(default=False) is_technician = models.BooleanField(default=False)
is_teacher = models.BooleanField(default=False)
avatar = models.ImageField(upload_to=_get_upload_to, null=True) avatar = models.ImageField(upload_to=_get_upload_to, null=True)
@property @property
@ -66,7 +67,7 @@ def create_superuser(sender, **kwargs):
print('Created admin account') print('Created admin account')
superuser.save() superuser.save()
username = 'test-user-technician' username = 'test-technician'
email = os.getenv('DJANGO_ADMIN_EMAIL') email = os.getenv('DJANGO_ADMIN_EMAIL')
password = os.getenv('DJANGO_ADMIN_PASSWORD') password = os.getenv('DJANGO_ADMIN_PASSWORD')
first_name = 'Test' first_name = 'Test'
@ -81,3 +82,35 @@ def create_superuser(sender, **kwargs):
user.is_active = True user.is_active = True
print('Created debug technician account') print('Created debug technician account')
user.save() user.save()
username = 'test-teacher'
email = os.getenv('DJANGO_ADMIN_EMAIL')
password = os.getenv('DJANGO_ADMIN_PASSWORD')
first_name = 'Test'
last_name = 'Teacher'
if not User.objects.filter(username=username).exists():
# Create the superuser with is_active set to False
user = User.objects.create_user(
username=username, email=email, password=password, first_name=first_name, last_name=last_name, is_teacher=True)
# Activate the user
user.is_active = True
print('Created debug teacher account')
user.save()
username = 'test-student'
email = os.getenv('DJANGO_ADMIN_EMAIL')
password = os.getenv('DJANGO_ADMIN_PASSWORD')
first_name = 'Test'
last_name = 'Student'
if not User.objects.filter(username=username).exists():
# Create the superuser with is_active set to False
user = User.objects.create_user(
username=username, email=email, password=password, first_name=first_name, last_name=last_name)
# Activate the user
user.is_active = True
print('Created debug student account')
user.save()

View file

@ -9,3 +9,23 @@ class IsTechnician(BasePermission):
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
return request.user.is_authenticated and request.user.is_technician return request.user.is_authenticated and request.user.is_technician
class IsTeacher(BasePermission):
message = "You must be a teacher to perform this action."
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_teacher
def has_object_permission(self, request, view, obj):
return request.user.is_authenticated and request.user.is_teacher
class IsStudent(BasePermission):
message = "You must be a student to perform this action."
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_student
def has_object_permission(self, request, view, obj):
return request.user.is_authenticated and request.user.is_student

View file

@ -18,7 +18,8 @@ class CustomUserSerializer(BaseUserSerializer):
class Meta(BaseUserSerializer.Meta): class Meta(BaseUserSerializer.Meta):
model = CustomUser model = CustomUser
fields = ('username', 'email', 'avatar', 'first_name', 'last_name',) fields = ('username', 'email', 'avatar', 'first_name',
'last_name', 'is_teacher', 'is_technician')
class UserRegistrationSerializer(serializers.ModelSerializer): class UserRegistrationSerializer(serializers.ModelSerializer):
@ -30,6 +31,7 @@ class UserRegistrationSerializer(serializers.ModelSerializer):
model = CustomUser # Use your custom user model here model = CustomUser # Use your custom user model here
fields = ('username', 'email', 'password', 'avatar', fields = ('username', 'email', 'password', 'avatar',
'first_name', 'last_name') 'first_name', 'last_name')
read_only_fields = ('is_teacher', 'is_technician')
def validate(self, attrs): def validate(self, attrs):
user = self.Meta.model(**attrs) user = self.Meta.model(**attrs)

View file

@ -3,5 +3,5 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('accounts/', include('accounts.urls')), path('accounts/', include('accounts.urls')),
path('equipments/', include('equipments.urls')), path('equipments/', include('equipments.urls')),
path('equipment_groups/', include('equipment_groups.urls')) path('transactions/', include('transactions.urls'))
] ]

View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class EquipmentGroupsConfig(AppConfig): class BreakagesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'equipment_groups' name = 'breakages'

View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -73,7 +73,7 @@ INSTALLED_APPS = [
'drf_spectacular_sidecar', 'drf_spectacular_sidecar',
'accounts', 'accounts',
'equipments', 'equipments',
'equipment_groups', 'transactions',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -137,8 +137,8 @@ REST_FRAMEWORK = {
# DRF-Spectacular # DRF-Spectacular
SPECTACULAR_SETTINGS = { SPECTACULAR_SETTINGS = {
'TITLE': 'CITC Equipment Tracker Backend', 'TITLE': 'Equipment Tracker Backend',
'DESCRIPTION': 'An IT Elective 4 Project', 'DESCRIPTION': 'A Project',
'VERSION': '1.0.0', 'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False, 'SERVE_INCLUDE_SCHEMA': False,
'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_DIST': 'SIDECAR',

View file

@ -1,9 +0,0 @@
from django.contrib import admin
from .models import EquipmentGroup
from simple_history.admin import SimpleHistoryAdmin
@admin.register(EquipmentGroup)
class EquipmentGroupAdmin(SimpleHistoryAdmin):
readonly_fields = ['status', 'date_added', 'last_updated']
list_display = ('name', 'status', 'date_added', 'last_updated')

View file

@ -1,53 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-02 12:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import simple_history.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('equipments', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='HistoricalEquipmentGroup',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('remarks', models.TextField(max_length=512)),
('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_updated', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical equipment group',
'verbose_name_plural': 'historical equipment groups',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='EquipmentGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('remarks', models.TextField(max_length=512)),
('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_updated', models.DateTimeField(auto_now=True)),
('equipments', models.ManyToManyField(to='equipments.equipmentinstance')),
],
),
]

View file

@ -1,16 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-05 12:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('equipment_groups', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='HistoricalEquipmentGroup',
),
]

View file

@ -1,39 +0,0 @@
from django.db import models
from django.utils.timezone import now
from simple_history.models import HistoricalRecords
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from equipments.models import EquipmentInstance
class EquipmentGroup(models.Model):
name = models.CharField(max_length=200)
remarks = models.TextField(max_length=512)
date_added = models.DateTimeField(default=now, editable=False)
last_updated = models.DateTimeField(auto_now=True, editable=False)
equipments = models.ManyToManyField(EquipmentInstance)
@property
def status(self):
if self.equipments.filter(status='Broken').exists():
return 'Broken'
elif self.equipments.filter(status='Under Maintenance').exists():
return 'Under Maintenance'
elif self.equipments.filter(status='Decomissioned').exists():
return 'Decomissioned'
else:
return 'Working'
def __str__(self):
return self.name
@receiver(post_migrate)
def create_superuser(sender, **kwargs):
if sender.name == 'equipment_groups':
PC = EquipmentInstance.objects.filter(id=1).first().id
KEYBOARD = EquipmentInstance.objects.filter(id=2).first().id
MOUSE = EquipmentInstance.objects.filter(id=3).first().id
GROUP, CREATED = EquipmentGroup.objects.get_or_create(
name="HP All-In-One PC Set", remarks="First PC set of citc tracker!")
GROUP.equipments.set([PC, KEYBOARD, MOUSE])

View file

@ -1,29 +0,0 @@
from rest_framework import serializers
from .models import EquipmentGroup, EquipmentInstance
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
# -- Equipment Group Serializers
class EquipmentGroupSerializer(serializers.HyperlinkedModelSerializer):
date_added = serializers.DateTimeField(
format="%m-%d-%Y %I:%M%p", read_only=True)
last_updated = serializers.DateTimeField(
format="%m-%d-%Y %I:%M%p", read_only=True)
equipments = serializers.SlugRelatedField(
many=True, slug_field='id', queryset=EquipmentInstance.objects.all())
name = serializers.CharField()
remarks = serializers.CharField()
class Meta:
model = EquipmentGroup
fields = ('__all__')
read_only_fields = ('id', 'last_updated', 'equipments',
'last_updated_by', 'date_added')
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['equipments'] = [
equipment.equipment.name for equipment in instance.equipments.all()]
return representation

View file

@ -1,16 +0,0 @@
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
# For viewing all equipments
router.register(r'equipment_groups', views.EquipmentGroupViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('', include(router.urls)),
# Last changed equipment group
path('equipment_groups/latest',
views.LastUpdatedEquipmentGroupViewSet.as_view()),
]

View file

@ -1,27 +0,0 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets, generics
from .models import EquipmentGroup
from . import serializers
from config.settings import DEBUG
from accounts.permissions import IsTechnician
# -- Equipment Viewsets
class EquipmentGroupViewSet(viewsets.ModelViewSet):
if (not DEBUG):
permission_classes = [IsAuthenticated, IsTechnician]
serializer_class = serializers.EquipmentGroupSerializer
queryset = EquipmentGroup.objects.all().order_by('id')
# Last changed equipment
class LastUpdatedEquipmentGroupViewSet(generics.ListAPIView):
if (not DEBUG):
permission_classes = [IsAuthenticated, IsTechnician]
serializer_class = serializers.EquipmentGroupSerializer
queryset = EquipmentGroup.objects.all().order_by('-date_added')
def get_queryset(self):
return super().get_queryset()[:1]

View file

@ -0,0 +1,11 @@
EQUIPMENT_CATEGORY_CHOICES = (
('Glassware', 'Glassware'),
('Miscellaneous', 'Miscellaneous')
)
EQUIPMENT_INSTANCE_STATUS_CHOICES = (
('Working', 'Working'),
('Broken', 'Broken'),
('Borrowed', 'Borrowed'),
)

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2023-12-02 12:25 # Generated by Django 4.2.7 on 2023-12-08 14:41
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -21,18 +21,18 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=40)), ('name', models.CharField(max_length=40)),
('description', models.TextField(max_length=512)), ('category', models.CharField(choices=[('Glassware', 'Glassware'), ('Miscellaneous', 'Miscellaneous')], default='Miscellaneous', max_length=20)),
('description', models.TextField(max_length=512, null=True)),
('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)), ('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
('category', models.CharField(choices=[('PC', 'PC'), ('NETWORKING', 'Networking'), ('CCTV', 'CCTV'), ('FURNITURE', 'Furniture'), ('PERIPHERALS', 'Peripherals'), ('MISC', 'Miscellaneous')], default='MISC', max_length=20)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='HistoricalEquipmentInstance', name='HistoricalEquipmentInstance',
fields=[ fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('status', models.CharField(choices=[('WORKING', 'Working'), ('BROKEN', 'Broken'), ('MAINTENANCE', 'Under Maintenance'), ('DECOMISSIONED', 'Decomissioned')], default='PENDING', max_length=20)), ('status', models.CharField(choices=[('Working', 'Working'), ('Broken', 'Broken'), ('Borrowed', 'Borrowed')], default='PENDING', max_length=20)),
('remarks', models.TextField(max_length=512)), ('remarks', models.TextField(max_length=512, null=True)),
('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)), ('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_updated', models.DateTimeField(blank=True, editable=False)), ('last_updated', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_id', models.AutoField(primary_key=True, serialize=False)),
@ -55,10 +55,10 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=40)), ('name', models.CharField(max_length=40)),
('description', models.TextField(max_length=512)), ('category', models.CharField(choices=[('Glassware', 'Glassware'), ('Miscellaneous', 'Miscellaneous')], default='Miscellaneous', max_length=20)),
('description', models.TextField(max_length=512, null=True)),
('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)), ('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_updated', models.DateTimeField(blank=True, editable=False)), ('last_updated', models.DateTimeField(blank=True, editable=False)),
('category', models.CharField(choices=[('PC', 'PC'), ('NETWORKING', 'Networking'), ('CCTV', 'CCTV'), ('FURNITURE', 'Furniture'), ('PERIPHERALS', 'Peripherals'), ('MISC', 'Miscellaneous')], default='MISC', max_length=20)),
('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)), ('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)), ('history_change_reason', models.CharField(max_length=100, null=True)),
@ -77,8 +77,8 @@ class Migration(migrations.Migration):
name='EquipmentInstance', name='EquipmentInstance',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('WORKING', 'Working'), ('BROKEN', 'Broken'), ('MAINTENANCE', 'Under Maintenance'), ('DECOMISSIONED', 'Decomissioned')], default='PENDING', max_length=20)), ('status', models.CharField(choices=[('Working', 'Working'), ('Broken', 'Broken'), ('Borrowed', 'Borrowed')], default='PENDING', max_length=20)),
('remarks', models.TextField(max_length=512)), ('remarks', models.TextField(max_length=512, null=True)),
('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)), ('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipments.equipment')), ('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipments.equipment')),

View file

@ -1,33 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-02 13:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('equipments', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='description',
field=models.TextField(max_length=512, null=True),
),
migrations.AlterField(
model_name='equipmentinstance',
name='remarks',
field=models.TextField(max_length=512, null=True),
),
migrations.AlterField(
model_name='historicalequipment',
name='description',
field=models.TextField(max_length=512, null=True),
),
migrations.AlterField(
model_name='historicalequipmentinstance',
name='remarks',
field=models.TextField(max_length=512, null=True),
),
]

View file

@ -3,26 +3,19 @@ from django.utils.timezone import now
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django.dispatch import receiver from django.dispatch import receiver
# Create your models here.
class Equipment(models.Model): class Equipment(models.Model):
EQUIPMENT_CATEGORY_CHOICES = (
CATEGORY_CHOICES = ( ('Glassware', 'Glassware'),
('PC', 'PC'), ('Miscellaneous', 'Miscellaneous')
('NETWORKING', 'Networking'),
('CCTV', 'CCTV'),
('FURNITURE', 'Furniture'),
('PERIPHERALS', 'Peripherals'),
('MISC', 'Miscellaneous')
) )
name = models.CharField(max_length=40) name = models.CharField(max_length=40)
category = models.CharField(
max_length=20, choices=EQUIPMENT_CATEGORY_CHOICES, default='Miscellaneous')
description = models.TextField(max_length=512, null=True) description = models.TextField(max_length=512, null=True)
date_added = models.DateTimeField(default=now, editable=False) date_added = models.DateTimeField(default=now, editable=False)
last_updated = models.DateTimeField(auto_now=True, editable=False) last_updated = models.DateTimeField(auto_now=True, editable=False)
category = models.CharField(
max_length=20, choices=CATEGORY_CHOICES, default='MISC')
history = HistoricalRecords() history = HistoricalRecords()
def __str__(self): def __str__(self):
@ -30,16 +23,14 @@ class Equipment(models.Model):
class EquipmentInstance(models.Model): class EquipmentInstance(models.Model):
STATUS_CHOICES = ( EQUIPMENT_INSTANCE_STATUS_CHOICES = (
('WORKING', 'Working'), ('Working', 'Working'),
('BROKEN', 'Broken'), ('Broken', 'Broken'),
('MAINTENANCE', 'Under Maintenance'), ('Borrowed', 'Borrowed'),
('DECOMISSIONED', 'Decomissioned'),
) )
equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE) equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE)
status = models.CharField( status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default='PENDING') max_length=20, choices=EQUIPMENT_INSTANCE_STATUS_CHOICES, default='PENDING')
remarks = models.TextField(max_length=512, null=True) remarks = models.TextField(max_length=512, null=True)
date_added = models.DateTimeField(default=now, editable=False) date_added = models.DateTimeField(default=now, editable=False)
last_updated = models.DateTimeField(auto_now=True, editable=False) last_updated = models.DateTimeField(auto_now=True, editable=False)
@ -53,14 +44,14 @@ class EquipmentInstance(models.Model):
def create_superuser(sender, **kwargs): def create_superuser(sender, **kwargs):
if sender.name == 'equipments': if sender.name == 'equipments':
EQUIPMENT, CREATED = Equipment.objects.get_or_create( EQUIPMENT, CREATED = Equipment.objects.get_or_create(
name="HP All-in-One PC", description="I5 6500 8GB RAM 1TB HDD", category="PC") name="Pyrex Beaker", description="", category="Glassware")
EQUIPMENT_INSTANCE, CREATED = EquipmentInstance.objects.get_or_create( EQUIPMENT_INSTANCE, CREATED = EquipmentInstance.objects.get_or_create(
equipment=EQUIPMENT, status="WORKING", remarks="First PC of citc equipment tracker!") equipment=EQUIPMENT, status="Working", remarks="First beaker of equipment tracker!")
EQUIPMENT, CREATED = Equipment.objects.get_or_create( EQUIPMENT, CREATED = Equipment.objects.get_or_create(
name="HP Keyboard", description="Generic Membrane Keyboard", category="PERIPHERALS") name="Bunsen Burner", description="", category="Miscellaneous")
EQUIPMENT_INSTANCE, CREATED = EquipmentInstance.objects.get_or_create( EQUIPMENT_INSTANCE, CREATED = EquipmentInstance.objects.get_or_create(
equipment=EQUIPMENT, status="WORKING", remarks="First keyboard of citc equipment tracker!") equipment=EQUIPMENT, status="Working", remarks="First bunsen burner of equipment tracker!")
EQUIPMENT, CREATED = Equipment.objects.get_or_create( EQUIPMENT, CREATED = Equipment.objects.get_or_create(
name="HP Mouse", description="Generic Mouse", category="PERIPHERALS") name="Microscope", description="", category="Miscellaneous")
EQUIPMENT_INSTANCE, CREATED = EquipmentInstance.objects.get_or_create( EQUIPMENT_INSTANCE, CREATED = EquipmentInstance.objects.get_or_create(
equipment=EQUIPMENT, status="WORKING", remarks="First mouse of citc equipment tracker!") equipment=EQUIPMENT, status="Working", remarks="First microscope of equipment tracker!")

View file

@ -3,7 +3,6 @@ from .models import Equipment, EquipmentInstance
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from django.db.models import F from django.db.models import F
from accounts.models import CustomUser
# -- Equipment Serializers # -- Equipment Serializers
@ -104,7 +103,8 @@ class EquipmentInstanceSerializer(serializers.HyperlinkedModelSerializer):
last_updated = serializers.DateTimeField( last_updated = serializers.DateTimeField(
format="%m-%d-%Y %I:%M%p", read_only=True) format="%m-%d-%Y %I:%M%p", read_only=True)
last_updated_by = serializers.SerializerMethodField() last_updated_by = serializers.SerializerMethodField()
status = serializers.ChoiceField(choices=EquipmentInstance.STATUS_CHOICES) status = serializers.ChoiceField(
choices=EquipmentInstance.EQUIPMENT_INSTANCE_STATUS_CHOICES)
# Forbid user from changing equipment field once the instance is already created # Forbid user from changing equipment field once the instance is already created
def update(self, instance, validated_data): def update(self, instance, validated_data):

View file

@ -1,8 +1,8 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: CITC Equipment Tracker Backend title: Equipment Tracker Backend
version: 1.0.0 version: 1.0.0
description: An IT Elective 4 Project description: A Project
paths: paths:
/api/v1/accounts/jwt/create/: /api/v1/accounts/jwt/create/:
post: post:
@ -520,6 +520,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -547,6 +548,7 @@ paths:
required: true required: true
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'201': '201':
content: content:
@ -567,6 +569,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -590,6 +593,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -622,6 +626,7 @@ paths:
required: true required: true
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -653,6 +658,7 @@ paths:
$ref: '#/components/schemas/PatchedEquipmentInstance' $ref: '#/components/schemas/PatchedEquipmentInstance'
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -673,6 +679,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'204': '204':
description: No response body description: No response body
@ -683,6 +690,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -699,6 +707,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -715,6 +724,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -742,6 +752,7 @@ paths:
required: true required: true
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'201': '201':
content: content:
@ -762,6 +773,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -785,6 +797,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -817,6 +830,7 @@ paths:
required: true required: true
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -848,6 +862,7 @@ paths:
$ref: '#/components/schemas/PatchedEquipment' $ref: '#/components/schemas/PatchedEquipment'
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -868,6 +883,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'204': '204':
description: No response body description: No response body
@ -878,6 +894,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -894,6 +911,7 @@ paths:
- api - api
security: security:
- jwtAuth: [] - jwtAuth: []
- {}
responses: responses:
'200': '200':
content: content:
@ -903,6 +921,147 @@ paths:
items: items:
$ref: '#/components/schemas/EquipmentLogs' $ref: '#/components/schemas/EquipmentLogs'
description: '' description: ''
/api/v1/transactions/:
get:
operationId: api_v1_transactions_list
tags:
- api
security:
- jwtAuth: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Transaction'
description: ''
post:
operationId: api_v1_transactions_create
tags:
- api
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Transaction'
multipart/form-data:
schema:
$ref: '#/components/schemas/Transaction'
required: true
security:
- jwtAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
description: ''
/api/v1/transactions/{id}/:
get:
operationId: api_v1_transactions_retrieve
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this transaction.
required: true
tags:
- api
security:
- jwtAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
description: ''
put:
operationId: api_v1_transactions_update
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this transaction.
required: true
tags:
- api
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Transaction'
multipart/form-data:
schema:
$ref: '#/components/schemas/Transaction'
required: true
security:
- jwtAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
description: ''
patch:
operationId: api_v1_transactions_partial_update
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this transaction.
required: true
tags:
- api
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedTransaction'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/PatchedTransaction'
multipart/form-data:
schema:
$ref: '#/components/schemas/PatchedTransaction'
security:
- jwtAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
description: ''
delete:
operationId: api_v1_transactions_destroy
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this transaction.
required: true
tags:
- api
security:
- jwtAuth: []
responses:
'204':
description: No response body
components: components:
schemas: schemas:
Activation: Activation:
@ -917,20 +1076,12 @@ components:
- uid - uid
CategoryEnum: CategoryEnum:
enum: enum:
- PC - Glassware
- NETWORKING - Miscellaneous
- CCTV
- FURNITURE
- PERIPHERALS
- MISC
type: string type: string
description: |- description: |-
* `PC` - PC * `Glassware` - Glassware
* `NETWORKING` - Networking * `Miscellaneous` - Miscellaneous
* `CCTV` - CCTV
* `FURNITURE` - Furniture
* `PERIPHERALS` - Peripherals
* `MISC` - Miscellaneous
CustomUser: CustomUser:
type: object type: object
properties: properties:
@ -953,6 +1104,10 @@ components:
last_name: last_name:
type: string type: string
maxLength: 100 maxLength: 100
is_teacher:
type: boolean
is_technician:
type: boolean
required: required:
- avatar - avatar
- first_name - first_name
@ -1215,6 +1370,10 @@ components:
last_name: last_name:
type: string type: string
maxLength: 100 maxLength: 100
is_teacher:
type: boolean
is_technician:
type: boolean
PatchedEquipment: PatchedEquipment:
type: object type: object
properties: properties:
@ -1267,6 +1426,29 @@ components:
type: string type: string
format: date-time format: date-time
readOnly: true readOnly: true
PatchedTransaction:
type: object
properties:
id:
type: integer
readOnly: true
borrower:
type: integer
nullable: true
teacher:
type: string
format: uri
nullable: true
equipments:
type: array
items:
type: integer
transaction_status:
$ref: '#/components/schemas/TransactionStatusEnum'
timestamp:
type: string
format: date-time
readOnly: true
SendEmailReset: SendEmailReset:
type: object type: object
properties: properties:
@ -1302,16 +1484,14 @@ components:
- new_username - new_username
StatusEnum: StatusEnum:
enum: enum:
- WORKING - Working
- BROKEN - Broken
- MAINTENANCE - Borrowed
- DECOMISSIONED
type: string type: string
description: |- description: |-
* `WORKING` - Working * `Working` - Working
* `BROKEN` - Broken * `Broken` - Broken
* `MAINTENANCE` - Under Maintenance * `Borrowed` - Borrowed
* `DECOMISSIONED` - Decomissioned
TokenObtainPair: TokenObtainPair:
type: object type: object
properties: properties:
@ -1352,6 +1532,53 @@ components:
writeOnly: true writeOnly: true
required: required:
- token - token
Transaction:
type: object
properties:
id:
type: integer
readOnly: true
borrower:
type: integer
nullable: true
teacher:
type: string
format: uri
nullable: true
equipments:
type: array
items:
type: integer
transaction_status:
$ref: '#/components/schemas/TransactionStatusEnum'
timestamp:
type: string
format: date-time
readOnly: true
required:
- id
- timestamp
- transaction_status
TransactionStatusEnum:
enum:
- Pending Approval
- Approved
- Rejected
- Cancelled
- Borrowed
- 'Returned: Pending Checking'
- 'With Breakages: Pending Resolution'
- Finalized
type: string
description: |-
* `Pending Approval` - Pending Approval
* `Approved` - Approved
* `Rejected` - Rejected
* `Cancelled` - Cancelled
* `Borrowed` - Borrowed
* `Returned: Pending Checking` - Returned: Pending Checking
* `With Breakages: Pending Resolution` - With Breakages: Pending Resolution
* `Finalized` - Finalized
UserRegistration: UserRegistration:
type: object type: object
properties: properties:

View file

@ -0,0 +1,16 @@
from django.contrib import admin
from .models import Transaction
from accounts.models import CustomUser
class TransactionAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "borrower":
kwargs["queryset"] = CustomUser.objects.exclude(
is_technician=True).exclude(is_teacher=True)
elif db_field.name == "teacher":
kwargs["queryset"] = CustomUser.objects.filter(is_teacher=True)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
admin.site.register(Transaction, TransactionAdmin)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TransactionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'transactions'

View file

@ -0,0 +1,30 @@
# Generated by Django 4.2.7 on 2023-12-08 14:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('equipments', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transaction_status', models.CharField(choices=[('Pending Approval', 'Pending Approval'), ('Approved', 'Approved'), ('Rejected', 'Rejected'), ('Cancelled', 'Cancelled'), ('Borrowed', 'Borrowed'), ('Returned: Pending Checking', 'Returned: Pending Checking'), ('With Breakages: Pending Resolution', 'With Breakages: Pending Resolution'), ('Finalized', 'Finalized')], default='Pending', max_length=40)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('borrower', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowed_transactions', to=settings.AUTH_USER_MODEL)),
('equipments', models.ManyToManyField(to='equipments.equipmentinstance')),
('teacher', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teacher_transactions', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,38 @@
from django.db import models
from accounts.models import CustomUser
from equipments.models import EquipmentInstance
from django.utils.timezone import now
class Transaction(models.Model):
TRANSACTION_STATUS_CHOICES = (
# Transaction is pending approval
('Pending Approval', 'Pending Approval'),
# Transaction has been approved, pending delivery by labtech
('Approved', 'Approved'),
# Tranasction has been rejected
('Rejected', 'Rejected'),
# Transaction has been approved but has been cancelled due to rare circumstances
('Cancelled', 'Cancelled'),
# Transaction has been delivered and is on borrow
('Borrowed', 'Borrowed'),
# Transaction has been returned, pending checking
('Returned: Pending Checking', 'Returned: Pending Checking'),
# Transaction has been breakages after being returned, pending resolution
('With Breakages: Pending Resolution',
'With Breakages: Pending Resolution'),
# Transaction has been finalized
('Finalized', 'Finalized'),
)
borrower = models.ForeignKey(
CustomUser, on_delete=models.SET_NULL, null=True, related_name='borrowed_transactions')
teacher = models.ForeignKey(
CustomUser, on_delete=models.SET_NULL, null=True, related_name='teacher_transactions')
equipments = models.ManyToManyField(EquipmentInstance)
transaction_status = models.CharField(
max_length=40, choices=TRANSACTION_STATUS_CHOICES, default='Pending')
timestamp = models.DateTimeField(default=now, editable=False)
def __str__(self):
return f"Transaction #{self.id} under {self.teacher} by {self.borrower}"

View file

@ -0,0 +1,156 @@
from rest_framework import serializers
from accounts.models import CustomUser
from equipments.models import EquipmentInstance
from .models import Transaction
from accounts.models import CustomUser
from config.settings import DEBUG
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
borrower = serializers.SlugRelatedField(
many=False, slug_field='id', queryset=CustomUser.objects.all(), required=False, allow_null=True)
equipments = serializers.SlugRelatedField(
many=True, slug_field='id', queryset=EquipmentInstance.objects.filter(status="Working"), required=False)
transaction_status = serializers.ChoiceField(
choices=Transaction.TRANSACTION_STATUS_CHOICES)
class Meta:
model = Transaction
fields = ['id', 'borrower', 'teacher',
'equipments', 'transaction_status', 'timestamp']
read_only_fields = ['id', 'timestamp']
def create(self, validated_data):
# Any transactions created will be associated with the one sending the POST/CREATE request
user = self.context['request'].user
validated_data.data['borrower'] = user
# All created transactions will be labelled as Pending
validated_data['transaction_status'] = 'Pending'
# If no teacher responsible for the borrow transaction is selected, raise an error
if 'teacher' not in validated_data:
raise serializers.ValidationError(
"You have not selected a teacher responsible for your borrow transaction")
# If no borrower responsible for the borrow transaction is selected, raise an error
if 'borrower' not in validated_data:
raise serializers.ValidationError(
"No borrower assigned for this transaction!")
# If the user in the teacher field != actually a teacher, raise an error
borrower = validated_data.get('borrower')
if borrower and borrower.is_teacher or borrower.is_technician:
raise serializers.ValidationError(
"The borrower must be a student. Not a teacher or techician")
# If the user in the teacher field != actually a teacher, raise an error
teacher = validated_data.get('teacher')
if teacher and not teacher.is_teacher:
raise serializers.ValidationError(
"The assigned teacher != a valid teacher")
# If the user in the teacher field != actually a teacher, raise an error
teacher = validated_data.get('teacher')
if teacher and not teacher.is_teacher:
raise serializers.ValidationError(
"The specified user != a teacher.")
# If there are no equipments specified, raise an error
if 'equipments' in validated_data and validated_data['equipments'] == []:
raise serializers.ValidationError(
"You cannot create a transaction without any equipments selected"
)
return super().create(validated_data)
def update(self, instance, validated_data):
user = self.context['request'].user
equipments = validated_data['equipments']
# Check if any of the equipment instances are already in a non-finalized transaction
for equipment in equipments:
existing__pending_transactions = Transaction.objects.filter(
equipments=equipment, status__in=['Pending', 'Approved', 'Borrowed', 'With Breakages: Pending Resolution'])
if existing__pending_transactions.exists():
raise serializers.ValidationError(
f"Equipment {equipment.id} is still part of a non-finalized transaction")
# If user != a teacher or a technician, forbid them from changing the status of a transaction
if not user.is_teacher and not user.technician and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.status:
raise serializers.ValidationError(
"You are not a teacher or technician. You do not have permission to change the status of transactions"
)
# If user != 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.status:
raise serializers.ValidationError(
"You do not have permission to change the status of a transaction"
)
# Do not allow changes to equipments on created transactions
if 'equipments' in validated_data and instance.equipments != validated_data['equipments']:
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.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
if 'transaction_status' in validated_data:
# For Pending transactions
# If not changing to Approved or Rejected, throw an error
if instance.status == 'Pending' and validated_data['transaction_status'] != 'Approved' or validated_data['transaction_status'] != 'Rejected':
raise serializers.ValidationError(
"A pending transaction can only change to Approved or Rejected"
)
# 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.status == 'Approved' and validated_data['transaction_status'] != 'Borrowed' or validated_data != 'Cancelled':
raise serializers.ValidationError(
"An already approved transaction can only changed to Borrowed (On borrow) or Cancelled"
)
# 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.status == 'Borrowed' and validated_data['transaction_status'] != 'Finalized' or validated_data != 'With Breakages: Pending Resolution':
raise serializers.ValidationError(
"A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution"
)
# 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.status == 'Returned: Pending Checking' and validated_data['transaction_status'] != 'Finalized' or validated_data != '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,
# 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.status == 'With Breakages: Pending Resolution':
raise serializers.ValidationError(
"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.status == 'Approved' and validated_data['transaction_status'] == 'Borrowed':
equipments = validated_data.get('equipments', [])
for equipment in equipments:
equipment.status = 'Borrowed'
equipment.save()
return super().update(validated_data)
# Changing equipment status of broken items when returned is handled in breakage reports
return super().update(instance, validated_data)

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,10 @@
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'', views.TransactionViewSet)
urlpatterns = [
path('', include(router.urls)),
]

View file

@ -0,0 +1,13 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets, generics
from .serializers import TransactionSerializer
from .models import Transaction
class TransactionViewSet(viewsets.ModelViewSet):
# Only allow GET, POST/CREATE
# Transactions cannot be deleted
http_method_names = ['get', 'post']
permission_classes = [IsAuthenticated]
serializer_class = TransactionSerializer
queryset = Transaction.objects.all()