added report app
This commit is contained in:
parent
ab3666b03d
commit
8eb07205e1
0
apps/report/__init__.py
Normal file
0
apps/report/__init__.py
Normal file
8
apps/report/admin.py
Normal file
8
apps/report/admin.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Report)
|
||||||
|
class ReportAdmin(admin.ModelAdmin):
|
||||||
|
"""Report admin model."""
|
||||||
8
apps/report/apps.py
Normal file
8
apps/report/apps.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ReportConfig(AppConfig):
|
||||||
|
name = 'report'
|
||||||
|
verbose_name = _('Report')
|
||||||
|
|
||||||
26
apps/report/filters.py
Normal file
26
apps/report/filters.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Filters for application report."""
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
|
||||||
|
class ReportFilterSet(filters.FilterSet):
|
||||||
|
"""Report filter set."""
|
||||||
|
source = filters.ChoiceFilter(
|
||||||
|
choices=Report.SOURCE_CHOICES,
|
||||||
|
help_text='Filter allow filtering QuerySet by a field - source.'
|
||||||
|
'Choices - 0 (Back office), 1 (Web), 2 (Mobile)'
|
||||||
|
)
|
||||||
|
category = filters.ChoiceFilter(
|
||||||
|
choices=Report.CATEGORY_CHOICES,
|
||||||
|
help_text='Filter allow filtering QuerySet by a field - category.'
|
||||||
|
'Choices - 0 (Bug), 1 (Suggestion/improvement)'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = Report
|
||||||
|
fields = (
|
||||||
|
'source',
|
||||||
|
'category',
|
||||||
|
)
|
||||||
31
apps/report/migrations/0001_initial.py
Normal file
31
apps/report/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-05 12:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Report',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
|
||||||
|
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('source', models.PositiveSmallIntegerField(choices=[(0, 'Back office'), (1, 'Web'), (2, 'Mobile')], verbose_name='source')),
|
||||||
|
('category', models.PositiveSmallIntegerField(choices=[(0, 'Bug'), (1, 'Suggestion/improvement')], verbose_name='category')),
|
||||||
|
('url', models.URLField(verbose_name='URL')),
|
||||||
|
('description', models.TextField(verbose_name='description')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Report',
|
||||||
|
'verbose_name_plural': 'Reports',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/report/migrations/__init__.py
Normal file
0
apps/report/migrations/__init__.py
Normal file
84
apps/report/models.py
Normal file
84
apps/report/models.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
|
from report.tasks import send_report_task
|
||||||
|
from utils.models import ProjectBaseMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ReportManager(models.Manager):
|
||||||
|
"""Manager for model Report."""
|
||||||
|
|
||||||
|
def make(self, source: int, category, url: str, description: str):
|
||||||
|
"""Make object."""
|
||||||
|
obj = self.create(
|
||||||
|
source=source,
|
||||||
|
category=category,
|
||||||
|
url=url,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
send_report_task.delay(obj.id)
|
||||||
|
else:
|
||||||
|
send_report_task(obj.id)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class ReportQuerySet(models.QuerySet):
|
||||||
|
"""QuerySet for model Report."""
|
||||||
|
|
||||||
|
def by_source(self, source: int):
|
||||||
|
"""Return QuerySet filtered by a source."""
|
||||||
|
return self.filter(source=source)
|
||||||
|
|
||||||
|
|
||||||
|
class Report(ProjectBaseMixin):
|
||||||
|
"""Report model."""
|
||||||
|
|
||||||
|
BACK_OFFICE = 0
|
||||||
|
WEB = 1
|
||||||
|
MOBILE = 2
|
||||||
|
|
||||||
|
SOURCE_CHOICES = (
|
||||||
|
(BACK_OFFICE, _('Back office')),
|
||||||
|
(WEB, _('Web')),
|
||||||
|
(MOBILE, _('Mobile')),
|
||||||
|
)
|
||||||
|
|
||||||
|
BUG = 0
|
||||||
|
SUGGESTION_IMPROVEMENT = 1
|
||||||
|
|
||||||
|
CATEGORY_CHOICES = (
|
||||||
|
(BUG, _('Bug')),
|
||||||
|
(SUGGESTION_IMPROVEMENT, _('Suggestion/improvement')),
|
||||||
|
)
|
||||||
|
|
||||||
|
source = models.PositiveSmallIntegerField(choices=SOURCE_CHOICES,
|
||||||
|
verbose_name=_('source'))
|
||||||
|
category = models.PositiveSmallIntegerField(choices=CATEGORY_CHOICES,
|
||||||
|
verbose_name=_('category'))
|
||||||
|
url = models.URLField(verbose_name=_('URL'))
|
||||||
|
description = models.TextField(verbose_name=_('description'))
|
||||||
|
|
||||||
|
objects = ReportManager.from_queryset(ReportQuerySet)()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
verbose_name = _('Report')
|
||||||
|
verbose_name_plural = _('Reports')
|
||||||
|
|
||||||
|
def get_body_email_message(self):
|
||||||
|
"""Prepare the body of the email message"""
|
||||||
|
return {
|
||||||
|
'subject': self.get_category_display(),
|
||||||
|
'message': str(self.description),
|
||||||
|
'html_message': self.description,
|
||||||
|
'from_email': settings.EMAIL_HOST_USER,
|
||||||
|
'recipient_list': [settings.EMAIL_TECHNICAL_SUPPORT, ],
|
||||||
|
}
|
||||||
|
|
||||||
|
def send_email(self):
|
||||||
|
"""Send an email reset user password"""
|
||||||
|
send_mail(**self.get_body_email_message())
|
||||||
|
|
||||||
34
apps/report/serializers.py
Normal file
34
apps/report/serializers.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""DRF-serializers for application report."""
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
|
||||||
|
class ReportBaseSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for model Report."""
|
||||||
|
|
||||||
|
category_display = serializers.CharField(source='get_category_display', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = Report
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'category',
|
||||||
|
'category_display',
|
||||||
|
'url',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'source': {'required': False},
|
||||||
|
'category': {'write_only': True}
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""An overridden validate method."""
|
||||||
|
attrs['source'] = self.context.get('view').get_source()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""An overridden create method."""
|
||||||
|
return self.Meta.model.objects.make(**validated_data)
|
||||||
18
apps/report/tasks.py
Normal file
18
apps/report/tasks.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Report app tasks."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_report_task(report_id: int):
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
report_qs = Report.objects.filter(id=report_id)
|
||||||
|
if report_qs.exists():
|
||||||
|
report = report_qs.first()
|
||||||
|
report.send_email()
|
||||||
|
else:
|
||||||
|
logger.error(f'Error sending report {report_id}')
|
||||||
1
apps/report/tests.py
Normal file
1
apps/report/tests.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Create your tests here.
|
||||||
0
apps/report/urls/__init__.py
Normal file
0
apps/report/urls/__init__.py
Normal file
10
apps/report/urls/back.py
Normal file
10
apps/report/urls/back.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Back office URL patterns for application report."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from report.views import back as views
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.ReportListCreateView.as_view(), name='report-list-create'),
|
||||||
|
]
|
||||||
9
apps/report/urls/common.py
Normal file
9
apps/report/urls/common.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""Common URL patterns for application report."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from report.views import common as views
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
urlpatterns = [
|
||||||
|
path('<int:pk>/', views.ReportRetrieveView.as_view(), name='report-retrieve')
|
||||||
|
]
|
||||||
5
apps/report/urls/mobile.py
Normal file
5
apps/report/urls/mobile.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Mobile URL patterns for application report."""
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
11
apps/report/urls/web.py
Normal file
11
apps/report/urls/web.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""Web URL patterns for application report."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from report.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
from report.views import web as views
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
|
||||||
|
urlpatterns = common_urlpatterns + [
|
||||||
|
path('', views.ReportListCreateView.as_view(), name='report-list-create'),
|
||||||
|
]
|
||||||
0
apps/report/views/__init__.py
Normal file
0
apps/report/views/__init__.py
Normal file
39
apps/report/views/back.py
Normal file
39
apps/report/views/back.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""Views for application report."""
|
||||||
|
from rest_framework.generics import ListCreateAPIView
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
from report.views.common import ReportBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class ReportListCreateView(ReportBaseView, ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
## View for getting list of reports or create a new one.
|
||||||
|
### POST-request data
|
||||||
|
Request data attributes:
|
||||||
|
* category: integer (0 - Bug, 1 - Suggestion improvement)
|
||||||
|
* url: char (URL)
|
||||||
|
* description: text (problem description)
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"category": 1,
|
||||||
|
"url": "http://google.com",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
*GET*
|
||||||
|
Return paginated list of reports.
|
||||||
|
|
||||||
|
*POST*
|
||||||
|
Creates a new report, and returns a serialized object.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Method that allows getting list of reports or create a new one and return serialized object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_source():
|
||||||
|
"""Return a source for view."""
|
||||||
|
return Report.BACK_OFFICE
|
||||||
58
apps/report/views/common.py
Normal file
58
apps/report/views/common.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""Common views for application report."""
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
from report.filters import ReportFilterSet
|
||||||
|
from report.models import Report
|
||||||
|
from report.serializers import ReportBaseSerializer
|
||||||
|
from utils.methods import get_permission_classes
|
||||||
|
from utils.permissions import (
|
||||||
|
IsEstablishmentManager, IsContentPageManager, IsReviewManager,
|
||||||
|
IsModerator, IsRestaurantInspector, IsArtisanInspector,
|
||||||
|
IsWineryWineInspector, IsDistilleryLiquorInspector, IsProducerFoodInspector,
|
||||||
|
IsEstablishmentAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportBaseView(generics.GenericAPIView):
|
||||||
|
"""
|
||||||
|
## Report base view.
|
||||||
|
"""
|
||||||
|
queryset = Report.objects.all()
|
||||||
|
serializer_class = ReportBaseSerializer
|
||||||
|
filter_class = ReportFilterSet
|
||||||
|
permission_classes = get_permission_classes(
|
||||||
|
IsEstablishmentManager, IsContentPageManager, IsReviewManager,
|
||||||
|
IsModerator, IsRestaurantInspector, IsArtisanInspector,
|
||||||
|
IsWineryWineInspector, IsDistilleryLiquorInspector, IsProducerFoodInspector,
|
||||||
|
IsEstablishmentAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_source():
|
||||||
|
"""Return a source for view."""
|
||||||
|
return NotImplementedError('You must implement "get_source" method')
|
||||||
|
|
||||||
|
|
||||||
|
class ReportRetrieveView(ReportBaseView, generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
## View for retrieving serialized instance.
|
||||||
|
### Response
|
||||||
|
Return serialized object.
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"count": 7,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Method that allows retrieving serialized report object.
|
||||||
|
"""
|
||||||
39
apps/report/views/web.py
Normal file
39
apps/report/views/web.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""Views for application report."""
|
||||||
|
from rest_framework.generics import ListCreateAPIView
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
from report.views.common import ReportBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class ReportListCreateView(ReportBaseView, ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
## View for getting list of reports or create a new one.
|
||||||
|
### POST-request data
|
||||||
|
Request data attributes:
|
||||||
|
* category: integer (0 - Bug, 1 - Suggestion improvement)
|
||||||
|
* url: char (URL)
|
||||||
|
* description: text (problem description)
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"category": 1,
|
||||||
|
"url": "http://google.com",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
*GET*
|
||||||
|
Return paginated list of reports.
|
||||||
|
|
||||||
|
*POST*
|
||||||
|
Creates a new report, and returns a serialized object.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Method that allows getting list of reports or create a new one and return serialized object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_source():
|
||||||
|
"""Return a source for view."""
|
||||||
|
return Report.WEB
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
"""Translation app models."""
|
"""Translation app models."""
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.apps import apps
|
|
||||||
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
||||||
|
|
||||||
|
|
||||||
class LanguageQuerySet(models.QuerySet):
|
class LanguageQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model Language"""
|
"""QuerySet for model Language"""
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
||||||
Tag = apps.get_model('tag', 'Tag')
|
Tag = apps.get_model('tag', 'Tag')
|
||||||
"""Creates or updates translation for EXISTING in DB Tag"""
|
"""Creates or updates translation for EXISTING in DB Tag"""
|
||||||
if not tag.pk or not isinstance(tag, Tag):
|
if not tag.pk or not isinstance(tag, Tag):
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
if tag.translation:
|
if tag.translation:
|
||||||
tag.translation.text = translations
|
tag.translation.text = translations
|
||||||
tag.translation.page = 'tag'
|
tag.translation.page = 'tag'
|
||||||
|
|
@ -74,7 +76,7 @@ class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
||||||
"""Creates or updates translation for EXISTING in DB TagCategory"""
|
"""Creates or updates translation for EXISTING in DB TagCategory"""
|
||||||
TagCategory = apps.get_model('tag', 'TagCategory')
|
TagCategory = apps.get_model('tag', 'TagCategory')
|
||||||
if not tag_category.pk or not isinstance(tag_category, TagCategory):
|
if not tag_category.pk or not isinstance(tag_category, TagCategory):
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
if tag_category.translation:
|
if tag_category.translation:
|
||||||
tag_category.translation.text = translations
|
tag_category.translation.text = translations
|
||||||
tag_category.translation.page = 'tag'
|
tag_category.translation.page = 'tag'
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ class OAuthProjectMixin:
|
||||||
|
|
||||||
def get_source(self):
|
def get_source(self):
|
||||||
"""Method to get of platform"""
|
"""Method to get of platform"""
|
||||||
return NotImplementedError
|
return NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class BaseAttributes(ProjectBaseMixin):
|
class BaseAttributes(ProjectBaseMixin):
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ PROJECT_APPS = [
|
||||||
'favorites.apps.FavoritesConfig',
|
'favorites.apps.FavoritesConfig',
|
||||||
'rating.apps.RatingConfig',
|
'rating.apps.RatingConfig',
|
||||||
'tag.apps.TagConfig',
|
'tag.apps.TagConfig',
|
||||||
|
'report.apps.ReportConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_APPS = [
|
EXTERNAL_APPS = [
|
||||||
|
|
@ -566,3 +567,5 @@ COUNTRY_CALLING_CODES = {
|
||||||
|
|
||||||
CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES = [590, 594, 1758, 596]
|
CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES = [590, 594, 1758, 596]
|
||||||
DEFAULT_CALLING_CODE_ANTILLES_GUYANE_WEST_INDIES = 590
|
DEFAULT_CALLING_CODE_ANTILLES_GUYANE_WEST_INDIES = 590
|
||||||
|
|
||||||
|
EMAIL_TECHNICAL_SUPPORT = 'it-report@gaultmillau.com'
|
||||||
|
|
|
||||||
|
|
@ -123,3 +123,5 @@ EMAIL_PORT = 587
|
||||||
|
|
||||||
# ADD TRANSFER TO INSTALLED APPS
|
# ADD TRANSFER TO INSTALLED APPS
|
||||||
INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
||||||
|
|
||||||
|
EMAIL_TECHNICAL_SUPPORT = 'a.feteleu@spider.ru'
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ urlpatterns = [
|
||||||
namespace='advertisement')),
|
namespace='advertisement')),
|
||||||
path('main/', include('main.urls.back')),
|
path('main/', include('main.urls.back')),
|
||||||
path('partner/', include('partner.urls.back')),
|
path('partner/', include('partner.urls.back')),
|
||||||
|
path('report/', include('report.urls.back')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,5 @@ urlpatterns = [
|
||||||
path('favorites/', include('favorites.urls')),
|
path('favorites/', include('favorites.urls')),
|
||||||
path('timetables/', include('timetable.urls.web')),
|
path('timetables/', include('timetable.urls.web')),
|
||||||
path('products/', include('product.urls.web')),
|
path('products/', include('product.urls.web')),
|
||||||
|
path('report/', include('report.urls.web')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user