Skip to content

Commit 90a04ea

Browse files
Merge pull request #378 from MySecondLanguage/user-query-added
User query added
2 parents 0f12997 + 2090614 commit 90a04ea

14 files changed

+295
-40
lines changed

nxtbn/core/admin_permissions.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
"""
2+
Dashboard User Permission Hierarchy:
3+
4+
- `is_superuser`:
5+
Grants unrestricted access to all dashboard functionalities without any limitations.
6+
Inherits all permissions of `is_store_admin` by default.
7+
8+
- `is_staff`:
9+
Required for all users to access the dashboard.
10+
11+
- `is_store_admin`:
12+
Has extensive permissions and authority within the dashboard.
13+
Can perform almost all operations except a few critical ones restricted to the superuser.
14+
Inherits all permissions assigned to other users by default.
15+
16+
- `is_store_staff`:
17+
Has limited access to the dashboard.
18+
Their permissions can be extended granularly by assigning specific permissions, which are managed by the store admin.
19+
20+
**Additional Notes:**
21+
- All users have read permissions by default, except for certain critical data that require explicit authorization.
22+
- The superuser automatically inherits all permissions of a store admin.
23+
- A store admin inherits all permissions granted to other users by default.
24+
"""
25+
26+
27+
128
from rest_framework.permissions import BasePermission
229
from rest_framework.permissions import SAFE_METHODS
330

@@ -7,22 +34,50 @@
734
import functools
835
from graphql import GraphQLError
936

37+
38+
class IsStoreAdmin(BasePermission):
39+
def has_permission(self, request, view):
40+
if not request.user.is_staff:
41+
return False
42+
return request.user.is_store_admin
43+
44+
class IsStoreStaff(BasePermission):
45+
def has_permission(self, request, view):
46+
if not request.user.is_staff:
47+
return False
48+
49+
if request.user.is_superuser:
50+
return True
51+
52+
if request.user.is_store_admin:
53+
return True
54+
55+
return request.user.is_store_staff
56+
1057
class GranularPermission(BasePermission):
1158
def get_permission_name(self, model_name, action):
1259

1360
return f"{model_name}.{action}"
1461

1562
def has_permission(self, request, view):
16-
if not request.user.is_authenticated:
63+
if not request.user.is_staff:
1764
return False
1865

1966
if request.user.is_superuser:
2067
return True
2168

69+
if request.user.is_store_admin:
70+
return True
71+
2272
if request.method in SAFE_METHODS and request.user.is_staff: # Every staff can view
2373
return True
2474

25-
model_cls = getattr(view, 'queryset', None) or getattr(view, 'model', None)
75+
model_cls = None
76+
if hasattr(view, 'get_queryset'): # Warning, Never use hasattr(view, 'queryset') as DRF cache this which may lead to unexpected behavior
77+
model_cls = view.get_queryset().model
78+
elif hasattr(view, 'model'):
79+
model_cls = view.model
80+
2681
if model_cls is None:
2782
return False
2883

@@ -38,18 +93,26 @@ def has_permission(self, request, view):
3893
class CommonPermissions(BasePermission):
3994

4095
def has_permission(self, request, view):
41-
if not request.user.is_authenticated:
96+
if not request.user.is_staff:
4297
return False
4398

4499
if request.user.is_superuser:
45100
return True
46101

102+
if request.user.is_store_admin:
103+
return True
104+
47105
if request.method in SAFE_METHODS and request.user.is_staff: # Every staff can view
48106
return True
49107

50108

51109

52-
model_cls = getattr(view, 'queryset', None) or getattr(view, 'model', None)
110+
model_cls = None
111+
if hasattr(view, 'get_queryset'): # Warning, Never use hasattr(view, 'queryset') as DRF cache this which may lead to unexpected behavior
112+
model_cls = view.get_queryset().model
113+
elif hasattr(view, 'model'):
114+
model_cls = view.model
115+
53116
if model_cls is None:
54117
return False
55118

@@ -73,6 +136,18 @@ def has_permission(self, request, view):
73136
return request.user.has_perm(required_permission)
74137

75138

139+
def has_required_perm(user, code: str, model_cls=None):
140+
if not user.is_staff:
141+
return False
142+
143+
if user.is_superuser:
144+
return True
145+
146+
if user.is_store_admin:
147+
return True
148+
149+
perm_code = model_cls._meta.app_label + '.' + code
150+
return user.has_perm(perm_code)
76151

77152

78153
def gql_required_perm(code: str): # Used in graphql only
@@ -100,6 +175,7 @@ def wrapper(self, info, *args, **kwargs):
100175
return decorator
101176

102177

178+
103179
def gql_staff_required(func): # Used in graphql only
104180
@functools.wraps(func)
105181
def wrapper(self, info, *args, **kwargs):

nxtbn/core/enum_perms.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
class PermissionsEnum(models.TextChoices):
55
CAN_APPROVE_ORDER = "can_approve_order"
66
CAN_CANCEL_ORDER = "can_cancel_order"
7-
CAN_SHIP_ORDER = "can_ship_order"
7+
CAN_SHIP_ORDER = "can_ship_order"
8+
CAN_PROCCSS_ORDER = "can_process_order"
9+
CAN_DELIVER_ORDER = "can_deliver_order"
10+
CAN_UPDATE_ORDER_PYMENT_TERM = "can_update_order_payment_term"
11+
CAN_UPDATE_ORDER_PAYMENT_METHOD = "can_update_order_payment_method"

nxtbn/order/api/dashboard/views.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import django_filters
2222
from django_filters import rest_framework as filters
2323

24-
from nxtbn.core.admin_permissions import GranularPermission, CommonPermissions
24+
from nxtbn.core.admin_permissions import GranularPermission, CommonPermissions, has_required_perm
2525
from nxtbn.core.enum_perms import PermissionsEnum
2626
from nxtbn.core.utils import to_currency_unit
2727
from nxtbn.order.proccesor.views import OrderProccessorAPIView
@@ -419,11 +419,41 @@ class OrderStatusUpdateAPIView(generics.UpdateAPIView):
419419
serializer_class = OrderStatusUpdateSerializer
420420
lookup_field = 'alias'
421421

422+
def check_permissions(self, request):
423+
status = request.data.get('status')
424+
user = request.user
425+
426+
print(status, 'status')
427+
428+
permission_map = {
429+
OrderStatus.CANCELLED: PermissionsEnum.CAN_CANCEL_ORDER,
430+
OrderStatus.SHIPPED: PermissionsEnum.CAN_SHIP_ORDER,
431+
OrderStatus.DELIVERED: PermissionsEnum.CAN_DELIVER_ORDER,
432+
OrderStatus.APPROVED: PermissionsEnum.CAN_APPROVE_ORDER,
433+
OrderStatus.PROCESSING: PermissionsEnum.CAN_PROCCSS_ORDER,
434+
}
435+
436+
required_permission = permission_map.get(status)
437+
print(required_permission, 'required_permission')
438+
if required_permission and not has_required_perm(user, required_permission, Order):
439+
self.permission_denied(
440+
request,
441+
message=_("You do not have permission to perform this action."),
442+
code='permission_denied'
443+
)
444+
422445
class OrderPaymentTermUpdateAPIView(generics.UpdateAPIView):
446+
model = Order
447+
permission_classes = (GranularPermission, )
423448
queryset = Order.objects.all()
424449
serializer_class = OrderPaymentUpdateSerializer
425450
lookup_field = 'alias'
451+
required_perm = PermissionsEnum.CAN_UPDATE_ORDER_PYMENT_TERM
452+
426453
class OrderPaymentMethodUpdateAPIView(generics.UpdateAPIView):
454+
model = Order
455+
permission_classes = (GranularPermission, )
456+
required_perm = PermissionsEnum.CAN_UPDATE_ORDER_PAYMENT_METHOD
427457
queryset = Order.objects.all()
428458
serializer_class = OrderPaymentMethodSerializer
429459
lookup_field = 'alias'
@@ -461,43 +491,29 @@ class ReturnRequestFilterMixing:
461491

462492

463493
class ReturnRequestAPIView(ReturnRequestFilterMixing, generics.ListCreateAPIView):
494+
permission_classes = (CommonPermissions, )
495+
model = ReturnRequest
464496
queryset = ReturnRequest.objects.all()
465497
serializer_class = ReturnRequestSerializer
466-
467-
HTTP_PERMISSIONS = {
468-
UserRole.STORE_MANAGER: {"POST", 'GET'},
469-
UserRole.ADMIN: {"all"},
470-
UserRole.ORDER_PROCESSOR: {"POST", 'GET'},
471-
UserRole.STORE_VIEWER: {"GET"},
472-
}
473-
474498

475499
class ReturnRequestDetailAPIView(generics.RetrieveUpdateAPIView):
500+
permission_classes = (CommonPermissions, )
501+
model = ReturnRequest
476502
queryset = ReturnRequest.objects.all()
477503
serializer_class = ReturnRequestDetailsSerializer
478504
lookup_field = 'id'
479505

480-
HTTP_PERMISSIONS = {
481-
UserRole.STORE_MANAGER: {"PUT", 'PATCH', 'GET'},
482-
UserRole.ADMIN: {"all"},
483-
UserRole.ORDER_PROCESSOR: {"PATCH", 'GET'},
484-
UserRole.STORE_VIEWER: {"GET"},
485-
}
486-
487506
def get_serializer_class(self):
488507
if self.request.method in ['PATCH', 'PUT']:
489508
return ReturnRequestStatusUpdateSerializer
490509
return self.serializer_class
491510

492511
class ReturnLineItemStatusUpdateAPIView(generics.UpdateAPIView):
512+
permission_classes = (CommonPermissions, )
513+
model = ReturnRequest
514+
493515
serializer_class = ReturnLineItemStatusUpdateSerializer
494516

495-
HTTP_PERMISSIONS = {
496-
UserRole.STORE_MANAGER: {"PUT", 'PATCH', 'GET'},
497-
UserRole.ADMIN: {"all"},
498-
UserRole.ORDER_PROCESSOR: {"PATCH", 'GET'},
499-
UserRole.STORE_VIEWER: {"GET"},
500-
}
501517

502518
def update(self, request, *args, **kwargs):
503519
serializer = self.get_serializer(data=request.data)
@@ -526,14 +542,10 @@ def update(self, request, *args, **kwargs):
526542

527543

528544
class ReturnRequestBulkUpdateAPIView(generics.UpdateAPIView):
529-
serializer_class = ReturnRequestBulkUpdateSerializer
545+
permission_classes = (CommonPermissions, )
546+
model = ReturnRequest
530547

531-
HTTP_PERMISSIONS = {
532-
UserRole.STORE_MANAGER: {'all'},
533-
UserRole.ADMIN: {"all"},
534-
UserRole.ORDER_PROCESSOR: {'all'},
535-
UserRole.STORE_VIEWER: {"GET"},
536-
}
548+
serializer_class = ReturnRequestBulkUpdateSerializer
537549

538550
def update(self, request, *args, **kwargs):
539551
serializer = self.get_serializer(data=request.data)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.11 on 2025-01-31 13:04
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('order', '0032_alter_order_options'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='order',
15+
options={'ordering': ('-created_at',), 'permissions': [('can_approve_order', 'Can approve order'), ('can_cancel_order', 'Can cancel order'), ('can_ship_order', 'Can ship order'), ('can_process_order', 'Can process order'), ('can_deliver_order', 'Can deliver order')]},
16+
),
17+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.11 on 2025-01-31 14:20
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('order', '0033_alter_order_options'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='order',
15+
options={'ordering': ('-created_at',), 'permissions': [('can_approve_order', 'Can approve order'), ('can_cancel_order', 'Can cancel order'), ('can_ship_order', 'Can ship order'), ('can_process_order', 'Can process order'), ('can_deliver_order', 'Can deliver order'), ('can_update_order_payment_term', 'Can update order payment term')]},
16+
),
17+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.11 on 2025-01-31 15:16
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('order', '0034_alter_order_options'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='order',
15+
options={'ordering': ('-created_at',), 'permissions': [('can_approve_order', 'Can approve order'), ('can_cancel_order', 'Can cancel order'), ('can_ship_order', 'Can ship order'), ('can_process_order', 'Can process order'), ('can_deliver_order', 'Can deliver order'), ('can_update_order_payment_term', 'Can update order payment term'), ('can_update_order_payment_method', 'Can update order payment method')]},
16+
),
17+
]

nxtbn/order/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ class Meta:
254254
(PermissionsEnum.CAN_APPROVE_ORDER, 'Can approve order'),
255255
(PermissionsEnum.CAN_CANCEL_ORDER, 'Can cancel order'),
256256
(PermissionsEnum.CAN_SHIP_ORDER, 'Can ship order'),
257+
(PermissionsEnum.CAN_PROCCSS_ORDER, 'Can process order'),
258+
(PermissionsEnum.CAN_DELIVER_ORDER, 'Can deliver order'),
259+
(PermissionsEnum.CAN_UPDATE_ORDER_PYMENT_TERM, 'Can update order payment term'),
260+
(PermissionsEnum.CAN_UPDATE_ORDER_PAYMENT_METHOD, 'Can update order payment method'),
257261
]
258262

259263
def save(self, *args, **kwargs):

nxtbn/tax/api/dashboard/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from rest_framework.permissions import AllowAny
55
from rest_framework.exceptions import APIException
66

7+
from nxtbn.core.admin_permissions import CommonPermissions
78
from nxtbn.core.paginator import NxtbnPagination
89

910
from nxtbn.tax.models import TaxClass, TaxClassTranslation, TaxRate
@@ -20,17 +21,23 @@
2021
from nxtbn.users import UserRole
2122

2223
class TaxClassView(generics.ListCreateAPIView):
24+
permission_classes = (CommonPermissions, )
25+
model = TaxClass
2326
queryset = TaxClass.objects.all()
2427
serializer_class = TaxClassSerializer
2528
pagination_class = NxtbnPagination
2629

2730

2831
class TaxClassDetailsView(generics.RetrieveUpdateDestroyAPIView):
32+
permission_classes = (CommonPermissions, )
33+
model = TaxClass
2934
queryset = TaxClass.objects.all()
3035
serializer_class = TaxClassDetailSerializer
3136
lookup_field = 'id'
3237

3338
class TaxRateListCreateAPIView(generics.ListCreateAPIView):
39+
permission_classes = (CommonPermissions, )
40+
model = TaxClass
3441
serializer_class = TaxRateSerializer
3542
queryset = TaxRate.objects.all()
3643
filter_backends = [
@@ -43,6 +50,8 @@ class TaxRateListCreateAPIView(generics.ListCreateAPIView):
4350

4451

4552
class TaxRateRetrieveUpdateDelete(generics.RetrieveUpdateDestroyAPIView):
53+
permission_classes = (CommonPermissions, )
54+
model = TaxClass
4655
serializer_class = TaxRateSerializer
4756
queryset = TaxRate.objects.all()
4857
lookup_field = 'id'

0 commit comments

Comments
 (0)