mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-29 09:43:43 +03:00
added forms, utils
This commit is contained in:
parent
d1b10a0067
commit
5a3b097532
|
@ -46,21 +46,32 @@ const select = document.getElementById('inputGroupSelect')
|
||||||
select.innerHTML += `<option value="${type}">${description}</option>`
|
select.innerHTML += `<option value="${type}">${description}</option>`
|
||||||
|
|
||||||
questionPrototype = `
|
questionPrototype = `
|
||||||
<div class="border p-4 mt-5 rounded-2">
|
<div class="row ms-2">
|
||||||
<input type="hidden" value="${type}" name="type" />
|
<div class="col col-11">
|
||||||
{% for field in question_form %}
|
<input type="hidden" value="${type}" name="type" />
|
||||||
{{ field|as_crispy_field }}
|
{% for field in question_form %}
|
||||||
{% endfor %}
|
{{ field|as_crispy_field }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input name="required" class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault">
|
||||||
|
<label class="form-check-label" for="flexSwitchCheckDefault">Required</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-1">
|
||||||
|
<button onclick="this.parentNode.parentNode.parentNode.remove()" type="button" class="m-2 btn border-0"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
questions.push(questionPrototype)
|
questions.push(questionPrototype)
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
button.onclick = function () {
|
button.onclick = function () {
|
||||||
console.log(q_index)
|
|
||||||
let index = select.selectedIndex;
|
let index = select.selectedIndex;
|
||||||
let div = questions[index];
|
let div = document.createElement("div");
|
||||||
form.innerHTML += div;
|
div.id = q_index;
|
||||||
|
div.className = "px-2 py-4 rounded-2 border mt-5";
|
||||||
|
div.innerHTML = questions[index]
|
||||||
|
form.appendChild(div);
|
||||||
q_index += 1;
|
q_index += 1;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
</body>
|
{% block content %}
|
||||||
</html>
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% autoescape off %}
|
||||||
|
{{ form }}
|
||||||
|
{% endautoescape %}
|
||||||
|
<button class="btn btn-success ms-4">Отправить</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Meta:
|
||||||
class BaseQuestionForm(forms.ModelForm):
|
class BaseQuestionForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BaseQuestion
|
model = BaseQuestion
|
||||||
fields = ["question", "help", "required"]
|
fields = ["question", "help"]
|
||||||
|
|
||||||
|
|
||||||
class TextQuestionForm(BaseQuestionForm):
|
class TextQuestionForm(BaseQuestionForm):
|
||||||
|
@ -67,6 +67,11 @@ class Meta(BaseQuestionForm.Meta):
|
||||||
|
|
||||||
|
|
||||||
class SelectQuestionForm(BaseQuestionForm):
|
class SelectQuestionForm(BaseQuestionForm):
|
||||||
|
# {} to add index on fronted for submission
|
||||||
|
answer = forms.CharField(
|
||||||
|
widget=forms.TextInput(attrs={"name": "{}_answers"}), required=False
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(BaseQuestionForm, self).__init__(*args, **kwargs)
|
super(BaseQuestionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -75,6 +80,7 @@ class Meta(BaseQuestionForm.Meta):
|
||||||
fields = BaseQuestionForm.Meta.fields + [
|
fields = BaseQuestionForm.Meta.fields + [
|
||||||
"min_required_answers",
|
"min_required_answers",
|
||||||
"max_required_answers",
|
"max_required_answers",
|
||||||
|
"answer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-02-27 06:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("test_platform", "0004_alter_numberquestion_correct_answer"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="basequestion",
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="basequestion",
|
||||||
|
name="order",
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="basequestion",
|
||||||
|
unique_together={("form", "order")},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -41,7 +42,7 @@ def available(self) -> bool:
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
# TODO change to admin
|
# TODO change to admin
|
||||||
return reverse("test_platform:create")
|
return reverse("test_platform:view", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"form: {self.name}"
|
return f"form: {self.name}"
|
||||||
|
@ -53,6 +54,7 @@ class BaseQuestion(PolymorphicModel, SubclassesMixin):
|
||||||
form: Form = models.ForeignKey(
|
form: Form = models.ForeignKey(
|
||||||
"test_platform.Form", related_name="fields", on_delete=models.CASCADE
|
"test_platform.Form", related_name="fields", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
order = models.IntegerField(default=0)
|
||||||
question = models.CharField(max_length=250, blank=False)
|
question = models.CharField(max_length=250, blank=False)
|
||||||
help = models.CharField(max_length=200, blank=True)
|
help = models.CharField(max_length=200, blank=True)
|
||||||
required = models.BooleanField(default=True)
|
required = models.BooleanField(default=True)
|
||||||
|
@ -60,6 +62,14 @@ class BaseQuestion(PolymorphicModel, SubclassesMixin):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.type} - {self.question}"
|
return f"{self.type} - {self.question}"
|
||||||
|
|
||||||
|
def generate_html(self, num) -> str:
|
||||||
|
return render_to_string(
|
||||||
|
f"fields/{self.type}.html", context={"question": self, "order": num}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["form", "order"]
|
||||||
|
|
||||||
|
|
||||||
class TextQuestion(BaseQuestion):
|
class TextQuestion(BaseQuestion):
|
||||||
type = "text"
|
type = "text"
|
||||||
|
|
|
@ -21,11 +21,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_fields_from_type(type: str):
|
def _get_fields_and_class_from_type(type: str):
|
||||||
for question in BaseQuestion.get_subclasses()[::-1]:
|
for question in BaseQuestion.get_subclasses()[::-1]:
|
||||||
if question.type == type:
|
if question.type == type:
|
||||||
form = question_forms[question]
|
form = question_forms[question]
|
||||||
return form.Meta.fields
|
return question, form.Meta.fields + ["required"]
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,17 +39,19 @@ def get_question_types() -> dict[BaseQuestion, BaseQuestionForm]:
|
||||||
|
|
||||||
def parse_form_create(values) -> list[dict[str, str]]:
|
def parse_form_create(values) -> list[dict[str, str]]:
|
||||||
offset: dict[str, int] = {}
|
offset: dict[str, int] = {}
|
||||||
res: list[dict[str, str]] = []
|
res: list[dict[str, str | bool]] = []
|
||||||
question_amount = len(values.getlist("type"))
|
question_amount = len(values.getlist("type"))
|
||||||
for i in range(question_amount):
|
for i in range(question_amount):
|
||||||
type = values.getlist("type")[i]
|
type = values.getlist("type")[i]
|
||||||
res.append({"type": type})
|
question, fields = _get_fields_and_class_from_type(type)
|
||||||
fields = _get_fields_from_type(type)
|
res.append({"type": question})
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if field in offset:
|
if field in offset:
|
||||||
offset[field] += 1
|
offset[field] += 1
|
||||||
else:
|
else:
|
||||||
offset[field] = 0
|
offset[field] = 0
|
||||||
value = values.getlist(field)[offset[field]]
|
value = values.getlist(field)[offset[field]]
|
||||||
|
if field == "required":
|
||||||
|
value = True if value != "off" else False
|
||||||
res[i][field] = value
|
res[i][field] = value
|
||||||
return res
|
return res
|
||||||
|
|
8
akarpov/test_platform/services/generators.py
Normal file
8
akarpov/test_platform/services/generators.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from akarpov.test_platform.models import Form
|
||||||
|
|
||||||
|
|
||||||
|
def generate_form_question(form: Form) -> str:
|
||||||
|
res = ""
|
||||||
|
for i, question in enumerate(form.fields.all()):
|
||||||
|
res += question.generate_html(i)
|
||||||
|
return res
|
6
akarpov/test_platform/templates/fields/base.html
Normal file
6
akarpov/test_platform/templates/fields/base.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<div order="{{ order }}" class="border-3 p-4">
|
||||||
|
<h1>{{ question.question }}</h1>
|
||||||
|
<p>{{ question.help }}</p>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
7
akarpov/test_platform/templates/fields/number.html
Normal file
7
akarpov/test_platform/templates/fields/number.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "fields/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
<input name="{{ order }}/{{ question.type }}" class="form-control" placeholder="Введите число" type="number" required="{{ question.required }}">
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
7
akarpov/test_platform/templates/fields/range.html
Normal file
7
akarpov/test_platform/templates/fields/range.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "fields/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
<input name="{{ order }}/{{ question.type }}" class="form-control" placeholder="Введите число" type="number" required="{{ question.required }}">
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
7
akarpov/test_platform/templates/fields/text.html
Normal file
7
akarpov/test_platform/templates/fields/text.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "fields/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
<input name="{{ order }}/{{ question.type }}" class="form-control" placeholder="Введите ответ" type="text" required="{{ question.required }}">
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
|
@ -1,6 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from akarpov.test_platform.views import from_create_view
|
from akarpov.test_platform.views import form_create_view, form_view
|
||||||
|
|
||||||
app_name = "test_platform"
|
app_name = "test_platform"
|
||||||
urlpatterns = [path("create", from_create_view, name="create")]
|
urlpatterns = [
|
||||||
|
path("create", form_create_view, name="create"),
|
||||||
|
path("<str:slug>", form_view, name="view"),
|
||||||
|
]
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.views.generic import CreateView
|
from django.views.generic import CreateView, DetailView
|
||||||
|
|
||||||
from akarpov.test_platform.forms import FormFormClass
|
from akarpov.test_platform.forms import FormFormClass
|
||||||
from akarpov.test_platform.models import Form
|
from akarpov.test_platform.models import BaseQuestion, Form
|
||||||
from akarpov.test_platform.services.forms import get_question_types, parse_form_create
|
from akarpov.test_platform.services.forms import get_question_types, parse_form_create
|
||||||
|
from akarpov.test_platform.services.generators import generate_form_question
|
||||||
|
|
||||||
|
|
||||||
class FromCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
class FromCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||||
|
@ -20,8 +21,27 @@ def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.creator = self.request.user
|
form.instance.creator = self.request.user
|
||||||
print(parse_form_create(self.request.POST))
|
s = super().form_valid(form)
|
||||||
return super().form_valid(form)
|
fields = parse_form_create(self.request.POST)
|
||||||
|
for i, field in enumerate(fields):
|
||||||
|
question: BaseQuestion = field["type"]
|
||||||
|
field.pop("type")
|
||||||
|
question.objects.create(**field, order=i, form_id=form.instance.id)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
from_create_view = FromCreateView.as_view()
|
form_create_view = FromCreateView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class FormView(DetailView):
|
||||||
|
template_name = "test_platform/view.html"
|
||||||
|
model = Form
|
||||||
|
slug_field = "slug"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["form"] = generate_form_question(self.object)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
form_view = FormView.as_view()
|
||||||
|
|
24
akarpov/utils/channels.py
Normal file
24
akarpov/utils/channels.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
if not self.scope.get("user", False) or not self.scope["user"].is_authenticated:
|
||||||
|
self.send_error("Требуется авторизация")
|
||||||
|
else:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConsumer(AsyncJsonWebsocketConsumer):
|
||||||
|
async def send_error(self, msg):
|
||||||
|
await self.send_json({"type": "error", "data": {"msg": msg}})
|
||||||
|
|
||||||
|
|
||||||
|
class SyncBaseConsumer(JsonWebsocketConsumer):
|
||||||
|
def send_error(self, msg):
|
||||||
|
self.send_json({"type": "error", "data": {"msg": msg}})
|
9
akarpov/utils/choices.py
Normal file
9
akarpov/utils/choices.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from django.db.models.enums import ChoicesMeta
|
||||||
|
|
||||||
|
|
||||||
|
def count_max_length(choices: Iterable | ChoicesMeta):
|
||||||
|
if isinstance(choices, ChoicesMeta):
|
||||||
|
return max([len(val) for val in choices.values])
|
||||||
|
return max([len(val) for val, _ in choices])
|
26
akarpov/utils/commands.py
Normal file
26
akarpov/utils/commands.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
from .query import batch_qs
|
||||||
|
|
||||||
|
|
||||||
|
def progress_tracker(
|
||||||
|
a_list: list, a_len: int = 0, step: int = 10, template: str = "Done %d/%d"
|
||||||
|
) -> Iterable:
|
||||||
|
if not a_len:
|
||||||
|
a_len = len(a_list)
|
||||||
|
for n, item in enumerate(a_list):
|
||||||
|
if n % step == 0:
|
||||||
|
print(template % (n, a_len))
|
||||||
|
yield item
|
||||||
|
|
||||||
|
|
||||||
|
def iterate_big_queryset(
|
||||||
|
qs: QuerySet, batch_size: int = 1000, step: int = 10
|
||||||
|
) -> Iterable:
|
||||||
|
return progress_tracker(
|
||||||
|
a_list=batch_qs(qs, batch_size=batch_size),
|
||||||
|
a_len=qs.count(),
|
||||||
|
step=step,
|
||||||
|
)
|
29
akarpov/utils/config.py
Normal file
29
akarpov/utils/config.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
def build_redis_uri(
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
username: str | None = None,
|
||||||
|
password: str | None = None,
|
||||||
|
db: str | None = None,
|
||||||
|
):
|
||||||
|
return build_conn_uri("redis", host, port, username, password, db)
|
||||||
|
|
||||||
|
|
||||||
|
def build_conn_uri(
|
||||||
|
proto: str,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
username: str | None = None,
|
||||||
|
password: str | None = None,
|
||||||
|
db: str | None = None,
|
||||||
|
):
|
||||||
|
creds_part = ""
|
||||||
|
if username or password:
|
||||||
|
if username:
|
||||||
|
creds_part = username
|
||||||
|
if password:
|
||||||
|
creds_part += f":{password}"
|
||||||
|
creds_part += "@"
|
||||||
|
uri = f"{proto}://{creds_part}{host}:{port}"
|
||||||
|
if db:
|
||||||
|
uri += f"/{db}"
|
||||||
|
return uri
|
16
akarpov/utils/factory.py
Normal file
16
akarpov/utils/factory.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import factory
|
||||||
|
|
||||||
|
|
||||||
|
class M2MPostAddition(factory.PostGeneration):
|
||||||
|
def __init__(self, rel_name):
|
||||||
|
self.rel_name = rel_name
|
||||||
|
super().__init__(self.handler)
|
||||||
|
|
||||||
|
def handler(self, obj, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
rel_manager = getattr(obj, self.rel_name)
|
||||||
|
for item in extracted:
|
||||||
|
rel_manager.add(item)
|
22
akarpov/utils/faker.py
Normal file
22
akarpov/utils/faker.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from decimal import Decimal as D
|
||||||
|
|
||||||
|
from faker.providers.python import Provider as PythonProvider
|
||||||
|
|
||||||
|
|
||||||
|
class MoneyProvider(PythonProvider):
|
||||||
|
def money(self):
|
||||||
|
return self.pydecimal(right_digits=2, min_value=D(1), max_value=D(1000))
|
||||||
|
|
||||||
|
|
||||||
|
additional_providers = [MoneyProvider]
|
||||||
|
|
||||||
|
|
||||||
|
def configure_factory_faker(factory_faker):
|
||||||
|
factory_faker._DEFAULT_LOCALE = "ru_RU"
|
||||||
|
for provider in additional_providers:
|
||||||
|
factory_faker.add_provider(provider, locale="ru_RU")
|
||||||
|
|
||||||
|
|
||||||
|
def configure_faker(faker):
|
||||||
|
for provider in additional_providers:
|
||||||
|
faker.add_provider(provider)
|
11
akarpov/utils/forms.py
Normal file
11
akarpov/utils/forms.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
def set_field_html_name(cls, new_name):
|
||||||
|
"""
|
||||||
|
This creates wrapper around the normal widget rendering,
|
||||||
|
allowing for a custom field name (new_name).
|
||||||
|
"""
|
||||||
|
old_render = cls.widget.render
|
||||||
|
|
||||||
|
def _widget_render_wrapper(name, value, attrs=None):
|
||||||
|
return old_render(new_name, value, attrs)
|
||||||
|
|
||||||
|
cls.widget.render = _widget_render_wrapper
|
8
akarpov/utils/log.py
Normal file
8
akarpov/utils/log.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class RequireDbDebugTrue(logging.Filter):
|
||||||
|
def filter(self, record) -> bool:
|
||||||
|
return settings.DB_DEBUG
|
8
akarpov/utils/query.py
Normal file
8
akarpov/utils/query.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
|
||||||
|
def batch_qs(qs: QuerySet, batch_size: int = 1000):
|
||||||
|
total = qs.count()
|
||||||
|
for start in range(0, total, batch_size):
|
||||||
|
end = min(start + batch_size, total)
|
||||||
|
yield from qs[start:end]
|
78
akarpov/utils/storage.py
Normal file
78
akarpov/utils/storage.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import gzip
|
||||||
|
import mimetypes
|
||||||
|
import shutil
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django_s3_storage.storage import (
|
||||||
|
_UNCOMPRESSED_SIZE_META_KEY,
|
||||||
|
S3Storage,
|
||||||
|
_wrap_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FixedS3Storage(S3Storage):
|
||||||
|
"""
|
||||||
|
Стандартный S3Storage использует boto3 для загрузки файла, который закрывает переданный
|
||||||
|
content-файл. Однако imagekit должен повторно его использовать, поэтому нужно обернуть
|
||||||
|
переданный контент во временный файл.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@_wrap_errors
|
||||||
|
def _save(self, name, content):
|
||||||
|
put_params = self._object_put_params(name)
|
||||||
|
temp_files = []
|
||||||
|
# The Django file storage API always rewinds the file before saving,
|
||||||
|
# therefor so should we.
|
||||||
|
content.seek(0)
|
||||||
|
# Convert content to bytes.
|
||||||
|
temp_file = self.new_temporary_file()
|
||||||
|
temp_files.append(temp_file)
|
||||||
|
for chunk in content.chunks():
|
||||||
|
temp_file.write(force_bytes(chunk))
|
||||||
|
temp_file.seek(0)
|
||||||
|
content = temp_file
|
||||||
|
# Calculate the content type.
|
||||||
|
content_type, _ = mimetypes.guess_type(name, strict=False)
|
||||||
|
content_type = content_type or "application/octet-stream"
|
||||||
|
put_params["ContentType"] = content_type
|
||||||
|
# Calculate the content encoding.
|
||||||
|
if self.settings.AWS_S3_GZIP:
|
||||||
|
# Check if the content type is compressible.
|
||||||
|
content_type_family, content_type_subtype = content_type.lower().split("/")
|
||||||
|
content_type_subtype = content_type_subtype.split("+")[-1]
|
||||||
|
if content_type_family == "text" or content_type_subtype in (
|
||||||
|
"xml",
|
||||||
|
"json",
|
||||||
|
"html",
|
||||||
|
"javascript",
|
||||||
|
):
|
||||||
|
# Compress the content.
|
||||||
|
temp_file = self.new_temporary_file()
|
||||||
|
temp_files.append(temp_file)
|
||||||
|
with closing(gzip.GzipFile(name, "wb", 9, temp_file)) as gzip_file:
|
||||||
|
shutil.copyfileobj(content, gzip_file)
|
||||||
|
# Only use the compressed version if the zipped version is actually smaller!
|
||||||
|
orig_size = content.tell()
|
||||||
|
if temp_file.tell() < orig_size:
|
||||||
|
temp_file.seek(0)
|
||||||
|
content = temp_file
|
||||||
|
put_params["ContentEncoding"] = "gzip"
|
||||||
|
put_params["Metadata"][_UNCOMPRESSED_SIZE_META_KEY] = "{:d}".format(
|
||||||
|
orig_size
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content.seek(0)
|
||||||
|
# Save the file.
|
||||||
|
self.s3_connection.upload_fileobj(
|
||||||
|
content,
|
||||||
|
put_params.pop("Bucket"),
|
||||||
|
put_params.pop("Key"),
|
||||||
|
ExtraArgs=put_params,
|
||||||
|
Config=self._transfer_config,
|
||||||
|
)
|
||||||
|
# Close all temp files.
|
||||||
|
for temp_file in temp_files:
|
||||||
|
temp_file.close()
|
||||||
|
# All done!
|
||||||
|
return name
|
20
akarpov/utils/validators.py
Normal file
20
akarpov/utils/validators.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from upload_validator import FileTypeValidator
|
||||||
|
|
||||||
|
validate_excel = FileTypeValidator(
|
||||||
|
allowed_types=[
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/zip",
|
||||||
|
],
|
||||||
|
allowed_extensions=[".xlsx"],
|
||||||
|
)
|
||||||
|
|
||||||
|
validate_zip = FileTypeValidator(
|
||||||
|
allowed_types=[
|
||||||
|
"application/zip",
|
||||||
|
"application/octet-stream",
|
||||||
|
"application/x-zip-compressed",
|
||||||
|
"multipart/x-zip",
|
||||||
|
],
|
||||||
|
allowed_extensions=[".zip"],
|
||||||
|
)
|
15
akarpov/utils/zip_field.py
Normal file
15
akarpov/utils/zip_field.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .validators import validate_zip
|
||||||
|
|
||||||
|
|
||||||
|
class ZipfileField(forms.FileField):
|
||||||
|
file_validators = [validate_zip]
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
for validator in self.file_validators:
|
||||||
|
validator(value)
|
||||||
|
return zipfile.ZipFile(value)
|
49
poetry.lock
generated
49
poetry.lock
generated
|
@ -445,6 +445,26 @@ files = [
|
||||||
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "channels"
|
||||||
|
version = "4.0.0"
|
||||||
|
description = "Brings async, event-driven capabilities to Django 3.2 and up."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "channels-4.0.0-py3-none-any.whl", hash = "sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4"},
|
||||||
|
{file = "channels-4.0.0.tar.gz", hash = "sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asgiref = ">=3.5.0,<4"
|
||||||
|
Django = ">=3.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
daphne = ["daphne (>=4.0.0)"]
|
||||||
|
tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.0.1"
|
version = "3.0.1"
|
||||||
|
@ -1348,6 +1368,21 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Django = ">=3.2"
|
Django = ">=3.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-upload-validator"
|
||||||
|
version = "1.1.6"
|
||||||
|
description = "A simple Django file type validator using python-magic"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "django-upload-validator-1.1.6.tar.gz", hash = "sha256:dbf93740fb42f8c9310b936838bbf99f873339c2f860de1c7c0d7a27464f7c8a"},
|
||||||
|
{file = "django_upload_validator-1.1.6-py3-none-any.whl", hash = "sha256:b4c1a2d2138b319288a20069c5dbdbbdb1257a447ec2f0b4e6cf9e8d6fd3d1bc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
python-magic = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangocms-admin-style"
|
name = "djangocms-admin-style"
|
||||||
version = "3.2.3"
|
version = "3.2.3"
|
||||||
|
@ -2921,6 +2956,18 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
six = ">=1.5"
|
six = ">=1.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-magic"
|
||||||
|
version = "0.4.27"
|
||||||
|
description = "File type identification using libmagic"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
files = [
|
||||||
|
{file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"},
|
||||||
|
{file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-slugify"
|
name = "python-slugify"
|
||||||
version = "7.0.0"
|
version = "7.0.0"
|
||||||
|
@ -3793,4 +3840,4 @@ files = [
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "47d4a706ed11e7da8599131523b32f44c6f9703559efd8b4f00245abd40a4a68"
|
content-hash = "eb14567f6a4fa6a89dce0c68e386ca5d09e26a1e823a3d11eeaf387aae0eba9f"
|
||||||
|
|
|
@ -69,6 +69,8 @@ amzqr = "^0.0.1"
|
||||||
django-chunked-upload = "^2.0.0"
|
django-chunked-upload = "^2.0.0"
|
||||||
drf-chunked-upload = "^0.5.1"
|
drf-chunked-upload = "^0.5.1"
|
||||||
django-active-link = "^0.1.8"
|
django-active-link = "^0.1.8"
|
||||||
|
channels = "^4.0.0"
|
||||||
|
django-upload-validator = "^1.1.6"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user