From 18fd27a9c4a5f16cb178da5bcb8544e9404c4858 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Thu, 24 Jul 2025 16:02:08 +0330 Subject: [PATCH] add pos device - fix closed quotas pagination --- Rasaddam_Backend/settings.py | 143 ++++++++++++++++++ Rasaddam_Backend/urls.py | 1 + .../0007_providercompany_user_relation.py | 20 +++ apps/pos_device/models.py | 11 +- apps/pos_device/web/api/v1/urls.py | 1 + apps/pos_device/web/api/v1/viewsets/device.py | 68 ++++++++- apps/product/admin.py | 3 + apps/product/web/api/v1/viewsets/quota_api.py | 27 +--- requirements.txt | 3 +- 9 files changed, 246 insertions(+), 31 deletions(-) create mode 100644 apps/pos_device/migrations/0007_providercompany_user_relation.py diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index c111481..55ca260 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -62,6 +62,7 @@ ALLOWED_HOSTS = [ # Application definition INSTALLED_APPS = [ + 'jazzmin', # noqa 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -91,6 +92,7 @@ INSTALLED_APPS = [ 'drf_yasg', "django_celery_results", "django_celery_beat", + ] MIDDLEWARE = [ @@ -349,3 +351,144 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False + +JAZZMIN_SETTINGS = { + # title of the window (Will default to current_admin_site.site_title if absent or None) + "site_title": "Library Admin", + + # Title on the login screen (19 chars max) (defaults to current_admin_site.site_header if absent or None) + "site_header": "Library", + + # Title on the brand (19 chars max) (defaults to current_admin_site.site_header if absent or None) + "site_brand": "Library", + + # Logo to use for your site, must be present in static files, used for brand on top left + "site_logo": "books/img/logo.png", + + # Logo to use for your site, must be present in static files, used for login form logo (defaults to site_logo) + "login_logo": None, + + # Logo to use for login form in dark themes (defaults to login_logo) + "login_logo_dark": None, + + # CSS classes that are applied to the logo above + "site_logo_classes": "img-circle", + + # Relative path to a favicon for your site, will default to site_logo if absent (ideally 32x32 px) + "site_icon": None, + + # Welcome text on the login screen + "welcome_sign": "Welcome to the library", + + # Copyright on the footer + "copyright": "Acme Library Ltd", + + # List of model admins to search from the search bar, search bar omitted if excluded + # If you want to use a single search field you dont need to use a list, you can use a simple string + "search_model": ["auth.User", "auth.Group"], + + # Field name on user model that contains avatar ImageField/URLField/Charfield or a callable that receives the user + "user_avatar": None, + + ############ + # Top Menu # + ############ + + # Links to put along the top menu + "topmenu_links": [ + + # Url that gets reversed (Permissions can be added) + {"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]}, + + # external url that opens in a new window (Permissions can be added) + {"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True}, + + # model admin to link to (Permissions checked against model) + {"model": "auth.User"}, + + # App with dropdown menu to all its models pages (Permissions checked against models) + {"app": "books"}, + ], + + ############# + # User Menu # + ############# + + # Additional links to include in the user menu on the top right ("app" url type is not allowed) + "usermenu_links": [ + {"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True}, + {"model": "auth.user"} + ], + + ############# + # Side Menu # + ############# + + # Whether to display the side menu + "show_sidebar": True, + + # Whether to aut expand the menu + "navigation_expanded": True, + + # Hide these apps when generating side menu e.g (auth) + "hide_apps": [], + + # Hide these models when generating side menu (e.g auth.user) + "hide_models": [], + + # List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models) + "order_with_respect_to": ["auth", "books", "books.author", "books.book"], + + # Custom links to append to app groups, keyed on app name + "custom_links": { + "books": [{ + "name": "Make Messages", + "url": "make_messages", + "icon": "fas fa-comments", + "permissions": ["books.view_book"] + }] + }, + + # Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.0.2,5.0.3,5.0.4,5.0.5,5.0.6,5.0.7,5.0.8,5.0.9,5.1.0,5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,5.11.2,5.11.1,5.10.0,5.9.0,5.8.2,5.8.1,5.7.2,5.7.1,5.7.0,5.6.3,5.5.0,5.4.2 + # for the full list of 5.13.0 free icon classes + "icons": { + "auth": "fas fa-users-cog", + "auth.user": "fas fa-user", + "auth.Group": "fas fa-users", + }, + # Icons that are used when one is not manually specified + "default_icon_parents": "fas fa-chevron-circle-right", + "default_icon_children": "fas fa-circle", + + ################# + # Related Modal # + ################# + # Use modals instead of popups + "related_modal_active": False, + + ############# + # UI Tweaks # + ############# + # Relative paths to custom CSS/JS scripts (must be present in static files) + "custom_css": None, + "custom_js": None, + # Whether to link font from fonts.googleapis.com (use custom_css to supply font otherwise) + "use_google_fonts_cdn": True, + # Whether to show the UI customizer on the sidebar + "show_ui_builder": False, + + ############### + # Change view # + ############### + # Render out the change view as a single form, or in tabs, current options are + # - single + # - horizontal_tabs (default) + # - vertical_tabs + # - collapsible + # - carousel + "changeform_format": "horizontal_tabs", + # override change forms on a per modeladmin basis + "changeform_format_overrides": {"auth.user": "collapsible", "auth.group": "vertical_tabs"}, + # Add a language dropdown into the admin + # "language_chooser": True, +} diff --git a/Rasaddam_Backend/urls.py b/Rasaddam_Backend/urls.py index f91782a..815ad50 100644 --- a/Rasaddam_Backend/urls.py +++ b/Rasaddam_Backend/urls.py @@ -24,6 +24,7 @@ from django.conf.urls import ( handler404, ) + # handler500 = 'apps.core.error_handler.handler500' # noqa urlpatterns = [ diff --git a/apps/pos_device/migrations/0007_providercompany_user_relation.py b/apps/pos_device/migrations/0007_providercompany_user_relation.py new file mode 100644 index 0000000..a952e0e --- /dev/null +++ b/apps/pos_device/migrations/0007_providercompany_user_relation.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0 on 2025-07-24 09:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authorization', '0019_page_is_active_permissions_is_active'), + ('pos_device', '0006_alter_posclientattribute_choices'), + ] + + operations = [ + migrations.AddField( + model_name='providercompany', + name='user_relation', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pos_provider', to='authorization.userrelations'), + ), + ] diff --git a/apps/pos_device/models.py b/apps/pos_device/models.py index 607ae26..8009586 100644 --- a/apps/pos_device/models.py +++ b/apps/pos_device/models.py @@ -1,10 +1,17 @@ -from django.db import models -from apps.core.models import BaseModel from apps.authentication.models import Organization from django.contrib.postgres.fields import ArrayField +from apps.authorization.models import UserRelations +from apps.core.models import BaseModel +from django.db import models class ProviderCompany(BaseModel): + user_relation = models.ForeignKey( + UserRelations, + related_name='pos_provider', + on_delete=models.CASCADE, + null=True + ) name_fa = models.CharField(max_length=250, null=True) name_en = models.CharField(max_length=250, null=True) activation = models.BooleanField(default=False) diff --git a/apps/pos_device/web/api/v1/urls.py b/apps/pos_device/web/api/v1/urls.py index 2dea624..5595926 100644 --- a/apps/pos_device/web/api/v1/urls.py +++ b/apps/pos_device/web/api/v1/urls.py @@ -7,6 +7,7 @@ router = DefaultRouter() router.register(r'client', client_views.POSClientViewSet, basename='client') router.register(r'attributes', client_views.POSClientAttributeViewSet, basename='attributes') router.register(r'provider', device_views.ProviderCompanyViewSet, basename='provider') +router.register(r'device', device_views.DeviceViewSet, basename='device') urlpatterns = [ path('v1/pos/', include(router.urls)) diff --git a/apps/pos_device/web/api/v1/viewsets/device.py b/apps/pos_device/web/api/v1/viewsets/device.py index e5dee6e..5cb6287 100644 --- a/apps/pos_device/web/api/v1/viewsets/device.py +++ b/apps/pos_device/web/api/v1/viewsets/device.py @@ -1,12 +1,16 @@ from apps.pos_device.web.api.v1.serilaizers import serializers as pos_serializer from apps.authentication.api.v1.api import UserViewSet +from apps.authorization.models import UserRelations from rest_framework.exceptions import APIException from apps.pos_device import models as pos_models from rest_framework.response import Response +from rest_framework.decorators import action from common.tools import CustomOperations from rest_framework import viewsets +from django.db import transaction from rest_framework import status + class ProviderCompanyViewSet(viewsets.ModelViewSet): # noqa queryset = pos_models.ProviderCompany.objects.all() serializer_class = pos_serializer.ProviderCompanySerializer @@ -15,21 +19,69 @@ class ProviderCompanyViewSet(viewsets.ModelViewSet): # noqa """ custom create of provider client """ try: - client = CustomOperations().custom_create( - request=request, - view=UserViewSet(), - data=request.data - ) - except Exception as e: - raise APIException(detail="data is invalid", code=403) + # creating user & relations + client = UserViewSet().create(request=request) - return Response(client, status=status.HTTP_201_CREATED) + if client.status_code == 201: + + # create provider + serializer = self.serializer_class(data=request.data['provider']) + if serializer.is_valid(): + provider = serializer.save() + provider.user_relation = UserRelations.objects.get( + user_id=client.data['id'] + ) + provider.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) + else: + return client + except Exception as e: + raise e class DeviceViewSet(viewsets.ModelViewSet): queryset = pos_models.Device.objects.all() serializer_class = pos_serializer.DeviceSerializer + def create(self, request, *args, **kwargs): + """ Custom create of pos devices """ + + company = pos_models.ProviderCompany.objects.get( + user_relation__user=request.user + ) + + # create device + request.data.update({'company': company.id}) + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + serializer.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) + + @action( + methods=['get'], + detail=False, + url_name='my_devices', + url_path='my_devices', + name='my_devices' + ) + @transaction.atomic + def my_devices(self, request): + """ list of company devices """ + + devices = self.queryset.filter( + company__user_relation__user=request.user + ) + + # paginate devices + page = self.paginate_queryset(devices) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + class DeviceVersionViewSet(viewsets.ModelViewSet): queryset = pos_models.DeviceVersion.objects.all() diff --git a/apps/product/admin.py b/apps/product/admin.py index 8c38f3f..d5f5706 100644 --- a/apps/product/admin.py +++ b/apps/product/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from .models import Quota # Register your models here. + +admin.site.register(Quota) \ No newline at end of file diff --git a/apps/product/web/api/v1/viewsets/quota_api.py b/apps/product/web/api/v1/viewsets/quota_api.py index 39a8635..8905142 100644 --- a/apps/product/web/api/v1/viewsets/quota_api.py +++ b/apps/product/web/api/v1/viewsets/quota_api.py @@ -268,6 +268,12 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa return Response(status.HTTP_200_OK) + @action( + methods=['patch'], + ) + def activate(self, request): + pass + @action( methods=['get'], detail=False, @@ -305,6 +311,7 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa """ list of organization closed quotas """ organization = get_organization_by_user(request.user) + # paginate queryset page = self.paginate_queryset( self.queryset.filter( @@ -412,26 +419,6 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa serializer = self.serializer_class(quotas, many=True).data return Response(serializer, status=status.HTTP_200_OK) - @action( - methods=['get'], - detail=False, - url_path='closed_quotas', - url_name='closed_quotas', - name='closed_quotas' - ) - def closed_quotas(self, request): - """ get list of close quotas for organization """ - - quotas = quotas = self.queryset.filter( - (Q(assigned_organizations=get_organization_by_user(request.user)) | - Q(registerer_organization=get_organization_by_user(request.user))) & - Q(is_closed=True) - ) - - serialize = self.serializer_class(quotas, many=True).data - - return Response(serialize, status=status.HTTP_200_OK) - @action( methods=['put'], detail=True, diff --git a/requirements.txt b/requirements.txt index 059161d..cc9894f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -81,4 +81,5 @@ django-celery-beat django-celery-results channels channels_redis -daphne \ No newline at end of file +daphne +django-jazzmin \ No newline at end of file