mirror of
https://github.com/lemeow125/Borrowing-TrackerBackend.git
synced 2025-04-27 02:01:24 +08:00
Migrate to postgres, add memcache and vastly improve available equipment instance query time from 1min 30 seconds to 12 seconds
This commit is contained in:
parent
0af8efa793
commit
b0b1f4db86
24 changed files with 1253 additions and 292 deletions
Binary file not shown.
|
@ -28,9 +28,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||
SECRET_KEY = str(os.getenv('SECRET_KEY'))
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
"https://csm-frontend.keannu1.duckdns.org", "https://csm-backend.keannu1.duckdns.org"]
|
||||
|
||||
|
@ -156,10 +156,21 @@ WSGI_APPLICATION = 'config.wsgi.application'
|
|||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
# DATABASES = {
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||
# 'NAME': BASE_DIR / 'db.sqlite3',
|
||||
# }
|
||||
# }
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('DB_NAME'),
|
||||
'USER': os.getenv('DB_USER'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD'),
|
||||
'HOST': os.getenv('DB_HOST', 'postgres'),
|
||||
'PORT': os.getenv('DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,3 +240,10 @@ SESSION_CACHE_ALIAS = "default"
|
|||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': "django.core.cache.backends.memcached.PyMemcacheCache",
|
||||
'LOCATION': 'memcached:11211',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.0.1 on 2024-01-06 16:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipmentinstance',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('Available', 'Available'), ('Pending', 'Pending'), ('Broken', 'Broken'), ('Borrowed', 'Borrowed')], db_index=True, default='Available', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalequipmentinstance',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('Available', 'Available'), ('Pending', 'Pending'), ('Broken', 'Broken'), ('Borrowed', 'Borrowed')], db_index=True, default='Available', max_length=20),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.0.1 on 2024-01-06 18:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipments', '0002_alter_equipmentinstance_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipment',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalequipment',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
]
|
Binary file not shown.
|
@ -4,6 +4,7 @@ from simple_history.models import HistoricalRecords
|
|||
from django.db.models.signals import post_migrate
|
||||
from django.dispatch import receiver
|
||||
from config import settings
|
||||
from django.core.cache import cache
|
||||
import csv
|
||||
import os
|
||||
|
||||
|
@ -13,7 +14,7 @@ class Equipment(models.Model):
|
|||
('Glassware', 'Glassware'),
|
||||
('Miscellaneous', 'Miscellaneous')
|
||||
)
|
||||
name = models.CharField(max_length=40)
|
||||
name = models.CharField(max_length=100)
|
||||
category = models.CharField(
|
||||
max_length=20, choices=EQUIPMENT_CATEGORY_CHOICES, default='Miscellaneous')
|
||||
description = models.TextField(max_length=512, null=True)
|
||||
|
@ -24,6 +25,10 @@ class Equipment(models.Model):
|
|||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
cache.delete('equipments')
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class EquipmentInstance(models.Model):
|
||||
EQUIPMENT_INSTANCE_STATUS_CHOICES = (
|
||||
|
@ -34,7 +39,7 @@ class EquipmentInstance(models.Model):
|
|||
)
|
||||
equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=EQUIPMENT_INSTANCE_STATUS_CHOICES, default='Available')
|
||||
max_length=20, choices=EQUIPMENT_INSTANCE_STATUS_CHOICES, default='Available', db_index=True)
|
||||
remarks = models.TextField(max_length=512, null=True)
|
||||
date_added = models.DateTimeField(default=now, editable=False)
|
||||
last_updated = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
@ -43,67 +48,77 @@ class EquipmentInstance(models.Model):
|
|||
def __str__(self):
|
||||
return f'{self.equipment.name}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
cache.delete('equipments')
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
seed_database = False
|
||||
|
||||
|
||||
@receiver(post_migrate)
|
||||
def create_superuser(sender, **kwargs):
|
||||
if sender.name == 'equipments':
|
||||
root_path = os.path.join(settings.MEDIA_ROOT, 'equipment_records')
|
||||
csv_files = [f for f in os.listdir(root_path) if f.endswith('.csv')]
|
||||
print('Warning: Postmigration script will migrate without checking for existing item instances')
|
||||
for csv_file in csv_files:
|
||||
csv_file_path = os.path.join(root_path, csv_file)
|
||||
filename = os.path.splitext(csv_file)[0]
|
||||
print('---', 'Adding Equipments from', filename, '---')
|
||||
with open(csv_file_path, newline='') as csvfile:
|
||||
if seed_database:
|
||||
root_path = os.path.join(settings.MEDIA_ROOT, 'equipment_records')
|
||||
csv_files = [f for f in os.listdir(
|
||||
root_path) if f.endswith('.csv')]
|
||||
print(
|
||||
'Warning: Postmigration script will migrate without checking for existing item instances')
|
||||
for csv_file in csv_files:
|
||||
csv_file_path = os.path.join(root_path, csv_file)
|
||||
filename = os.path.splitext(csv_file)[0]
|
||||
print('---', 'Adding Equipments from', filename, '---')
|
||||
with open(csv_file_path, newline='') as csvfile:
|
||||
|
||||
reader = csv.reader(csvfile)
|
||||
next(reader) # Skip the header row
|
||||
for row in reader:
|
||||
if not any(row):
|
||||
continue
|
||||
reader = csv.reader(csvfile)
|
||||
next(reader) # Skip the header row
|
||||
for row in reader:
|
||||
if not any(row):
|
||||
continue
|
||||
|
||||
# Get equipment information
|
||||
category = filename.split('-')[0]
|
||||
name = row[2] + ' ' + row[1]
|
||||
# If quantity is missing, set value as 0
|
||||
if (row[3] == ''):
|
||||
available = 0
|
||||
else:
|
||||
available = int(row[3])
|
||||
# If quantity of broken instances is missing, set value as 0
|
||||
if (row[4] == ''):
|
||||
broken = 0
|
||||
else:
|
||||
available = available - int(row[4])
|
||||
broken = int(row[4])
|
||||
# Get equipment information
|
||||
category = filename.split('-')[0]
|
||||
name = row[2] + ' ' + row[1]
|
||||
# If quantity is missing, set value as 0
|
||||
if (row[3] == ''):
|
||||
available = 0
|
||||
else:
|
||||
available = int(row[3])
|
||||
# If quantity of broken instances is missing, set value as 0
|
||||
if (row[4] == ''):
|
||||
broken = 0
|
||||
else:
|
||||
available = available - int(row[4])
|
||||
broken = int(row[4])
|
||||
|
||||
def create_instances(a, b, c):
|
||||
print('Adding', a, 'number of working', c.name)
|
||||
# Add working equipments
|
||||
if (a >= 1):
|
||||
for i in range(a):
|
||||
EquipmentInstance.objects.create(
|
||||
equipment=c, status='Available')
|
||||
def create_instances(a, b, c):
|
||||
print('Adding', a, 'number of working', c.name)
|
||||
# Add working equipments
|
||||
if (a >= 1):
|
||||
for i in range(a):
|
||||
EquipmentInstance.objects.create(
|
||||
equipment=c, status='Available')
|
||||
|
||||
if (b >= 1):
|
||||
print('Adding', a, 'number of broken', c.name)
|
||||
# Add broken equipments
|
||||
for i in range(b):
|
||||
EquipmentInstance.objects.create(
|
||||
equipment=c, status='Broken')
|
||||
if (b >= 1):
|
||||
print('Adding', b, 'number of broken', c.name)
|
||||
# Add broken equipments
|
||||
for i in range(b):
|
||||
EquipmentInstance.objects.create(
|
||||
equipment=c, status='Broken')
|
||||
|
||||
EQUIPMENT = Equipment.objects.filter(
|
||||
name=name, category=category).first()
|
||||
EQUIPMENT = Equipment.objects.filter(
|
||||
name=name, category=category).first()
|
||||
|
||||
# Check if equipment exists
|
||||
if (EQUIPMENT):
|
||||
# If so, add equipment instances
|
||||
create_instances(available, broken, EQUIPMENT)
|
||||
# Check if equipment exists
|
||||
if (EQUIPMENT):
|
||||
# If so, add equipment instances
|
||||
create_instances(available, broken, EQUIPMENT)
|
||||
|
||||
# If not, create equipment
|
||||
else:
|
||||
# Create the equipment first
|
||||
EQUIPMENT = Equipment.objects.create(
|
||||
name=name, category=category)
|
||||
# Then create the instances
|
||||
create_instances(available, broken, EQUIPMENT)
|
||||
# If not, create equipment
|
||||
else:
|
||||
# Create the equipment first
|
||||
EQUIPMENT = Equipment.objects.create(
|
||||
name=name, category=category)
|
||||
# Then create the instances
|
||||
create_instances(available, broken, EQUIPMENT)
|
||||
|
|
|
@ -155,7 +155,6 @@ class EquipmentInstanceSerializer(serializers.HyperlinkedModelSerializer):
|
|||
format="%m-%d-%Y %I:%M %p", read_only=True)
|
||||
last_updated = serializers.DateTimeField(
|
||||
format="%m-%d-%Y %I:%M %p", read_only=True)
|
||||
last_updated_by = serializers.SerializerMethodField()
|
||||
status = serializers.ChoiceField(
|
||||
choices=EquipmentInstance.EQUIPMENT_INSTANCE_STATUS_CHOICES)
|
||||
|
||||
|
@ -221,19 +220,11 @@ class EquipmentInstanceSerializer(serializers.HyperlinkedModelSerializer):
|
|||
class Meta:
|
||||
model = EquipmentInstance
|
||||
fields = ('id', 'equipment', 'equipment_name', 'category', 'status', 'remarks',
|
||||
'last_updated', 'last_updated_by', 'date_added')
|
||||
read_only_fields = ('id', 'last_updated', 'equipment_name', 'category',
|
||||
'last_updated_by', 'date_added', 'equipment_name')
|
||||
'last_updated', 'date_added')
|
||||
read_only_fields = ('id', 'last_upated', 'equipment_name', 'category',
|
||||
'date_added', 'equipment_name')
|
||||
extra_kwargs = {"remarks": {"required": False, "allow_null": True}}
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_history_user(self, obj):
|
||||
return obj.history_user.username if obj.history_user else None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_last_updated_by(self, obj):
|
||||
return obj.history.first().history_user.username if obj.history.first().history_user else None
|
||||
|
||||
|
||||
class EquipmentInstanceLogsSerializer(serializers.HyperlinkedModelSerializer):
|
||||
history_date = serializers.DateTimeField(
|
||||
|
|
|
@ -6,7 +6,7 @@ from . import serializers
|
|||
from config.settings import DEBUG
|
||||
from accounts.permissions import IsTechnician
|
||||
from transactions.models import Transaction
|
||||
|
||||
from django.core.cache import cache
|
||||
# -- Equipment Viewsets
|
||||
|
||||
|
||||
|
@ -16,6 +16,15 @@ class EquipmentViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = serializers.EquipmentSerializer
|
||||
queryset = Equipment.objects.all().order_by('id')
|
||||
|
||||
def get_queryset(self):
|
||||
key = 'equipments'
|
||||
|
||||
queryset = cache.get(key)
|
||||
if not queryset:
|
||||
queryset = super().get_queryset()
|
||||
cache.set(key, queryset, timeout=60*60*24)
|
||||
return queryset
|
||||
|
||||
# For viewing all logs for all equipments
|
||||
|
||||
|
||||
|
@ -69,19 +78,28 @@ class AvailableEquipmentInstanceViewSet(generics.ListAPIView):
|
|||
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', 'Rejected', 'Cancelled']))
|
||||
key = 'available_equipment_instances'
|
||||
|
||||
# Get all equipment instances associated with non-finalized transactions
|
||||
non_finalized_equipments = EquipmentInstance.objects.filter(
|
||||
transaction__in=non_finalized_transactions)
|
||||
queryset = cache.get(key)
|
||||
if not queryset:
|
||||
# Get all non-finalized transactions
|
||||
non_finalized_transactions = Transaction.objects.filter(
|
||||
~Q(transaction_status__in=[
|
||||
'Finalized', 'Rejected', 'Cancelled'])
|
||||
).prefetch_related('equipments')
|
||||
|
||||
# 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')
|
||||
# Get all equipment instances associated with non-finalized transactions
|
||||
non_finalized_equipments = EquipmentInstance.objects.filter(
|
||||
transaction__in=non_finalized_transactions
|
||||
).prefetch_related('equipment')
|
||||
|
||||
return queryset
|
||||
# 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)
|
||||
)
|
||||
cache.set(key, list(queryset), timeout=60*60*24)
|
||||
|
||||
return queryset.prefetch_related('equipment')
|
||||
|
||||
# For viewing all equipment instance logs
|
||||
|
||||
|
|
125
equipment_tracker/media/Glassware-2nd Floor Chemlab.csv
Normal file
125
equipment_tracker/media/Glassware-2nd Floor Chemlab.csv
Normal file
|
@ -0,0 +1,125 @@
|
|||
,NAME,Brand/Supplier,Qty.,Breakages,Remarks
|
||||
1,"Beaker, 1000mL",Pyrex,1,,
|
||||
3,"Beaker, 100mL",Mars Lab,108,12,
|
||||
5,"Beaker, 150mL",Pyrex,50,11,
|
||||
8,"Beaker, 250mL",Pyrex,50,13,
|
||||
11,"Beaker, 400mL",Pyrex,20,1,
|
||||
14,"Beaker, 50mL",Mars Lab,44,13,
|
||||
15,"Beaker, 600mL",Pyrex,15,1,
|
||||
17,"Bell Jar, Micro, Bottom Plate, 1000ml",,1,,PKI donated
|
||||
18,"Bell Jar, Micro, Bottom Plate, 200ml",,1,,PKI donated
|
||||
19,"Bell Jar, Micro, Bottom Plate, 500ml",,1,,PKI donated
|
||||
31,"Bottle, BOD, 300mL",,3,,
|
||||
34,"Buret, Automatic Auto Refill, 50mL",Kimax,2,,
|
||||
35,"Buret, Automatic Straight",,3,,
|
||||
36,"Buret, Glass Stopcock, 50mL",Pyrex,2,,
|
||||
39,"Buret, Teflon Stopcock, 10mL",Pyrex,1,,
|
||||
40,"Buret, Teflon Stopcock, 25mL",Pyrex,2,,
|
||||
41,"Buret, Teflon Stopcock, 50mL",Pyrex,4,,
|
||||
42,"Buret, Teflon Stopcock, 50mL",Mars Lab,,,
|
||||
46,"Column, Distilling, Drip Tip",,1,,PKI donated
|
||||
47,"Column, Vigreux, Pattern-Multi",,1,,PKI donated
|
||||
48,"Concentrator, 250mL",,1,,PKI donated
|
||||
49,"Concentrator, 500mL",,1,,PKI donated
|
||||
50,"Condenser, Allihn, 24/38",,1,,PKI donated
|
||||
51,"Condenser, Allihn, 24/40, 300mm",,1,,PKI donated
|
||||
52,"Condenser, Allihn, Drip – Tip",,1,,PKI donated
|
||||
53,"Condenser, Allihn, Rotary Evaporator",,1,,PKI donated
|
||||
54,"Condenser, Coiled-Distillation",,1,,PKI donated
|
||||
55,"Condenser, Graham, Drip Tip, 19/38",,1,,PKI donated
|
||||
56,"Condenser, Graham, TS-Joint, 55/50",,1,,PKI donated
|
||||
57,"Condenser, Leibig, Drip – tip",,1,,PKI donated
|
||||
58,"Condenser, Leibig, Drip tip, 24/40, 41x 300mm",,1,,PKI donated
|
||||
59,"Condenser, West Drip Tip, 55/50",,1,,PKI donated
|
||||
60,"Condenser, with Still Head",,1,,PKI donated
|
||||
61,Connecting Bulb for Kjeldal,,1,,PKI donated
|
||||
62,"Connecting Tube, Clasein, 24/29",,1,,PKI donated
|
||||
63,"Cuvette, Glass, 3.5mL capacity",Mars Lab,8,,
|
||||
64,"Cylinder, Graduated, 1000mL",Pyrex,1,,
|
||||
66,"Cylinder, Graduated, 100mL",Mars Lab,52,4,
|
||||
67,"Cylinder, Graduated, 10mL",Pyrex,7,,
|
||||
69,"Cylinder, Graduated, 25mL",Pyrex,10,,
|
||||
70,"Cylinder, Graduated, 50mL",Pyrex,5,,
|
||||
72,"Cylinder, Graduated, 250mL",Mars Lab,10,,
|
||||
72,"Cylinder, Graduated,Plastic 100mL",,1,,
|
||||
73,"Dessicator, Glass, 10.4in.(O.D) x 8.11in.",,1,,
|
||||
74,"Dessicator, Glass, 10.5in.(O.D) x 8.30in.",,1,,
|
||||
75,"Dessicator, Glass, 10.5in.(O.D) x 9.23in.",,1,,
|
||||
76,"Dessicator, Glass, 12.6in.(O.D) x 12.7in.",,1,,
|
||||
77,"Dessicator, Glass, 6.63in.(O.D) x 6.00in.",,1,,
|
||||
78,"Dessicator, Glass, 6.74in.(O.D) x 6.16in.",,1,,
|
||||
79,"Dessicator, Glass, 7,500mL",Mars Lab,4,,
|
||||
80,"Dessicator, Glass, 8.63in.(O.D) x 7.91in.",,1,,
|
||||
81,"Dessicator, Glass, 8.67in.(O.D) x 7.61in.",,1,,
|
||||
82,"Dessicator, Glass, 8.92in.(O.D) x 6.89in.",,1,,
|
||||
83,"Dessicator, Glass, 8.93in.(O.D) x 9.10in.",,1,,
|
||||
84,"Distillation Set up, Distilling Flask(1000mL) and Condenser",Mars Lab,3,,
|
||||
85,"Distilling Flask, 1000mL",Pyrex,6,,
|
||||
86,"Distilling Flask, Round Bottom, 100mL",,3,,
|
||||
87,"Distilling Reciever, Dean Stark",,2,,
|
||||
92,"Flask, Centrifuge, TS-Joint, 1000mL",,1,,PKI donated
|
||||
93,"Flask, Centrifuge, TS-Joint, 1000mL",Vidrex,2,,
|
||||
94,"Flask, Conical 500mL",Mars Lab,20,,
|
||||
95,"Flask, Distilling, Round Bottom, 1000mL",,1,,PKI donated
|
||||
96,"Flask, Distilling, Round Bottom, 1000mL",Pyrex,1,,
|
||||
98,"Flask, Erlenmeyer, Low Actinic, 250mL",Pyrex,4,,
|
||||
99,"Flask, Erlenmeyer, Narrowmouth, 1000mL",Pyrex,5,,
|
||||
11,"Flask, Erlenmeyer, Narrowmouth, 125mL",Pyrex,11,,
|
||||
103,"Flask, Erlenmeyer, Narrowmouth, 250mL",Pyrex,9,,
|
||||
105,"Flask, Erlenmeyer, Narrowmouth, 300mL",Pyrex,3,,
|
||||
63,"Flask, Erlenmeyer, Narrowmouth, 500mL",Pyrex,63,6,
|
||||
116,"Flask, Kjeldal, 200mL",,8,,
|
||||
117,"Flask, Kjeldal, 500mL",,1,,
|
||||
118,"Flask, Kjeldal, 500mL",Mars Lab,33,,
|
||||
119,"Flask, Pear Shape, Shortneck, 1000mL",Pyrex,4,,
|
||||
120,"Flask, Pear Shape, Shortneck, 100mL",Pyrex,8,,
|
||||
121,"Flask, Pear Shape, Shortneck, 500mL",Pyrex,5,,
|
||||
122,"Flask, Pear Shape, Shortneck, 50mL",Pyrex,2,,
|
||||
131,"Flask, Volumetric, 100mL",Pyrex,7,,
|
||||
132,"Flask, Volumetric, 100mL",Mars Lab,4,,
|
||||
134,"Flask, Volumetric, 200mL",Pyrex,4,,
|
||||
136,"Flask, Volumetric, 250mL",Pyrex,15,,
|
||||
139,"Flask, Volumetric, 500mL",Pyrex,1,,
|
||||
141,"Flask, Volumetric, 50mL",Pyrex,1,,
|
||||
142,"Flask, Volumetric, 50mL",Mars Lab,7,,flasks have a broken stoppers
|
||||
143,"Funnel, Dropping, 1000mL",Pyrex,1,,PKI donated
|
||||
145,"Funnel, Separatory, Pear Shape, 100mL",Vidrex,3,,
|
||||
147,"Funnel, Separatory, Pear Shape, 250mL",Mars Lab,23,,
|
||||
148,"Funnel, Separatory, Pear Shape, 500mL",Pyrex,11,,
|
||||
149,"Funnel, Separatory, Pear Shape, 500mL",Pyrex,1,,
|
||||
149,"Funnel, Separatory, Pear Shape, 50mL",Pyrex,1,,
|
||||
150,"Funnel, Short Stem, 100mm",Pyrex,4,,
|
||||
152,"Funnel, Short Stem, 60° angle",Mars Lab,23,,
|
||||
171,"Pipet, Measuring, 10mL",Mars Lab,11,,
|
||||
172,"Pipet, Measuring, 1mL",,3,,
|
||||
174,"Pipet, Measuring, 25mL",,19,,
|
||||
190,"Pycnometer, 25mL",Mars Lab,1,,
|
||||
192,Recovery Bend w/ 2 Vertical Cones,,1,,
|
||||
193,Retort,,3,2,
|
||||
194,"Slide Glass, 100pcs. Per box",Matsunami,8,,
|
||||
197,"Slide Glass, 72pcs. Per pack",Sail Brand,1,,
|
||||
198,"Soxhlet Extraction Tube, TS-Joint, 34/28",Vidrex,4,,
|
||||
199,"Soxhlet Extraction Tube, TS-Joint, Large",,12,,
|
||||
200,Stirring Rod,,5,,
|
||||
221,Tilting Dispense,,5,,
|
||||
223,"Watch Glass, 100mm dia. ",Mars Lab,50,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
|
75
equipment_tracker/media/Glassware-Dispensing Room.csv
Normal file
75
equipment_tracker/media/Glassware-Dispensing Room.csv
Normal file
|
@ -0,0 +1,75 @@
|
|||
,NAME,Brand/Supplier,Qty.,Breakages,Remarks
|
||||
1,"Beaker, 1000mL",,2,,
|
||||
3,"Beaker, 100mL",,148,3,
|
||||
6,"Beaker, 150mL",,48,,
|
||||
8,"Beaker, 250mL",,132,8,
|
||||
11,"Beaker, 400mL",,26,,
|
||||
14,"Beaker, 50mL",,122,3,
|
||||
15,"Beaker, 500mL",,1,,
|
||||
16,"Beaker, 600mL",,14,,
|
||||
17,"Beaker, 800mL",,1,,
|
||||
38,"Buret, Teflon Stopcock, 100mL",,13,,
|
||||
40,"Buret, Teflon Stopcock, 25mL",,9,,
|
||||
41,"Buret, Teflon Stopcock, 50mL",,11,,
|
||||
64,"Cylinder, Graduated, 250mL",,7,,
|
||||
65,"Cylinder, Graduated, 100mL",,38,2,
|
||||
67,"Cylinder, Graduated, 10mL",,44,1,
|
||||
69,"Cylinder, Graduated, 25mL",,16,1,
|
||||
70,"Cylinder, Graduated, 50mL",,20,2,
|
||||
84,"Distillation Set up, Distilling Flask(1000mL) and Condenser",,3,,
|
||||
99,"Flask, Erlenmeyer, Narrowmouth, 1000mL",,18,,
|
||||
100,"Flask, Erlenmeyer, Narrowmouth, 125mL",,37,2,
|
||||
102,"Flask, Erlenmeyer, Narrowmouth, 2000mL",,2,,
|
||||
103,"Flask, Erlenmeyer, Narrowmouth, 250mL",,52,4,
|
||||
105,"Flask, Erlenmeyer, Narrowmouth, 300mL",,21,,
|
||||
106,"Flask, Erlenmeyer, Narrowmouth, 500mL",,41,,
|
||||
112,"Flask, Filtering 500mL",,11,2,
|
||||
129,"Flask, Volumetric, 1000mL",,10,,
|
||||
131,"Flask, Volumetric, 100mL",,,,
|
||||
132,"Flask, Volumetric, 100mL",,38,,
|
||||
134,"Flask, Volumetric, 200mL",,5,,
|
||||
137,"Flask, Volumetric, 250mL",,10,,
|
||||
139,"Flask, Volumetric, 500mL",,15,,
|
||||
141,"Flask, Volumetric, 50mL",,11,,
|
||||
146,"Funnel, Separatory, Pear Shape, 250mL",,13,1,
|
||||
148,"Funnel, Separatory, Pear Shape, 500mL",,2,,
|
||||
151,"Funnel, Short Stem, 50mm",,28,1,
|
||||
163,"Petridish with cover, Glass, 100x15mm",,23,,
|
||||
165,"Petridish without cover, Glass, 100x15mm",,3,,
|
||||
166,"Petridish, with cover, Glass,140mmx17mm",,3,,
|
||||
167,"Petridish, with cover, Glass,60mmx15mm",,5,,
|
||||
168,"Petridish, without cover, Glass,140x17mm",,7,,
|
||||
171,"Pipet, Measuring, 10mL",,15,,
|
||||
172,"Pipet, Measuring, 1mL",,11,,
|
||||
174,"Pipet, Measuring, 25mL",,12,,
|
||||
176,"Pipet, Measuring, 2mL",,4,,
|
||||
178,"Pipet, Measuring, 5mL",,5,,
|
||||
179,"Pipet, Volumetric 15mL",,2,,
|
||||
180,"Pipet, Volumetric, 10mL",,22,1,
|
||||
181,"Pipet, Volumetric, 1mL",,2,,
|
||||
182,"Pipet, Volumetric, 20mL",,1,,
|
||||
186,"Pipet, Volumetric, 50mL",,2,,
|
||||
187,"Pipet, Volumetric, 5mL",,4,,
|
||||
189,"Pycnometer, 25mL",,8,,
|
||||
200,Stirring Rod,,64,,
|
||||
223,"Watch Glass, 100mm dia. ",,46,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
|
88
equipment_tracker/media/Glassware-Main Stockroom.csv
Normal file
88
equipment_tracker/media/Glassware-Main Stockroom.csv
Normal file
|
@ -0,0 +1,88 @@
|
|||
,NAME,Brand/Supplier,Qty.,Breakages,Remarks
|
||||
3,"Beaker, 100mL",,27,,
|
||||
6,"Beaker, 150mL",,2,,
|
||||
8,"Beaker, 250mL",,116,,
|
||||
11,"Beaker, 400mL",,185,,
|
||||
15,"Beaker, 50mL",,30,,
|
||||
20,"Bottle Reagent, Clear, NM, 1000mL",,6,,
|
||||
21,"Bottle Reagent, Clear, NM, 1000mL",Mars Lab,15,,
|
||||
22,"Bottle Reagent, Clear, NM, 100mL",,13,,
|
||||
23,"Bottle Reagent, Clear, NM, 2000mL",,5,,
|
||||
24,"Bottle Reagent, Clear, NM, 2500mL",,2,,
|
||||
25,"Bottle Reagent, Clear, NM, 5000mL",,6,,
|
||||
26,"Bottle Reagent, Clear, NM, 500mL",,32,,
|
||||
27,"Bottle Reagent, Clear, WM, 250mL",,11,,
|
||||
28,"Bottle Reagent, Clear, WM, 40mL",,3,,
|
||||
29,"Bottle Reagent, Clear, WM, 500mL",,4,,
|
||||
30,"Bottle, BOD, 100mL",,12,,
|
||||
31,"Bottle, BOD, 300mL",,48,26,
|
||||
32,"Bottle, Media, 250mL",,6,,
|
||||
33,"Bottle, Media, 500mL",,2,,
|
||||
,"Buret, Teflon Stopcock, 100mL",,5,,
|
||||
41,"Buret, Teflon Stopcock, 50mL",,84,,
|
||||
64,"Cylinder, Graduated, 100mL",,36,,
|
||||
65,"Cylinder, Graduated, 10mL",,38,,
|
||||
67,"Cylinder, Graduated, 250mL",,3,,
|
||||
70,"Cylinder, Graduated, 50mL",,28,,
|
||||
84,"Distillation Set up, Distilling Flask(1000mL) and Condenser",,5,,
|
||||
99,"Flask, Erlenmeyer, Narrowmouth, 1000mL",,17,,
|
||||
100,"Flask, Erlenmeyer, Narrowmouth, 125mL",,13,,
|
||||
102,"Flask, Erlenmeyer, Narrowmouth, 2000mL",,7,,
|
||||
103,"Flask, Erlenmeyer, Narrowmouth, 250mL",,68,,
|
||||
105,"Flask, Erlenmeyer, Narrowmouth, 300mL",,1,,
|
||||
106,"Flask, Erlenmeyer, Narrowmouth, 500mL",,9,,
|
||||
112,"Flask, Filtering 500mL",,55,,
|
||||
124,"Flask, Round Bottom, Longneck, 500mL",,19,,
|
||||
132,"Flask, Volumetric, 100mL",,7,,
|
||||
139,"Flask, Volumetric, 500mL",,20,,
|
||||
146,"Funnel, Separatory, Pear Shape, 250mL",,23,,
|
||||
162,Nitrogen Distillation Apparatus,,1,,
|
||||
174,"Pipet, Measuring, 25mL",,37,,
|
||||
189,"Pycnometer, 25mL",,19,,
|
||||
200,Stirring Rod,,68,,
|
||||
194,"Slide Glass, 100pcs. Per box",Matsunami,10,,
|
||||
195,"Slide Glass, 100pcs. Per box",Mars Lab,18,,
|
||||
196,"Slide Glass, 50pcs. Per pack",Toshinriko,1,,
|
||||
197,"Slide Glass, 72pcs. Per pack",Sail Brand,1,,
|
||||
198,"Soxhlet Extraction Tube, TS-Joint, 34/28",Vidrex,4,,
|
||||
199,"Soxhlet Extraction Tube, TS-Joint, Large",,12,,
|
||||
202,Test Tube with Stopper 10mL,Mars Lab,18,,
|
||||
203,Test Tube with screw caps 30mL,Mars Lab,236,,
|
||||
204,"Test Tube, 10mL",Mars Lab,1924,45,
|
||||
205,"Test Tube, 12mm OD x 120mmL",Hach,400,42,
|
||||
206,"Test Tube, 12mm OD x 7mmL",,60,,
|
||||
207,"Test Tube, 13mm OD x 110mmL",,88,,
|
||||
208,"Test Tube, 13mm OD x 75mmL",,5,,
|
||||
209,"Test Tube, 15mm OD x 100mmL",,16,,
|
||||
210,"Test Tube, 16mm OD x 100mmL",,65,,
|
||||
211,"Test Tube, 16mm OD x 125mmL",,191,20,
|
||||
212,"Test Tube, 16mm OD x 60mmL",,64,,
|
||||
213,"Test Tube, 18mm OD x 165mmL",,98,,
|
||||
214,"Test Tube, 18mm OD x 16mmL",,54,,
|
||||
215,"Test Tube, 20mL",Mars Lab,237,,
|
||||
216,"Test Tube, 25mm OD x 200mmL",,81,,
|
||||
217,"Test Tube, 50mL",Mars Lab,90,,
|
||||
218,"Test Tube, Borosilicate Glass, 10/15",,2,,
|
||||
219,"Test Tube, Borosilicate Glass, 100mL",,34,,
|
||||
220,"Test Tube, Borosilicate Glass, 50mL",,55,,
|
||||
223,"Watch Glass, 100mm dia. ",,86,23,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
|
49
equipment_tracker/media/Miscellaneous-2nd Floor Chemlab.csv
Normal file
49
equipment_tracker/media/Miscellaneous-2nd Floor Chemlab.csv
Normal file
|
@ -0,0 +1,49 @@
|
|||
,NAME,Brand/Supplier,Qty.,Breakages,Remarks
|
||||
1,Alcohol Lamp,Mars Lab,20,,
|
||||
2,"Basin, Plastic, 12inch Dia.",Mars Lab,1,,
|
||||
3,"Bottle, Washing, Assorted, 500mL",Mars Lab,83,,
|
||||
4,"Brush, Test Tube, Nylon, Regular",,12,,
|
||||
5,Bunsen Burner,Mars Lab,53,,
|
||||
6,"Clamp, Double Buret",Mars Lab,70,,
|
||||
7,"Clamp, Extension",,268,,
|
||||
8,"Clamp, Holder",Mars Lab,108,,
|
||||
9,Conductivity Apparatus,Mars Lab,76,,
|
||||
10,"Crucible Tong, 9 ¾cm",,68,,
|
||||
11,"Crucible with cover,Porcelain 30mL",Mars Lab,76,,
|
||||
14,"Iron Ring, 117mm I.D",Mars Lab,46,,
|
||||
16,"Mortar & Pestle, Porcelain, 400mL Vol.",Mars Lab,20,,
|
||||
17,"Spot Plate, Porcelain, 12 Crates",Mars Lab,92,,
|
||||
18,Test Tube Holder,Mars Lab,244,,
|
||||
19,"Test Tube Rack, Stainless, 50holes (ID)",,131,,
|
||||
20,"Test Tube Rack, Stainless, 50holes (ID), Big",,16,,
|
||||
21,"Thermometer, Alcohol -10° - 350°C",Mars Lab,119,,
|
||||
22,"Thermometer, Mercury -10° - 350°C",Mars Lab,113,,
|
||||
23,Triple Beam Balance,Mars Lab,28,,
|
||||
24,Water Bath,Mars Lab,73,,
|
||||
25,"Wire Gauze, Ceramic Centered, 5x5",Mars Lab,6,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
|
57
equipment_tracker/media/Miscellaneous-Dispensing Room.csv
Normal file
57
equipment_tracker/media/Miscellaneous-Dispensing Room.csv
Normal file
|
@ -0,0 +1,57 @@
|
|||
,NAME,Brand/Supplier,Qty.,Breakages,Remarks
|
||||
1,Alcohol Lamp,Mars Lab,9,1,
|
||||
2,"Basin, Plastic, 12inch Dia.",Mars Lab,4,,
|
||||
3,"Bottle, Washing, Assorted, 500mL",Mars Lab,67,4,
|
||||
4,"Brush, Test Tube, Nylon, Regular",,47,12,
|
||||
5,"Bulb, Aspirator, Orange, Pointed Tip",,33,16,
|
||||
6,Bunsen Burner,Mars Lab,15,4,
|
||||
7,"Clamp, Double Buret",Mars Lab,26,,
|
||||
8,"Clamp, Extension",,30,3,
|
||||
9,"Crucible Tong, 9 ¾cm",,37,,
|
||||
10,"Crucible with cover,Porcelain 30mL",Mars Lab,82,34,
|
||||
11,"Crucible, Metal w/o cover, 30mL",,8,,
|
||||
12,"Evaporating dish, 120mL",Mars Lab,22,15,
|
||||
13,"Forceps, Stainless",Mars Lab,10,3,
|
||||
14,Funnel Stand,,9,,
|
||||
15,"Funnel, Buchner, 200mm",,5,,
|
||||
16,"Hot Plate, Single Burner",Mars Lab,5,1,
|
||||
17,"Hydrometer, range: 1.000 tp 1.070",Mars Lab,,,
|
||||
18,"Iron Ring, 117mm I.D",Mars Lab,24,,
|
||||
19,Iron Stand,Mars Lab,26,,
|
||||
20,"Iron Wire Triangles, 3.8(1.5)cm(in.)",,9,4,
|
||||
21,"Mortar & Pestle, Porcelain, 400mL Vol.",Mars Lab,28,,
|
||||
22,"Spot Plate, Porcelain, 12 Crates",Mars Lab,19,,
|
||||
23,"Spot Plate, Porcelain, 12 Holes, Plastic",,5,4,
|
||||
24,Test Tube Holder,Mars Lab,31,,
|
||||
25,"Test Tube Rack, Stainless, 50holes (ID), ",,34,,
|
||||
26,"Thermometer, Alcohol -10° - 350°C",Mars Lab,44,12,
|
||||
27,"Thermometer, Mercury -10° - 350°C",Mars Lab,29,11,
|
||||
28,Triple Beam Balance,Mars Lab,11,,
|
||||
29,Water Bath,Mars Lab,21,5,
|
||||
30,"Wire Gauze, Ceramic Centered, 5x5",Mars Lab,60,45,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
|
49
equipment_tracker/media/Miscellaneous-Main Stockroom.csv
Normal file
49
equipment_tracker/media/Miscellaneous-Main Stockroom.csv
Normal file
|
@ -0,0 +1,49 @@
|
|||
,NAME,Brand/Supplier,Qty.,Breakages,Remarks
|
||||
1,Alcohol Lamp,Mars Lab,20,,
|
||||
2,"Basin, Plastic, 12inch Dia.",Mars Lab,1,,
|
||||
3,"Bottle, Washing, Assorted, 500mL",Mars Lab,83,,
|
||||
4,"Brush, Test Tube, Nylon, Regular",,12,,
|
||||
5,Bunsen Burner,Mars Lab,53,,
|
||||
6,"Clamp, Double Buret",Mars Lab,70,,
|
||||
7,"Clamp, Extension",,268,,
|
||||
8,"Clamp, Holder",Mars Lab,108,,
|
||||
9,Conductivity Apparatus,Mars Lab,76,,
|
||||
10,"Crucible Tong, 9 ¾cm",,68,,
|
||||
11,"Crucible with cover,Porcelain 30mL",Mars Lab,76,,
|
||||
14,"Iron Ring, 117mm I.D",Mars Lab,46,,
|
||||
16,"Mortar & Pestle, Porcelain, 400mL Vol.",Mars Lab,20,,
|
||||
17,"Spot Plate, Porcelain, 12 Crates",Mars Lab,92,,
|
||||
18,Test Tube Holder,Mars Lab,244,,
|
||||
19,"Test Tube Rack, Stainless, 50holes (ID)",,131,,
|
||||
20,"Test Tube Rack, Stainless, 50holes (ID), Big",,16,,
|
||||
21,"Thermometer, Alcohol -10° - 350°C",Mars Lab,119,,
|
||||
22,"Thermometer, Mercury -10° - 350°C",Mars Lab,113,,
|
||||
23,Triple Beam Balance,Mars Lab,28,,
|
||||
24,Water Bath,Mars Lab,73,,
|
||||
25,"Wire Gauze, Ceramic Centered, 5x5",Mars Lab,6,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
||||
,,,,,
|
|
|
@ -1586,9 +1586,14 @@ components:
|
|||
remarks:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 512
|
||||
transaction_status:
|
||||
$ref: '#/components/schemas/TransactionStatusEnum'
|
||||
additional_members:
|
||||
type: string
|
||||
nullable: true
|
||||
consumables:
|
||||
type: string
|
||||
nullable: true
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
@ -1697,9 +1702,14 @@ components:
|
|||
remarks:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 512
|
||||
transaction_status:
|
||||
$ref: '#/components/schemas/TransactionStatusEnum'
|
||||
additional_members:
|
||||
type: string
|
||||
nullable: true
|
||||
consumables:
|
||||
type: string
|
||||
nullable: true
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.1 on 2024-01-06 16:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='transaction_status',
|
||||
field=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')], db_index=True, default='Pending', max_length=40),
|
||||
),
|
||||
]
|
|
@ -2,6 +2,7 @@ from django.db import models
|
|||
from accounts.models import CustomUser
|
||||
from equipments.models import EquipmentInstance
|
||||
from django.utils.timezone import now
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
|
@ -34,9 +35,13 @@ class Transaction(models.Model):
|
|||
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')
|
||||
max_length=40, choices=TRANSACTION_STATUS_CHOICES, default='Pending', db_index=True)
|
||||
subject = models.TextField(max_length=128)
|
||||
timestamp = models.DateTimeField(default=now, editable=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Transaction #{self.id} under {self.teacher} by {self.borrower}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
cache.delete('non_finalized_transactions')
|
||||
return super().save(*args, **kwargs)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue