diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index 6c78a6e..28c985b 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -35,7 +35,8 @@ ALLOWED_HOSTS = [ 'https://api.rasadyaar.net', 'https://api.dam.rasadyaar.net', 'http://localhost:3000', - 'http://192.168.88.130:3000' + 'http://192.168.88.130:3000', + 'https://rasaddam-front.liara.run' ] # Application definition @@ -151,6 +152,7 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), + 'EXCEPTION_HANDLER': 'apps.core.error_handler.custom_exception_handler', "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 25, 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' @@ -282,16 +284,6 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 50242880 CORS_ORIGIN_ALLOW_ALL = False CORS_ALLOW_CREDENTIALS = True -# CORS_ORIGIN_WHITELIST = ( -# # 'http://localhost:8080', -# # 'http://127.0.0.1:8080', -# # 'http://127.0.0.1:3000', -# # 'http://localhost:3000', -# # 'http://192.168.88.130:3000', -# # 'https://rasadyar.net' -# '*', -# ) -# CORS_ALLOWED_ORIGINS = ( 'http://localhost:8080', 'http://127.0.0.1:8080', @@ -299,6 +291,7 @@ CORS_ALLOWED_ORIGINS = ( 'http://localhost:3000', 'http://192.168.88.130:3000', 'https://rasadyar.net' + 'https://rasaddam-front.liara.run' ) SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') diff --git a/Rasaddam_Backend/urls.py b/Rasaddam_Backend/urls.py index 075609b..e9cd972 100644 --- a/Rasaddam_Backend/urls.py +++ b/Rasaddam_Backend/urls.py @@ -17,6 +17,14 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from apps.core.swagger import schema_view +from django.conf.urls import ( + handler400, + handler403, + handler500, + handler404, +) + +# handler500 = 'apps.core.error_handler.handler500' # noqa urlpatterns = [ path('admin/', admin.site.urls), diff --git a/apps/authentication/migrations/0018_organization_additional_data.py b/apps/authentication/migrations/0018_organization_additional_data.py new file mode 100644 index 0000000..458d67e --- /dev/null +++ b/apps/authentication/migrations/0018_organization_additional_data.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-25 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0017_bankaccountinformation_creator_info_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='additional_data', + field=models.JSONField(default=dict), + ), + ] diff --git a/apps/authentication/migrations/0019_organizationtype_code.py b/apps/authentication/migrations/0019_organizationtype_code.py new file mode 100644 index 0000000..311d3c2 --- /dev/null +++ b/apps/authentication/migrations/0019_organizationtype_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-26 11:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0018_organization_additional_data'), + ] + + operations = [ + migrations.AddField( + model_name='organizationtype', + name='code', + field=models.IntegerField(default=0), + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 195838f..0c30eca 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -76,6 +76,7 @@ class OrganizationType(BaseModel): ) key = models.CharField(choices=organization_keys, max_length=3) name = models.CharField(max_length=50, null=True) + code = models.IntegerField(default=0) def __str__(self): return f'{self.key}-{self.name}' @@ -118,6 +119,7 @@ class Organization(BaseModel): related_name='parents', null=True ) + additional_data = models.JSONField(default=dict) def __str__(self): return f'{self.name}-{self.type}' diff --git a/apps/core/error_handler.py b/apps/core/error_handler.py new file mode 100644 index 0000000..af68f1e --- /dev/null +++ b/apps/core/error_handler.py @@ -0,0 +1,15 @@ +from django.http import JsonResponse +from rest_framework.views import exception_handler + + +def custom_exception_handler(exc, context): + response = exception_handler(exc, context) + + if response is not None: + response.data['status_code'] = response.status_code + response.data['message'] = response.data.get('detail', str(exc)) + del response.data['detail'] + else: + response = JsonResponse({'message': str(exc), 'status_code': 500}) + response.status_code = 500 + return response diff --git a/apps/herd/permissions.py b/apps/herd/permissions.py index 25edb52..7cd31d9 100644 --- a/apps/herd/permissions.py +++ b/apps/herd/permissions.py @@ -1,25 +1,37 @@ -from rest_framework import permissions +from apps.core import permissions -# example Code -class AuthorAllStaffAllButEditOrReadOnly(permissions.BasePermission): - edit_methods = ("PUT", "PATCH") +class HerdCreatePermission(permissions.BasePermission): + """ permission to create herd """ def has_permission(self, request, view): - if request.user.is_authenticated: + user_level_info = self.get_user_permissions(request, view) + if 'herd_create' in user_level_info['permissions']: return True - def has_object_permission(self, request, view, obj): - if request.user.is_superuser: + +class HerdUpdatePermission(permissions.BasePermission): + """ permission to update herd """ + + def has_permission(self, request, view): + user_level_info = self.get_user_permissions(request, view) + if 'herd_update' in user_level_info['permissions']: return True - if request.method in permissions.SAFE_METHODS: + +class HerdTrashPermission(permissions.BasePermission): + """ permission to trash herd """ + + def has_permission(self, request, view): + user_level_info = self.get_user_permissions(request, view) + if 'herd_trash' in user_level_info['permissions']: return True - if obj.author == request.user: - return True - if request.user.is_staff and request.method not in self.edit_methods: - return True +class HerdDeletePermission(permissions.BasePermission): + """ permission to delete herd """ - return False + def has_permission(self, request, view): + user_level_info = self.get_user_permissions(request, view) + if 'herd_delete' in user_level_info['permissions']: + return True diff --git a/apps/tag/exceptions.py b/apps/tag/exceptions.py new file mode 100644 index 0000000..5641803 --- /dev/null +++ b/apps/tag/exceptions.py @@ -0,0 +1,8 @@ +from rest_framework.exceptions import APIException +from rest_framework import status + + +class SpeciesNumberCheckException(APIException): + status_code = status.HTTP_403_FORBIDDEN + default_detail = 'Entered species number is more than user free tags' + default_code = 'more than free tags' diff --git a/apps/tag/migrations/0008_remove_tag_code_tag_country_code_tag_ownership_code_and_more.py b/apps/tag/migrations/0008_remove_tag_code_tag_country_code_tag_ownership_code_and_more.py new file mode 100644 index 0000000..567d257 --- /dev/null +++ b/apps/tag/migrations/0008_remove_tag_code_tag_country_code_tag_ownership_code_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 5.0 on 2025-05-25 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0007_tag_creator_info_tag_modifier_info'), + ] + + operations = [ + migrations.RemoveField( + model_name='tag', + name='code', + ), + migrations.AddField( + model_name='tag', + name='country_code', + field=models.IntegerField(default=364), + ), + migrations.AddField( + model_name='tag', + name='ownership_code', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='tag', + name='serial', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='tag', + name='species_code', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='tag', + name='static_code', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='tag', + name='tag_code', + field=models.CharField(max_length=20, null=True), + ), + ] diff --git a/apps/tag/migrations/0009_alter_tag_tag_code.py b/apps/tag/migrations/0009_alter_tag_tag_code.py new file mode 100644 index 0000000..46561f1 --- /dev/null +++ b/apps/tag/migrations/0009_alter_tag_tag_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-25 11:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0008_remove_tag_code_tag_country_code_tag_ownership_code_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='tag_code', + field=models.CharField(max_length=20, unique=True), + ), + ] diff --git a/apps/tag/migrations/0010_alter_tag_tag_code.py b/apps/tag/migrations/0010_alter_tag_tag_code.py new file mode 100644 index 0000000..919da36 --- /dev/null +++ b/apps/tag/migrations/0010_alter_tag_tag_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-25 11:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0009_alter_tag_tag_code'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='tag_code', + field=models.CharField(max_length=20, null=True, unique=True), + ), + ] diff --git a/apps/tag/migrations/0011_remove_tag_city_remove_tag_province.py b/apps/tag/migrations/0011_remove_tag_city_remove_tag_province.py new file mode 100644 index 0000000..41566b6 --- /dev/null +++ b/apps/tag/migrations/0011_remove_tag_city_remove_tag_province.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0 on 2025-05-25 12:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0010_alter_tag_tag_code'), + ] + + operations = [ + migrations.RemoveField( + model_name='tag', + name='city', + ), + migrations.RemoveField( + model_name='tag', + name='province', + ), + ] diff --git a/apps/tag/migrations/0012_remove_tag_serial.py b/apps/tag/migrations/0012_remove_tag_serial.py new file mode 100644 index 0000000..bc106ed --- /dev/null +++ b/apps/tag/migrations/0012_remove_tag_serial.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0 on 2025-05-26 05:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0011_remove_tag_city_remove_tag_province'), + ] + + operations = [ + migrations.RemoveField( + model_name='tag', + name='serial', + ), + ] diff --git a/apps/tag/migrations/0013_tag_serial.py b/apps/tag/migrations/0013_tag_serial.py new file mode 100644 index 0000000..8e54a8f --- /dev/null +++ b/apps/tag/migrations/0013_tag_serial.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-26 05:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0012_remove_tag_serial'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='serial', + field=models.CharField(default=0, max_length=8), + ), + ] diff --git a/apps/tag/migrations/0014_alter_tag_serial.py b/apps/tag/migrations/0014_alter_tag_serial.py new file mode 100644 index 0000000..f807dcd --- /dev/null +++ b/apps/tag/migrations/0014_alter_tag_serial.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-26 07:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0013_tag_serial'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='serial', + field=models.CharField(max_length=8, unique=True), + ), + ] diff --git a/apps/tag/migrations/0015_alter_tag_status_allocatedtags_tagassignment_and_more.py b/apps/tag/migrations/0015_alter_tag_status_allocatedtags_tagassignment_and_more.py new file mode 100644 index 0000000..7ac801a --- /dev/null +++ b/apps/tag/migrations/0015_alter_tag_status_allocatedtags_tagassignment_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0 on 2025-05-26 11:50 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0019_organizationtype_code'), + ('tag', '0014_alter_tag_serial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='status', + field=models.CharField(default='Free', max_length=20), + ), + migrations.CreateModel( + name='AllocatedTags', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ('tag', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocated_tags', to='tag.tag')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TagAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('document', models.CharField(max_length=50, null=True)), + ('serial_sender_part', models.IntegerField(default=0)), + ('serial_recipient_part', models.IntegerField(default=0)), + ('serial_date_part', models.CharField(max_length=3, null=True)), + ('serial_random_part', models.IntegerField(default=0)), + ('serial', models.CharField(max_length=20, null=True)), + ('status', models.CharField(choices=[('A', 'Accept'), ('W', 'Waiting'), ('C', 'Cancel')], default='W', max_length=1)), + ('allocated_tags', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tag_assignment_alloc', to='tag.allocatedtags')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tag_assignment_org', to='authentication.organization')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='allocatedtags', + name='assignment', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assigned_allocated_tags', to='tag.tagassignment'), + ), + ] diff --git a/apps/tag/migrations/0016_alter_tag_serial.py b/apps/tag/migrations/0016_alter_tag_serial.py new file mode 100644 index 0000000..f174e21 --- /dev/null +++ b/apps/tag/migrations/0016_alter_tag_serial.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-26 12:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0015_alter_tag_status_allocatedtags_tagassignment_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='serial', + field=models.CharField(max_length=8), + ), + ] diff --git a/apps/tag/migrations/0017_alter_tag_status.py b/apps/tag/migrations/0017_alter_tag_status.py new file mode 100644 index 0000000..33bca4d --- /dev/null +++ b/apps/tag/migrations/0017_alter_tag_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-26 12:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0016_alter_tag_serial'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='status', + field=models.CharField(default='F', max_length=20), + ), + ] diff --git a/apps/tag/migrations/0018_remove_tagassignment_allocated_tags.py b/apps/tag/migrations/0018_remove_tagassignment_allocated_tags.py new file mode 100644 index 0000000..8f44c7a --- /dev/null +++ b/apps/tag/migrations/0018_remove_tagassignment_allocated_tags.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0 on 2025-05-26 13:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0017_alter_tag_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='tagassignment', + name='allocated_tags', + ), + ] diff --git a/apps/tag/migrations/0019_allocatedtags_status.py b/apps/tag/migrations/0019_allocatedtags_status.py new file mode 100644 index 0000000..3b0fe2b --- /dev/null +++ b/apps/tag/migrations/0019_allocatedtags_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-27 06:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0018_remove_tagassignment_allocated_tags'), + ] + + operations = [ + migrations.AddField( + model_name='allocatedtags', + name='status', + field=models.CharField(default='W', max_length=1), + ), + ] diff --git a/apps/tag/migrations/0020_alter_tagassignment_document.py b/apps/tag/migrations/0020_alter_tagassignment_document.py new file mode 100644 index 0000000..f324987 --- /dev/null +++ b/apps/tag/migrations/0020_alter_tagassignment_document.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-27 08:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0019_allocatedtags_status'), + ] + + operations = [ + migrations.AlterField( + model_name='tagassignment', + name='document', + field=models.CharField(max_length=150, null=True), + ), + ] diff --git a/apps/tag/migrations/0021_allocatedtags_species_code.py b/apps/tag/migrations/0021_allocatedtags_species_code.py new file mode 100644 index 0000000..290aa5f --- /dev/null +++ b/apps/tag/migrations/0021_allocatedtags_species_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-05-27 10:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0020_alter_tagassignment_document'), + ] + + operations = [ + migrations.AddField( + model_name='allocatedtags', + name='species_code', + field=models.IntegerField(default=0), + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index e704a29..61b74b7 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -1,36 +1,121 @@ from apps.livestock import models as livestock_models from apps.authentication import models as auth_models from apps.authorization import models as authoriz_models +from apps.tag.tools import tag_code_serial_scanning from apps.core.models import BaseModel +from crum import get_current_user from django.db import models +from jdatetime import datetime class Tag(BaseModel): - code = models.CharField(max_length=20) - province = models.ForeignKey( - auth_models.Province, - on_delete=models.CASCADE, - related_name="tag_province", - null=True - ) - city = models.ForeignKey( - auth_models.City, - on_delete=models.CASCADE, - related_name='tag_city', - null=True - ) + country_code = models.IntegerField(default=364) + static_code = models.IntegerField(default=0) + ownership_code = models.IntegerField(default=0) + species_code = models.IntegerField(default=0) + serial = models.CharField(max_length=8) + tag_code = models.CharField(max_length=20, unique=True, null=True) organization = models.ForeignKey( auth_models.Organization, on_delete=models.CASCADE, related_name='tag_organization', null=True ) - status = models.CharField(max_length=20, null=True) + status_choices = ( + ('F', 'Free'), + ('A', 'Assigned') + ) + status = models.CharField(max_length=20, default="F") def __str__(self): - return f'{self.code}' + return f'{self.id}-{self.tag_code}' def save(self, *args, **kwargs): + # set zeros for serial + if not self.tag_code: + self.serial = tag_code_serial_scanning(self.serial) + if not self.tag_code: + # set total tag code + self.tag_code = f"{self.country_code}" \ + f"{self.static_code}" \ + f"{self.ownership_code}" \ + f"{self.species_code}" \ + f"{self.serial}" + if not self.organization: + # set user organization for tag + user = get_current_user() + self.organization = ( + authoriz_models.UserRelations.objects.select_related( + 'organization' + ).get( + user=user + )).organization + super(Tag, self).save(*args, **kwargs) +class TagAssignment(BaseModel): + organization = models.ForeignKey( + auth_models.Organization, + on_delete=models.CASCADE, + related_name='tag_assignment_org', + null=True + ) + document = models.CharField(max_length=150, null=True) + serial_sender_part = models.IntegerField(default=0) + serial_recipient_part = models.IntegerField(default=0) + serial_date_part = models.CharField(max_length=3, null=True) + serial_random_part = models.IntegerField(default=0) + serial = models.CharField(max_length=20, null=True) + status_choices = ( + ('A', 'Accept'), + ('W', 'Waiting'), + ('C', 'Cancel'), + ) + status = models.CharField(max_length=1, choices=status_choices, default="W") + + def __str__(self): + return f'{self.serial}-{self.organization.name}' + + def save(self, *args, **kwargs): + self.serial_recipient_part = self.organization.type.code + self.serial_date_part = str( + datetime.now().year + )[3] + str( + datetime.now().month + ) + if not self.serial: + self.serial = f"" \ + f"{self.serial_sender_part}" \ + f"{self.serial_recipient_part}" \ + f"{self.serial_date_part}" \ + f"{self.serial_random_part}" + super(TagAssignment, self).save(*args, **kwargs) + + +class AllocatedTags(BaseModel): + tag = models.ForeignKey( + Tag, + on_delete=models.CASCADE, + related_name='allocated_tags', + null=True + ) + assignment = models.ForeignKey( + TagAssignment, + on_delete=models.CASCADE, + related_name='assigned_allocated_tags', + null=True + ) + species_code = models.IntegerField(default=0) + status_choices = ( + ('A', 'Accept'), + ('W', 'Waiting'), + ('C', 'Cancel') + ) + status = models.CharField(max_length=1, default='W') + + def __str__(self): + return f'{self.tag.tag_code}-{self.assignment.serial}' + + def save(self, *args, **kwargs): + super(AllocatedTags, self).save(*args, **kwargs) diff --git a/apps/tag/tools.py b/apps/tag/tools.py new file mode 100644 index 0000000..b3ecec4 --- /dev/null +++ b/apps/tag/tools.py @@ -0,0 +1,20 @@ +import typing + + +def tag_code_serial_scanning(serial: str = None) -> typing.AnyStr: + """ + serial code is 8 number serial, + set 4 first numbers to 0 + """ + if len(str(serial)) == 4: + scanned_serial = "0000" + str(serial) + if len(str(serial)) == 5: + scanned_serial = "000" + str(serial) + if len(str(serial)) == 6: + scanned_serial = "00" + str(serial) + if len(str(serial)) == 7: + scanned_serial = "0" + str(serial) + else: + pass + return scanned_serial + diff --git a/apps/tag/web/api/v1/api.py b/apps/tag/web/api/v1/api.py index d9aaa82..ddf8e9a 100644 --- a/apps/tag/web/api/v1/api.py +++ b/apps/tag/web/api/v1/api.py @@ -2,11 +2,22 @@ from rest_framework import viewsets from apps.tag import models as tag_models from rest_framework import status from rest_framework.response import Response -from .serializers import TagSerializer +from .serializers import ( + TagSerializer, + TagAssignmentSerializer, + AllocatedTagsSerializer +) from rest_framework.decorators import action from django.db import transaction from rest_framework.exceptions import APIException from apps.tag import permissions as tag_permissions +from apps.authorization import models as authorize_models +from apps.tag.tools import tag_code_serial_scanning +from apps.tag import exceptions as tag_exceptions +from common.helpers import detect_file_extension +from common.liara_tools import upload_to_liara +from django.db import IntegrityError +import typing def trash(queryset, pk): @@ -23,9 +34,47 @@ def delete(queryset, pk): class TagViewSet(viewsets.ModelViewSet): + """ Tag View Set """ queryset = tag_models.Tag.objects.all() serializer_class = TagSerializer + @transaction.atomic + def create(self, request: object, *args: list, **kwargs: dict) -> typing.Any: + """ Create tag for livestocks """ # noqa + tag_objects = [] + serial_start_range, serial_end_range = request.data['serial_range'] # serial_range is like [500, 550] + while serial_start_range <= serial_end_range: + try: + request.data.update({'serial': str(serial_start_range)}) + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + tag_objects.append(serializer.save()) + except IntegrityError as e: # if tag exists before + if 'unique constraint' in e.args[0]: + return Response("tag exists", status.HTTP_406_NOT_ACCEPTABLE) + serial_start_range += 1 + + serializer = self.serializer_class(tag_objects, many=True) + return Response(serializer.data, status.HTTP_201_CREATED) + + @action( + methods=['get'], + detail=False, + url_path='ownership_code', + url_name='ownership_code', + name='ownership_code' + ) + @transaction.atomic + def ownership_code(self, request) -> typing.Any: + """ just show ownership code of organization """ + try: + ownership_code = authorize_models.UserRelations.objects.select_related('organization').get( + user=request.user + ) + return Response(ownership_code.organization.additional_data, status.HTTP_200_OK) + except APIException as e: + return Response(e, status.HTTP_204_NO_CONTENT) + @action( methods=['put'], detail=True, @@ -55,4 +104,157 @@ class TagViewSet(viewsets.ModelViewSet): delete(self.queryset, pk) return Response(status=status.HTTP_200_OK) except APIException as e: - return Response(e, status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(e, status=status.HTTP_204_NO_CONTENT) + + +class TagAssignmentViewSet(viewsets.ModelViewSet): + """ assignment of tags """ + queryset = tag_models.TagAssignment.objects.all() + serializer_class = TagAssignmentSerializer + + @transaction.atomic + def create(self, request: object, *args: list, **kwargs: dict) -> typing.Any: + """ assign tags to organizations """ + response = {} + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + tag_assignment = serializer.save() + + # get tags by species number like: 2 tags of species code 4 + tags_to_allocate = request.data['allocated_tags'] + for tag in tags_to_allocate: + tags = tag_models.Tag.objects.filter( + species_code=tag['species_code'], + status='F' + ).order_by('id')[:tag['number']] + + # check species number + if tags.count() < tag['number']: + raise tag_exceptions.SpeciesNumberCheckException() + + # create assignment + for tag_to_allocate in tags: + tag_models.AllocatedTags( + tag=tag_to_allocate, + assignment=tag_assignment, + status='W', + species_code=tag['species_code'] + ).save() # noqa + tag_to_allocate.status = 'W' # change tag status from free to waiting + tag_to_allocate.save() + + return Response(serializer.data, status.HTTP_201_CREATED) + + @action( + methods=['post', ], + detail=True, + url_name='assign_document', + url_path='assign_document', + name='assign_document' + ) + @transaction.atomic + def assign_document(self, request, pk=None): + """ set document for tag assignment """ + + # get tag assignment object & set document url + assignment = self.queryset.get(id=pk) + + # upload document file to liara storage + document = request.FILES.get('document') + document_url = upload_to_liara( + document, + f'tag_assignment_document_{assignment.serial}.{str(document).split(".")[1]}' + ) + assignment.document = document_url + assignment.status = 'A' + assignment.save() + serializer = self.serializer_class(assignment) + return Response(serializer.data, status=status.HTTP_200_OK) + + @action( + methods=['post'], + detail=True, + url_path='remove_tags_by_group', + url_name='remove_tags_by_group', + name='remove_tags_by_group' + ) + def remove_assigned_tags_by_group(self, request, pk=None): + """ remove assigned tags """ + + tag_assignment = self.queryset.get(id=pk) + for species in request.data['species_group']: + allocated_tags = tag_assignment.assigned_allocated_tags.filter(species_code=species) + for allocate in allocated_tags: + # change status of tag from allocated to free + allocate.tag.status = 'F' + allocate.tag.save() + allocated_tags.delete() + + return Response(status.HTTP_200_OK) + + @action( + methods=['put'], + detail=True, + url_path='trash', + url_name='trash', + name='trash', + ) + @transaction.atomic + def trash(self, request, pk=None): + """ Sent TagAssigment to trash """ + try: + trash(self.queryset, pk) + except APIException as e: + return Response(e, status.HTTP_204_NO_CONTENT) + + @action( + methods=['post'], + detail=True, + url_name='delete', + url_path='delete', + name='delete' + ) + @transaction.atomic + def delete(self, request, pk=None): + """ Full delete of TagAssignment object """ + try: + delete(self.queryset, pk) + return Response(status=status.HTTP_200_OK) + except APIException as e: + return Response(e, status=status.HTTP_204_NO_CONTENT) + + +class AllocatedTagsViewSet(viewsets.ModelViewSet): + queryset = tag_models.AllocatedTags.objects.all() + serializer_class = AllocatedTagsSerializer + + @action( + methods=['put'], + detail=True, + url_path='trash', + url_name='trash', + name='trash', + ) + @transaction.atomic + def trash(self, request, pk=None): + """ Sent AllocatedTag to trash """ + try: + trash(self.queryset, pk) + except APIException as e: + return Response(e, status.HTTP_204_NO_CONTENT) + + @action( + methods=['post'], + detail=True, + url_name='delete', + url_path='delete', + name='delete' + ) + @transaction.atomic + def delete(self, request, pk=None): + """ Full delete of AllocatedTag object """ + try: + delete(self.queryset, pk) + return Response(status=status.HTTP_200_OK) + except APIException as e: + return Response(e, status=status.HTTP_204_NO_CONTENT) diff --git a/apps/tag/web/api/v1/serializers.py b/apps/tag/web/api/v1/serializers.py index 51ed6a7..8695521 100644 --- a/apps/tag/web/api/v1/serializers.py +++ b/apps/tag/web/api/v1/serializers.py @@ -1,27 +1,104 @@ +from apps.authentication.api.v1.serializers import serializer as auth_serializers from rest_framework import serializers from apps.tag import models as tag_models -from apps.authentication.api.v1.serializers import serializer as auth_serializers class TagSerializer(serializers.ModelSerializer): """ Tag Model Serializer """ + class Meta: model = tag_models.Tag fields = [ 'id', - 'code', - 'province', - 'city', + 'country_code', + 'static_code', + 'ownership_code', + 'species_code', + 'serial', + 'tag_code', 'organization', 'status', ] + def update(self, instance, validated_data): + """ update tag information """ + + instance.country_code = validated_data.get( + 'country_code', instance.country_code + ) + instance.static_code = validated_data.get( + 'static_code', instance.static_code + ) + instance.ownership_code = validated_data.get( + 'ownership_code', instance.ownership_code + ) + instance.species_code = validated_data.get( + 'species_code', instance.species_code + ) + instance.serial = validated_data.get( + 'serial', instance.serial + ) + instance.tag_code = validated_data.get( + 'tag_code', instance.tag_code + ) + if validated_data.get('organization'): + instance.organization = tag_models.auth_models.Organization.objects.get( + id=validated_data.get('organization', instance.organization).id + ) + instance.status = validated_data.get('status', instance.status) + instance.save() + return instance + def to_representation(self, instance): """ Customize output of serializer """ representation = super().to_representation(instance) if isinstance(instance, tag_models.Tag): - representation['province'] = auth_serializers.ProvinceSerializer(instance.province).data - representation['city'] = auth_serializers.CitySerializer(instance.city).data representation['organization'] = auth_serializers.OrganizationSerializer(instance.organization).data + return representation + + +class TagAssignmentSerializer(serializers.ModelSerializer): + """ assigned tag serializer """ + + class Meta: + model = tag_models.TagAssignment + fields = [ + 'id', + 'organization', + 'document', + 'serial', + ] + + def to_representation(self, instance): + """ custom output for serializer """ + representation = super().to_representation(instance) + representation['serial'] = instance.serial + representation['document'] = instance.document + representation['organization'] = auth_serializers.OrganizationSerializer( + instance.organization + ).data + representation['allocated_tags'] = AllocatedTagsSerializer( + instance.assigned_allocated_tags.all(), many=True + ).data + return representation + + +class AllocatedTagsSerializer(serializers.ModelSerializer): + """ allocated tags serializer """ + + class Meta: + model = tag_models.AllocatedTags + fields = [ + 'id', + 'tag', + 'assignment', + 'status' + ] + + def to_representation(self, instance): + """ custom output for serializer """ + representation = super().to_representation(instance) + representation['tag'] = TagSerializer(instance.tag).data + representation['status'] = instance.status return representation diff --git a/apps/tag/web/api/v1/urls.py b/apps/tag/web/api/v1/urls.py index 1702391..4269124 100644 --- a/apps/tag/web/api/v1/urls.py +++ b/apps/tag/web/api/v1/urls.py @@ -1,9 +1,15 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .api import TagViewSet +from .api import ( + TagViewSet, + TagAssignmentViewSet, + AllocatedTagsViewSet +) router = DefaultRouter() router.register(r'tag', TagViewSet, basename='tag') +router.register(r'tag_assignment', TagAssignmentViewSet, basename='tag_assignment') +router.register(r'allocated_tag', AllocatedTagsViewSet, basename='allocated_tag') urlpatterns = [ path('v1/', include(router.urls)) diff --git a/common/helpers.py b/common/helpers.py index e69de29..a3ddb58 100644 --- a/common/helpers.py +++ b/common/helpers.py @@ -0,0 +1,7 @@ +import typing + + +def detect_file_extension(file_name: str) -> typing.AnyStr: + """ detect extension of a file like: jpg, png, pdf """ + extended = file_name.split('.') + return extended[1] diff --git a/common/liara_tools.py b/common/liara_tools.py new file mode 100644 index 0000000..6092848 --- /dev/null +++ b/common/liara_tools.py @@ -0,0 +1,99 @@ +from botocore.exceptions import NoCredentialsError +import boto3 +import logging +from PIL import Image +import io +import base64 + +LIARA_ENDPOINT = 'https://storage.c2.liara.space' +LIARA_BUCKET_NAME = 'ticket-rasadyar' +LIARA_ACCESS_KEY = "gvqohestrakmqi6n" +LIARA_SECRET_KEY = '7240fdd8-59bc-4f02-b5e6-4a124e37fa0e' + + +def upload_to_liara(file_obj, file_name): + try: + s3 = boto3.client( + 's3', + endpoint_url=LIARA_ENDPOINT, + aws_access_key_id=LIARA_ACCESS_KEY, + aws_secret_access_key=LIARA_SECRET_KEY + ) + + s3.upload_fileobj( + file_obj, + LIARA_BUCKET_NAME, + file_name, + ExtraArgs={'ACL': 'public-read'} # دسترسی عمومی + ) + + return f"{LIARA_ENDPOINT}/{LIARA_BUCKET_NAME}/{file_name}" + + except NoCredentialsError: + raise Exception("اعتبارنامه‌های AWS معتبر نیستند") + except Exception as e: + raise Exception(f"خطا در آپلود فایل: {e}") + + +def connect(): + logging.basicConfig(level=logging.INFO) + + try: + s3 = boto3.client( + 's3', + endpoint_url=LIARA_ENDPOINT, + aws_access_key_id=LIARA_ACCESS_KEY, + aws_secret_access_key=LIARA_SECRET_KEY + ) + except Exception as exc: + logging.info(exc) + return s3 + + +def upload_object_resize_to_liara(image_data, object_name): + try: + imgdata = base64.b64decode(image_data) + img = Image.open(io.BytesIO(imgdata)) + + img.thumbnail((500, 500)) + + buffer = io.BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + + s3_resource = boto3.resource( + 's3', + endpoint_url=LIARA_ENDPOINT, + aws_access_key_id=LIARA_ACCESS_KEY, + aws_secret_access_key=LIARA_SECRET_KEY + ) + + bucket = s3_resource.Bucket(LIARA_BUCKET_NAME) + bucket.put_object( + ACL='public-read', + Body=buffer, + Key=object_name, + ContentType='image/png' + ) + + return f"{LIARA_ENDPOINT}/{LIARA_BUCKET_NAME}/{object_name}" + + except Exception as e: + raise Exception(f"خطا در آپلود فایل: {e}") + + +def delete_file_from_liara(file_name): + try: + s3 = boto3.client( + 's3', + endpoint_url=LIARA_ENDPOINT, + aws_access_key_id=LIARA_ACCESS_KEY, + aws_secret_access_key=LIARA_SECRET_KEY + ) + + s3.delete_object(Bucket=LIARA_BUCKET_NAME, Key=file_name) + + except NoCredentialsError: + raise Exception("اعتبارنامه‌های AWS معتبر نیستند") + except Exception as e: + raise Exception(f"خطا در آپلود فایل: {e}")