filters+groups

dev
Aleksandr Popov 3 years ago committed by Timofey Malinin
parent 886128f1d3
commit e846d109e7

@ -174,7 +174,6 @@ SWAGGER_SETTINGS = {
'SWAGGER_PATH': 'django_static/swagger/swagger.yaml', 'SWAGGER_PATH': 'django_static/swagger/swagger.yaml',
} }
SRID = 4326 SRID = 4326
# celery config # celery config
@ -188,3 +187,4 @@ AGE_DAY_LIMIT = 270
AGE_DAY_BORDER = 30 AGE_DAY_BORDER = 30
EXCEL_EXPORT_FILENAME = 'placement_points.xlsx' EXCEL_EXPORT_FILENAME = 'placement_points.xlsx'
JSON_EXPORT_FILENAME = 'placement_points.json' JSON_EXPORT_FILENAME = 'placement_points.json'
DATA_UPLOAD_MAX_NUMBER_FIELDS = None

@ -90,3 +90,4 @@ virtualenv==20.20.0
wcwidth==0.2.6 wcwidth==0.2.6
xlrd==1.2.0 xlrd==1.2.0
XlsxWriter==3.0.8 XlsxWriter==3.0.8
django-filter==23.2

@ -0,0 +1,20 @@
# Generated by Django 3.2 on 2023-08-03 15:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('service', '0023_auto_20230801_1654'),
]
operations = [
migrations.CreateModel(
name='TempFiles',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.TextField()),
],
),
]

@ -0,0 +1,24 @@
# Generated by Django 3.2 on 2023-08-04 10:49
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('service', '0024_tempfiles'),
]
operations = [
migrations.AlterField(
model_name='otherobjectsgroup',
name='category',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='service.otherobjectscategory'),
),
migrations.AlterField(
model_name='post_and_pvzgroup',
name='category',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='service.post_and_pvzcategory'),
),
]

@ -165,7 +165,7 @@ class Post_and_pvzGroup(models.Model):
return self.category.name + ' ' + self.name return self.category.name + ' ' + self.name
name = models.TextField(null=False, blank=False, verbose_name='Название группы') name = models.TextField(null=False, blank=False, verbose_name='Название группы')
category = models.ForeignKey('Post_and_pvzCategory', default=None, related_name='post_and_pvz_group', category = models.ForeignKey('Post_and_pvzCategory', default=None, related_name='groups',
on_delete=models.CASCADE) on_delete=models.CASCADE)
image = models.ImageField(blank=True, null=True, upload_to='post_and_pvz_group_images/', verbose_name='Картинка') image = models.ImageField(blank=True, null=True, upload_to='post_and_pvz_group_images/', verbose_name='Картинка')
visible = models.BooleanField(default=True) visible = models.BooleanField(default=True)
@ -195,7 +195,7 @@ class OtherObjectsGroup(models.Model):
return self.category.name + ' ' + self.name return self.category.name + ' ' + self.name
name = models.TextField(null=False, blank=False, verbose_name='Название группы') name = models.TextField(null=False, blank=False, verbose_name='Название группы')
category = models.ForeignKey('OtherObjectsCategory', default=None, related_name='otherobjectsgroup', category = models.ForeignKey('OtherObjectsCategory', default=None, related_name='groups',
on_delete=models.CASCADE) on_delete=models.CASCADE)
image = models.ImageField(blank=True, null=True, upload_to='other_objects_group_images/', verbose_name='Картинка') image = models.ImageField(blank=True, null=True, upload_to='other_objects_group_images/', verbose_name='Картинка')
visible = models.BooleanField(default=True) visible = models.BooleanField(default=True)
@ -221,3 +221,7 @@ class TaskStatus(models.Model):
class LastMLCall(models.Model): class LastMLCall(models.Model):
dt = models.DateTimeField(auto_now_add=True) dt = models.DateTimeField(auto_now_add=True)
class TempFiles(models.Model):
data = models.TextField(blank=False, null=False)

@ -9,6 +9,34 @@ class PlacementPointSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class PostAndPVZGroupSerializer(serializers.ModelSerializer):
class Meta:
model = models.Post_and_pvzGroup
fields = '__all__'
class PostAndPVZCategorySerializer(serializers.ModelSerializer):
groups = PostAndPVZGroupSerializer(read_only=True, many=True)
class Meta:
model = models.Post_and_pvzCategory
fields = '__all__'
class OtherObjectsGroupSerializer(serializers.ModelSerializer):
class Meta:
model = models.OtherObjectsGroup
fields = '__all__'
class OtherObjectsCategorySerializer(serializers.ModelSerializer):
groups = OtherObjectsGroupSerializer(many=True, read_only=True)
class Meta:
model = models.OtherObjectsCategory
fields = '__all__'
class RayonSerializer(serializers.ModelSerializer): class RayonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.AO model = models.AO

@ -11,6 +11,12 @@ from service.enums import PointStatus
from service.tasks import raschet from service.tasks import raschet
class LayerService:
@staticmethod
def get_post_and_pvz_categroies():
return models.Post_and_pvzCategory.objects.all(), models.Post_and_pvzGroup.objects.all()
class PointService: class PointService:
def update_fact(self, postamat_id: str, fact: int): def update_fact(self, postamat_id: str, fact: int):
qs = self.get_point_by_postamat_id(postamat_id) qs = self.get_point_by_postamat_id(postamat_id)

@ -15,17 +15,13 @@ from sklearn import metrics
from sklearn import model_selection as ms from sklearn import model_selection as ms
from sqlalchemy import text from sqlalchemy import text
from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.db.models.functions import Distance
import requests
from io import StringIO
from postamates.settings import AGE_DAY_LIMIT from postamates.settings import AGE_DAY_LIMIT
from postamates.settings import DB_URL from postamates.settings import DB_URL
from service.models import PlacementPoint, LastMLCall from service.models import PlacementPoint, LastMLCall
from service import models from service import models
from service.utils import log_to_telegram
import base64
def log_to_telegram(msg): from io import StringIO
requests.post('https://api.telegram.org/bot6275517704:AAHVp_qv9d9NU740JJdOM2fJdgS4r1AgJrw/sendMessage',
json={"chat_id": "-555238820", "text": msg})
@shared_task() @shared_task()
@ -314,8 +310,33 @@ def raschet():
@shared_task @shared_task
def calculate_group_distance(groups: list): def load_post_and_pvz(obj_id: int):
status, _ = models.TaskStatus.objects.get_or_create(task_name='Расчет ближайшего расстояния') file = models.TempFiles.objects.get(id=obj_id)
status, _ = models.TaskStatus.objects.get_or_create(task_name='Загрузка ПВЗ и Постаматов')
excel_file = base64.b64decode(file.data)
df = pd.read_excel(excel_file)
df = df.replace(np.nan, None)
df = df.replace('NaT', None)
df.columns = df.columns.str.lower()
data_len = df.shape[0]
for _ind, row in enumerate(df.to_dict('records')):
status.status = "Загрузка данных: " + str(int(_ind / data_len * 100)) + "%"
status.save()
category = row.get('category')
group = row.get('group')
if category:
cat, _ = models.Post_and_pvzCategory.objects.get_or_create(name=category)
if group:
gr, _ = models.Post_and_pvzGroup.objects.get_or_create(name=group, category=cat)
row['category'] = cat
row['group'] = gr
lon = str(row.pop('lon'))
lat = str(row.pop("lat"))
row['wkt'] = "POINT(" + lon + " " + lat + ")"
models.Post_and_pvz.objects.get_or_create(**row)
status.status = "Загрузка данных завершена"
status.save()
groups = df[['group', 'category']].drop_duplicates().to_dict(orient='records')
points = models.PlacementPoint.objects.all() points = models.PlacementPoint.objects.all()
num_points = points.count() num_points = points.count()
total = len(groups) * num_points total = len(groups) * num_points
@ -349,3 +370,57 @@ def add_age_day():
c2 = PlacementPoint.objects.filter(sample_trn=True).count() c2 = PlacementPoint.objects.filter(sample_trn=True).count()
if c2 - c1 != 0: if c2 - c1 != 0:
raschet.delay() raschet.delay()
@shared_task()
def load_other_objects(obj_id: int):
file = models.TempFiles.objects.get(id=obj_id)
status, _ = models.TaskStatus.objects.get_or_create(task_name='Загрузка Прочих объектов')
excel_file = base64.b64decode(file.data)
df = pd.read_excel(excel_file)
df = df.replace(np.nan, None)
df = df.replace('NaT', None)
df.columns = df.columns.str.lower()
data_len = df.shape[0]
for _ind, row in enumerate(df.to_dict('records')):
status.status = "Загрузка данных: " + str(int(_ind / data_len * 100)) + "%"
status.save()
category = row.get('category')
group = row.get('group')
if category:
cat, _ = models.OtherObjectsCategory.objects.get_or_create(name=category)
if group:
gr, _ = models.OtherObjectsGroup.objects.get_or_create(name=group, category=cat)
row['category'] = cat
row['group'] = gr
lon = str(row.pop('lon'))
lat = str(row.pop("lat"))
row['wkt'] = "POINT(" + lon + " " + lat + ")"
models.OtherObjects.objects.get_or_create(**row)
status.status = "Загрузка данных завершена"
status.save()
@shared_task()
def load_data(obj_id: int):
status, _ = models.TaskStatus.objects.get_or_create(task_name='Загрузка Точек')
file = models.TempFiles.objects.get(id=obj_id)
csv_file = base64.b64decode(file.data)
models.PlacementPoint.objects.all().delete()
s = str(csv_file, 'utf-8')
data = StringIO(s)
df = pd.read_csv(data, delimiter=';')
df = df.replace(np.nan, None)
df = df.replace('NaT', None)
data_len = df.shape[0]
for _ind, row in enumerate(df.to_dict('records')):
status.status = "Загрузка данных: " + str(int(_ind / data_len * 100)) + "%"
status.save()
data = {
k: row[k] for k in row.keys() if
k not in ['id', 'location_id', 'area', 'district', 'age_month']
}
models.PlacementPoint.objects.create(**data)
status.status = "Загрузка данных завершена"
status.save()
models.TempFiles.objects.all().delete()

@ -30,6 +30,9 @@ schema_view = get_schema_view(
urlpatterns = [ urlpatterns = [
path('placement_points/', include([*router.urls]), name='placement_points'), path('placement_points/', include([*router.urls]), name='placement_points'),
path('ao_rayons', views.AOViewSet.as_view({'get': 'list'}), name='ao_and_rayons'), path('ao_rayons', views.AOViewSet.as_view({'get': 'list'}), name='ao_and_rayons'),
path('postamate_and_pvz_groups', views.PostAndPVZCategoryViewSet.as_view({'get': 'list'}),
name='postamate_and_pvz_groups'),
path('other_object_groups', views.OtherObjectsCategoryViewSet.as_view({'get': 'list'}), name='other_object_groups'),
url(r'load_csv/', views.refresh_placement_points.as_view(), name='upload_placement_points'), url(r'load_csv/', views.refresh_placement_points.as_view(), name='upload_placement_points'),
url(r'upload_ao_and_rayons/', views.load_ao_and_rayons.as_view(), name='upload_ao_and_rayons'), url(r'upload_ao_and_rayons/', views.load_ao_and_rayons.as_view(), name='upload_ao_and_rayons'),
url(r'upload_post_and_pvz/', views.upload_post_and_pvz, name='upload_post_and_pvz'), url(r'upload_post_and_pvz/', views.upload_post_and_pvz, name='upload_post_and_pvz'),

@ -3,23 +3,10 @@ import numpy as np
import pandas as pd import pandas as pd
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from geojson import MultiPolygon from geojson import MultiPolygon
from tqdm import tqdm
from service.tasks import calculate_group_distance
from service import models from service import models
import requests
from tqdm import tqdm
def load_data(filepath: str):
models.PlacementPoint.objects.all().delete()
df = pd.read_csv(filepath)
df = df.replace(np.nan, None)
df = df.replace('NaT', None)
for row in tqdm(df.to_dict('records'), desc='Loading data...'):
data = {
k: row[k] for k in row.keys() if
k not in ['id', 'location_id', 'area', 'district', 'age_month']
}
models.PlacementPoint.objects.create(**data)
def load_ao_and_rayons( def load_ao_and_rayons(
@ -44,52 +31,15 @@ def load_ao_and_rayons(
models.Rayon.objects.create(**{'name': name, 'polygon': GEOSGeometry(str(MultiPolygon(coords))), 'AO': ao}) models.Rayon.objects.create(**{'name': name, 'polygon': GEOSGeometry(str(MultiPolygon(coords))), 'AO': ao})
def load_rivals(filepath: str):
df = pd.read_excel(filepath)
df = df.replace(np.nan, None)
df = df.replace('NaT', None)
df.columns = df.columns.str.lower()
for row in tqdm(df.to_dict('records'), desc='Loading data...'):
category = row.get('category')
group = row.get('group')
if category:
cat, _ = models.Post_and_pvzCategory.objects.get_or_create(name=category)
if group:
gr, _ = models.Post_and_pvzGroup.objects.get_or_create(name=group, category=cat)
row['category'] = cat
row['group'] = gr
lon = str(row.pop('lon'))
lat = str(row.pop("lat"))
row['wkt'] = "POINT(" + lon + " " + lat + ")"
models.Post_and_pvz.objects.get_or_create(**row)
new_groups = df[['group', 'category']].drop_duplicates().to_dict(orient='records')
calculate_group_distance.delay(new_groups)
def load_other_objects(filepath: str):
df = pd.read_excel(filepath)
df = df.replace(np.nan, None)
df = df.replace('NaT', None)
df.columns = df.columns.str.lower()
for row in tqdm(df.to_dict('records'), desc='Loading data...'):
category = row.get('category')
group = row.get('group')
if category:
cat, _ = models.OtherObjectsCategory.objects.get_or_create(name=category)
if group:
gr, _ = models.OtherObjectsGroup.objects.get_or_create(name=group, category=cat)
row['category'] = cat
row['group'] = gr
lon = str(row.pop('lon'))
lat = str(row.pop("lat"))
row['wkt'] = "POINT(" + lon + " " + lat + ")"
models.OtherObjects.objects.get_or_create(**row)
def load_dist(filepath: str): def load_dist(filepath: str):
models.PointDist.objects.all().delete() models.PointDist.objects.all().delete()
df = pd.read_csv(filepath) df = pd.read_csv(filepath)
for row in tqdm(df.to_dict('records'), desc='Loading data...'): for row in df.to_dict('records'):
row['id1'] = models.PlacementPoint.objects.get(pk=row.get('id1')) row['id1'] = models.PlacementPoint.objects.get(pk=row.get('id1'))
row['id2'] = models.PlacementPoint.objects.get(pk=row.get('id2')) row['id2'] = models.PlacementPoint.objects.get(pk=row.get('id2'))
models.PointDist.objects.create(**row) models.PointDist.objects.create(**row)
def log_to_telegram(msg):
requests.post('https://api.telegram.org/bot6275517704:AAHVp_qv9d9NU740JJdOM2fJdgS4r1AgJrw/sendMessage',
json={"chat_id": "-555238820", "text": msg})

@ -22,11 +22,12 @@ from service import utils
from service.enums import PointStatus from service.enums import PointStatus
from service.permissions import UserPermission from service.permissions import UserPermission
from service.service import PointService from service.service import PointService
from service.tasks import raschet from service.tasks import raschet, load_post_and_pvz, load_other_objects, load_data
from service.utils import load_data
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib import messages from django.contrib import messages
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class AOViewSet(ReadOnlyModelViewSet): class AOViewSet(ReadOnlyModelViewSet):
@ -35,11 +36,79 @@ class AOViewSet(ReadOnlyModelViewSet):
permission_classes = [AllowAny] permission_classes = [AllowAny]
class PostAndPVZCategoryViewSet(ReadOnlyModelViewSet):
serializer_class = serializers.PostAndPVZCategorySerializer
queryset = models.Post_and_pvzCategory.objects
class OtherObjectsCategoryViewSet(ReadOnlyModelViewSet):
serializer_class = serializers.OtherObjectsCategorySerializer
queryset = models.OtherObjectsCategory.objects
class PlacementPointViewSet(ReadOnlyModelViewSet): class PlacementPointViewSet(ReadOnlyModelViewSet):
serializer_class = serializers.PlacementPointSerializer serializer_class = serializers.PlacementPointSerializer
queryset = models.PlacementPoint.objects queryset = models.PlacementPoint.objects
pagination_class = pagination.MyPagination pagination_class = pagination.MyPagination
permission_classes = [UserPermission] permission_classes = [UserPermission]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
@property
def filterset_fields(self):
model_cls = self.queryset.model
fieldset = {
field.name: [
"exact",
"gt",
"gte",
"lt",
"lte",
"in",
"iexact",
"startswith",
"istartswith",
"endswith",
"iendswith",
"regex",
"iregex",
"isnull",
"contains",
"icontains",
]
for field in [
field
for field in model_cls._meta.get_fields()
if field.get_internal_type()
not in [
"JSONField",
"ForeignKey",
"ManyToManyField",
"OneToOneField",
"PointField"
]
]
}
# filters for model relations
for field in [
field
for field in model_cls._meta.get_fields()
if field.get_internal_type()
in [
"ForeignKey",
"ManyToManyField",
"OneToOneField",
]
]:
fieldset[field.name] = [
"exact",
"gt",
"gte",
"lt",
"lte",
]
return fieldset
def get_queryset(self): def get_queryset(self):
qs = self.queryset.all().order_by('id') qs = self.queryset.all().order_by('id')
@ -59,6 +128,7 @@ class PlacementPointViewSet(ReadOnlyModelViewSet):
delta_current = self.request.GET.get('delta_current[]') delta_current = self.request.GET.get('delta_current[]')
rayons = self.request.GET.get('area[]') rayons = self.request.GET.get('area[]')
aos = self.request.GET.get('district[]') aos = self.request.GET.get('district[]')
group_dists = self.request.GET.getlist('dist_to_group')
if location_ids: if location_ids:
location_ids = list(location_ids.split(',')) location_ids = list(location_ids.split(','))
qs = qs.filter(pk__in=location_ids) qs = qs.filter(pk__in=location_ids)
@ -108,8 +178,20 @@ class PlacementPointViewSet(ReadOnlyModelViewSet):
inclded = list(included.split(',')) inclded = list(included.split(','))
qs2 = models.PlacementPoint.objects.filter(pk__in=inclded).all() qs2 = models.PlacementPoint.objects.filter(pk__in=inclded).all()
qs = (qs | qs2).distinct() qs = (qs | qs2).distinct()
if group_dists:
g_d = [list(g.split(',')) for g in group_dists]
for group in g_d:
filtered_points = list(
models.PlacementPointPVZDistance.objects.filter(pvz_postamates_group__id=int(group[0]),
dist__lt=int(group[1])).values_list(
'placement_point__id', flat=True))
qs = qs.filter(id__in=filtered_points)
return qs return qs
@action(methods=['get'], detail=False)
def get_filterset_fields(self, request, *args, **kwargs):
return Response(self.filterset_fields, status=HTTPStatus.OK)
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def filters(self, request): def filters(self, request):
qs = self.get_queryset() qs = self.get_queryset()
@ -231,8 +313,11 @@ class refresh_placement_points(APIView):
@staticmethod @staticmethod
def post(request): def post(request):
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
file = request.FILES['file'] file = request.FILES['file'].file
load_data(file) file_bytes = file.read()
csv_base64 = base64.b64encode(file_bytes).decode()
obj = models.TempFiles.objects.create(data=csv_base64)
load_data.delay(obj.id)
messages.success(request, 'Файл точек успешно загружен') messages.success(request, 'Файл точек успешно загружен')
return redirect('/admin') return redirect('/admin')
@ -248,21 +333,30 @@ class load_ao_and_rayons(APIView):
return redirect('/admin') return redirect('/admin')
import base64
@api_view(['POST']) @api_view(['POST'])
def upload_post_and_pvz(request): def upload_post_and_pvz(request):
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
file_rivals = request.FILES['file_post_and_pvz'] file_rivals = request.FILES['file_post_and_pvz'].file
utils.load_rivals(file_rivals) file_bytes = file_rivals.read()
messages.success(request, 'Файл ПВЗ и Постаматов успешно загружен') excel_base64 = base64.b64encode(file_bytes).decode()
obj = models.TempFiles.objects.create(data=excel_base64)
load_post_and_pvz.delay(obj.id)
messages.success(request, 'Загрузка ПВЗ и Постаматов началась. Отслеживайте выполнение в Статусе фоновых задач')
return redirect('/admin') return redirect('/admin')
@api_view(['POST']) @api_view(['POST'])
def upload_other_objects(request): def upload_other_objects(request):
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
file_rivals = request.FILES['file_other_objects'] file = request.FILES['file_other_objects']
utils.load_other_objects(file_rivals) file_bytes = file.read()
messages.success(request, 'Файл прочих объектов успешно загружен') excel_base64 = base64.b64encode(file_bytes).decode()
obj = models.TempFiles.objects.create(data=excel_base64)
load_other_objects.delay(obj.id)
messages.success(request, 'Загрузка Прочих объектов началась. Отслеживайте выполнение в Статусе фоновых задач')
return redirect('/admin') return redirect('/admin')

@ -0,0 +1,51 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% translate 'Delete multiple objects' %}
</div>
{% endblock %}
{% block content %}
{% if perms_lacking %}
<p>{% blocktranslate %}Deleting the selected {{ objects_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktranslate %}</p>
<ul>{{ perms_lacking|unordered_list }}</ul>
{% elif protected %}
<p>{% blocktranslate %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktranslate %}</p>
<ul>{{ protected|unordered_list }}</ul>
{% else %}
<p>{% blocktranslate %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktranslate %}</p>
{% include "admin/includes/object_delete_summary.html" %}
<h2>{% translate "Objects" %}</h2>
{% for deletable_object in deletable_objects %}
{% if 100 < deletable_object|length %}
<p>{{ deletable_object|length }} objects</p>
{% else %}
<ul>{{ deletable_object|unordered_list }}</ul>
{% endif %}
{% endfor %}
<form method="post">{% csrf_token %}
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}">
{% endfor %}
<input type="hidden" name="action" value="delete_selected">
<input type="hidden" name="post" value="yes">
<input type="submit" value="{% translate 'Yes, Im sure' %}">
<a href="#" class="button cancel-link">{% translate "No, take me back" %}</a>
</div>
</form>
{% endif %}
{% endblock %}
Loading…
Cancel
Save