basic promotion handler
This commit is contained in:
parent
1b31455709
commit
75924dd2b6
21 changed files with 312 additions and 115 deletions
10
apps/promotions/admin.py
Normal file
10
apps/promotions/admin.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Promotion, Plan
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Plan)
|
||||||
|
admin.site.register(Promotion)
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class SampleConfig(AppConfig):
|
class PromotionsConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'apps.sample'
|
name = 'apps.promotions'
|
||||||
43
apps/promotions/handlers.py
Normal file
43
apps/promotions/handlers.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from enum import property as enum_property # ??
|
||||||
|
|
||||||
|
|
||||||
|
class PromotionTypeChoices(models.TextChoices):
|
||||||
|
WALLET_PERCENTAGE = 'wallet percentage', _('Wallet percentage')
|
||||||
|
OTHERS = 'others', _('others')
|
||||||
|
|
||||||
|
@enum_property
|
||||||
|
def handler(self):
|
||||||
|
if self.name == self.WALLET_PERCENTAGE.name:
|
||||||
|
return WalletPercentageHandler
|
||||||
|
|
||||||
|
elif self.name == self.OTHERS.name:
|
||||||
|
return OtherHandler
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class BasePromotionHandler:
|
||||||
|
def __init__(self, plan):
|
||||||
|
self.plan = plan
|
||||||
|
|
||||||
|
def calculate(self, **kwargs):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class WalletPercentageHandler(BasePromotionHandler):
|
||||||
|
def calculate(self, **kwargs):
|
||||||
|
base_amount = kwargs.get('base_amount', 0)
|
||||||
|
|
||||||
|
if base_amount <= 1000 and self.plan.balance >= 1000:
|
||||||
|
promotion_amount = base_amount
|
||||||
|
|
||||||
|
else:
|
||||||
|
promotion_amount = 1000 if self.plan.balance >= 1000 else self.plan.balance
|
||||||
|
return promotion_amount
|
||||||
|
|
||||||
|
|
||||||
|
class OtherHandler(BasePromotionHandler):
|
||||||
|
pass
|
||||||
52
apps/promotions/migrations/0001_initial.py
Normal file
52
apps/promotions/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-08-02 16:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Plan',
|
||||||
|
fields=[
|
||||||
|
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||||
|
('title', models.CharField(db_index=True, max_length=255, verbose_name='title')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='description')),
|
||||||
|
('promotion_type', models.CharField(choices=[('wallet percentage', 'Wallet percentage'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')),
|
||||||
|
('policy', models.JSONField(blank=True, default=dict, null=True, verbose_name='policy')),
|
||||||
|
('balance', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='balance')),
|
||||||
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Promotion',
|
||||||
|
fields=[
|
||||||
|
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||||
|
('state', models.IntegerField(choices=[(0, 'Created'), (1, 'Assigned'), (10, 'Expired')], default=0)),
|
||||||
|
('data', models.JSONField(blank=True, null=True)),
|
||||||
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.plan')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/promotions/migrations/__init__.py
Normal file
0
apps/promotions/migrations/__init__.py
Normal file
68
apps/promotions/models.py
Normal file
68
apps/promotions/models.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from oauth2_provider.settings import oauth2_settings
|
||||||
|
|
||||||
|
from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler
|
||||||
|
from utils.models import BaseModel
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class PlanQuerySet(models.QuerySet):
|
||||||
|
def calculate(self, user, application=None):
|
||||||
|
for plan in self.filter(application=application)[:10]:
|
||||||
|
if not Promotion.objects.used(user):
|
||||||
|
plan.calculate(user)
|
||||||
|
|
||||||
|
|
||||||
|
class Plan(BaseModel):
|
||||||
|
# user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||||
|
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||||
|
related_name='+', null=True, blank=True)
|
||||||
|
title = models.CharField(max_length=255, verbose_name=_('title'), db_index=True)
|
||||||
|
description = models.TextField(verbose_name=_('description'), blank=True)
|
||||||
|
promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True, choices=PromotionTypeChoices.choices)
|
||||||
|
policy = models.JSONField(default=dict, verbose_name=_('policy'), null=True, blank=True)
|
||||||
|
balance = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_('balance'), default=0)
|
||||||
|
|
||||||
|
objects = PlanQuerySet.as_manager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def handler(self) -> BasePromotionHandler:
|
||||||
|
return PromotionTypeChoices(self.promotion_type).handler(
|
||||||
|
application=self.application,
|
||||||
|
policy=self.policy,
|
||||||
|
balance=self.balance,
|
||||||
|
)
|
||||||
|
|
||||||
|
def calculate_promotion(self, **kwargs):
|
||||||
|
handler = self.handler
|
||||||
|
self.base_amount = kwargs.get('base_amount', 0)
|
||||||
|
self.promotion_amount = handler.calculate(**kwargs)
|
||||||
|
return self.promotion_amount
|
||||||
|
|
||||||
|
|
||||||
|
class PromotionQuerySet(models.QuerySet):
|
||||||
|
def used(self, user):
|
||||||
|
return self.filter(user=user, state__in=[Promotion.StateChoices.CREATED])
|
||||||
|
|
||||||
|
|
||||||
|
class Promotion(BaseModel):
|
||||||
|
class StateChoices(models.IntegerChoices):
|
||||||
|
CREATED = 0, _('Created')
|
||||||
|
ASSIGNED = 1, _('Assigned')
|
||||||
|
EXPIRED = 10, _('Expired')
|
||||||
|
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||||
|
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||||
|
related_name='+', null=True, blank=True)
|
||||||
|
plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True)
|
||||||
|
state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
|
||||||
|
|
||||||
|
data = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
|
objects = PromotionQuerySet.as_manager()
|
||||||
|
|
||||||
47
apps/promotions/serializers.py
Normal file
47
apps/promotions/serializers.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Promotion, Plan
|
||||||
|
|
||||||
|
|
||||||
|
class PlanSerializer(serializers.ModelSerializer):
|
||||||
|
promotion_amount = serializers.SerializerMethodField(method_name='get_promotion_amount')
|
||||||
|
|
||||||
|
def get_promotion_amount(self, obj):
|
||||||
|
user = self.context['request'].user
|
||||||
|
return obj.calculate_promotion(user)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Plan
|
||||||
|
fields = (
|
||||||
|
"promotion_amount",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"application",
|
||||||
|
)
|
||||||
|
read_only_fields = ('application', "promotion_amount")
|
||||||
|
|
||||||
|
class PromotionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Promotion
|
||||||
|
fields = (
|
||||||
|
"user",
|
||||||
|
"application",
|
||||||
|
)
|
||||||
|
read_only_fields = ('user','application')
|
||||||
|
|
||||||
|
class CalculatedPlanSerializer(serializers.ModelSerializer):
|
||||||
|
base_amount = serializers.IntegerField(required=True)
|
||||||
|
promotion_amount = serializers.IntegerField(read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Promotion
|
||||||
|
fields = (
|
||||||
|
"base_amount",
|
||||||
|
"user",
|
||||||
|
"application",
|
||||||
|
"promotion_amount"
|
||||||
|
)
|
||||||
|
read_only_fields = ('user','application', "promotion_amount")
|
||||||
|
|
||||||
|
# def get_promotion_amount(self, obj):
|
||||||
|
# user = self.context['request'].user
|
||||||
|
# return obj.calculate_promotion(user)
|
||||||
|
|
@ -5,13 +5,13 @@ from utils.router import ProfileRouter
|
||||||
from . import views
|
from . import views
|
||||||
from django.urls import NoReverseMatch, path, re_path, include
|
from django.urls import NoReverseMatch, path, re_path, include
|
||||||
|
|
||||||
app_name = 'sample'
|
app_name = 'promotions'
|
||||||
|
|
||||||
application_router = DefaultRouter()
|
application_router = DefaultRouter()
|
||||||
application_router.register(r'', views.ApplicationSampleViewSet, basename='application' )
|
application_router.register(r'', views.ApplicationPlanViewSet, basename='application' )
|
||||||
|
|
||||||
user_router = ProfileRouter()
|
user_router = DefaultRouter()
|
||||||
user_router.register('', views.UserSampleViewSet, basename='user')
|
user_router.register('', views.UserPlanViewSet, basename='user')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('api/v1/application/<user_uuid>/', include(application_router.urls), name='application'),
|
path('api/v1/application/<user_uuid>/', include(application_router.urls), name='application'),
|
||||||
84
apps/promotions/views.py
Normal file
84
apps/promotions/views.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import render
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
|
||||||
|
from rest_framework import viewsets, mixins
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
|
from .models import Promotion, Plan
|
||||||
|
from .serializers import PlanSerializer, PromotionSerializer, CalculatedPlanSerializer
|
||||||
|
from apps.gooyal_oauth2.models import Application
|
||||||
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
|
from apps.gooyal_oauth2.utils import get_application
|
||||||
|
from ..users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
|
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
|
||||||
|
from rest_framework import viewsets, mixins
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
|
from .models import Plan, Promotion
|
||||||
|
from apps.gooyal_oauth2.models import Application
|
||||||
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
|
from apps.gooyal_oauth2.utils import get_application
|
||||||
|
from ..users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationPlanViewSet(mixins.ListModelMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
GenericViewSet):
|
||||||
|
queryset = Plan.objects.all()
|
||||||
|
serializer_class = PlanSerializer
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
|
required_alternate_scopes = {
|
||||||
|
"POST": [["promotions.application.user-plans:submit"]],
|
||||||
|
"GET": [["promotions.application.user-promotions:list-retrieve"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
# def perform_create(self, serializer):
|
||||||
|
# user_uuid = self.kwargs.get('user_uuid')
|
||||||
|
# user = User.objects.get(uuid=user_uuid)
|
||||||
|
# application = get_application(self.request)
|
||||||
|
# instance = serializer.save(user=user, application=application)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPlanViewSet(mixins.RetrieveModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
GenericViewSet):
|
||||||
|
|
||||||
|
serializer_class = PlanSerializer
|
||||||
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
|
required_alternate_scopes = {
|
||||||
|
"GET": [["promotions.user.self-plans:list-retrieve"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
application = get_application(self.request)
|
||||||
|
|
||||||
|
queryset = Plan.objects.all()
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@extend_schema(responses=CalculatedPlanSerializer(many=True))
|
||||||
|
@action(detail=False, methods=['POST'], serializer_class=CalculatedPlanSerializer)
|
||||||
|
def calculate(self, request):
|
||||||
|
user = self.request.user
|
||||||
|
serializer = self.get_serializer(data=self.request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
print(serializer.validated_data)
|
||||||
|
plans = self.paginate_queryset(self.filter_queryset(self.get_queryset()))
|
||||||
|
for plan in plans:
|
||||||
|
plan.calculate_promotion(**serializer.validated_data)
|
||||||
|
serializer = self.get_serializer(plans, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
from functools import update_wrapper
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from .models import Sample
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Sample)
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
from oauth2_provider.settings import oauth2_settings
|
|
||||||
|
|
||||||
from utils.models import BaseModel
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class SampleQuerySet(models.QuerySet):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Sample(BaseModel):
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
|
||||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
|
||||||
related_name='+', null=True, blank=True)
|
|
||||||
|
|
||||||
data = models.JSONField(null=True, blank=True)
|
|
||||||
|
|
||||||
objects = SampleQuerySet.as_manager()
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from .models import Sample
|
|
||||||
|
|
||||||
|
|
||||||
class SampleSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Sample
|
|
||||||
fields = (
|
|
||||||
"user",
|
|
||||||
"application",
|
|
||||||
"data"
|
|
||||||
)
|
|
||||||
read_only_fields = ('user','application')
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import render
|
|
||||||
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
|
|
||||||
from rest_framework import viewsets, mixins
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
|
||||||
|
|
||||||
from .models import PushUser, PushMessage
|
|
||||||
from .serializers import PushUserSerializer, PushMessageSerializer
|
|
||||||
from apps.gooyal_oauth2.models import Application
|
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
|
||||||
from apps.gooyal_oauth2.utils import get_application
|
|
||||||
from ..users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.urls import reverse
|
|
||||||
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
|
|
||||||
from rest_framework import viewsets, mixins
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
|
||||||
|
|
||||||
from .models import Sample
|
|
||||||
from .serializers import SampleSerializer
|
|
||||||
from apps.gooyal_oauth2.models import Application
|
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
|
||||||
from apps.gooyal_oauth2.utils import get_application
|
|
||||||
from ..users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationSampleViewSet(mixins.ListModelMixin,
|
|
||||||
mixins.CreateModelMixin,
|
|
||||||
GenericViewSet):
|
|
||||||
queryset = Sample.objects.all()
|
|
||||||
serializer_class = SampleSerializer
|
|
||||||
|
|
||||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
|
||||||
required_alternate_scopes = {
|
|
||||||
"POST": [["service_template.application.user-sample:submit"]],
|
|
||||||
"GET": [["service_template.application.user-sample:list-retrieve"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
user_uuid = self.kwargs.get('user_uuid')
|
|
||||||
user = User.objects.get(uuid=user_uuid)
|
|
||||||
application = get_application(self.request)
|
|
||||||
instance = serializer.save(user=user, application=application)
|
|
||||||
|
|
||||||
|
|
||||||
class UserSampleViewSet(mixins.RetrieveModelMixin,
|
|
||||||
GenericViewSet):
|
|
||||||
queryset = Sample.objects.all()
|
|
||||||
serializer_class = SampleSerializer
|
|
||||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
|
||||||
required_alternate_scopes = {
|
|
||||||
"GET": [["service_template.user.self-sample:list-retrieve"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ INSTALLED_APPS = [
|
||||||
'utils',
|
'utils',
|
||||||
'apps.users',
|
'apps.users',
|
||||||
'apps.gooyal_oauth2',
|
'apps.gooyal_oauth2',
|
||||||
# 'apps.sample',
|
'apps.promotions',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('', include('utils.urls')),
|
path('', include('utils.urls')),
|
||||||
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
||||||
# path('sample/', include('apps.sample.urls', namespace='sample')),
|
path('promotions/', include('apps.promotions.urls', namespace='promotions')),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue