mirror of
				https://github.com/Alexander-D-Karpov/akarpov
				synced 2025-10-31 13:27:25 +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>` | ||||
| 
 | ||||
|   questionPrototype = ` | ||||
|   <div class="border p-4 mt-5 rounded-2"> | ||||
|   <input type="hidden" value="${type}" name="type" /> | ||||
|   {% for field in question_form %} | ||||
|       {{ field|as_crispy_field }} | ||||
|   {% endfor %} | ||||
|   <div class="row ms-2"> | ||||
|   <div class="col col-11"> | ||||
|     <input type="hidden" value="${type}" name="type" /> | ||||
|     {% for field in question_form %} | ||||
|         {{ 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> | ||||
|   ` | ||||
|   questions.push(questionPrototype) | ||||
| {% endfor %} | ||||
| 
 | ||||
| button.onclick = function () { | ||||
|     console.log(q_index) | ||||
|     let index = select.selectedIndex; | ||||
|     let div = questions[index]; | ||||
|     form.innerHTML += div; | ||||
|     let div = document.createElement("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; | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8"> | ||||
|   <title>Title</title> | ||||
| </head> | ||||
| <body> | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| </body> | ||||
| </html> | ||||
| {% block content %} | ||||
| <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 Meta: | ||||
|         model = BaseQuestion | ||||
|         fields = ["question", "help", "required"] | ||||
|         fields = ["question", "help"] | ||||
| 
 | ||||
| 
 | ||||
| class TextQuestionForm(BaseQuestionForm): | ||||
|  | @ -67,6 +67,11 @@ class Meta(BaseQuestionForm.Meta): | |||
| 
 | ||||
| 
 | ||||
| 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): | ||||
|         super(BaseQuestionForm, self).__init__(*args, **kwargs) | ||||
| 
 | ||||
|  | @ -75,6 +80,7 @@ class Meta(BaseQuestionForm.Meta): | |||
|         fields = BaseQuestionForm.Meta.fields + [ | ||||
|             "min_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 | ||||
| 
 | ||||
| from django.db import models | ||||
| from django.template.loader import render_to_string | ||||
| from django.urls import reverse | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | @ -41,7 +42,7 @@ def available(self) -> bool: | |||
| 
 | ||||
|     def get_absolute_url(self): | ||||
|         # TODO change to admin | ||||
|         return reverse("test_platform:create") | ||||
|         return reverse("test_platform:view", kwargs={"slug": self.slug}) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"form: {self.name}" | ||||
|  | @ -53,6 +54,7 @@ class BaseQuestion(PolymorphicModel, SubclassesMixin): | |||
|     form: Form = models.ForeignKey( | ||||
|         "test_platform.Form", related_name="fields", on_delete=models.CASCADE | ||||
|     ) | ||||
|     order = models.IntegerField(default=0) | ||||
|     question = models.CharField(max_length=250, blank=False) | ||||
|     help = models.CharField(max_length=200, blank=True) | ||||
|     required = models.BooleanField(default=True) | ||||
|  | @ -60,6 +62,14 @@ class BaseQuestion(PolymorphicModel, SubclassesMixin): | |||
|     def __str__(self): | ||||
|         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): | ||||
|     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]: | ||||
|         if question.type == type: | ||||
|             form = question_forms[question] | ||||
|             return form.Meta.fields | ||||
|             return question, form.Meta.fields + ["required"] | ||||
|     raise ValueError | ||||
| 
 | ||||
| 
 | ||||
|  | @ -39,17 +39,19 @@ def get_question_types() -> dict[BaseQuestion, BaseQuestionForm]: | |||
| 
 | ||||
| def parse_form_create(values) -> list[dict[str, str]]: | ||||
|     offset: dict[str, int] = {} | ||||
|     res: list[dict[str, str]] = [] | ||||
|     res: list[dict[str, str | bool]] = [] | ||||
|     question_amount = len(values.getlist("type")) | ||||
|     for i in range(question_amount): | ||||
|         type = values.getlist("type")[i] | ||||
|         res.append({"type": type}) | ||||
|         fields = _get_fields_from_type(type) | ||||
|         question, fields = _get_fields_and_class_from_type(type) | ||||
|         res.append({"type": question}) | ||||
|         for field in fields: | ||||
|             if field in offset: | ||||
|                 offset[field] += 1 | ||||
|             else: | ||||
|                 offset[field] = 0 | ||||
|             value = values.getlist(field)[offset[field]] | ||||
|             if field == "required": | ||||
|                 value = True if value != "off" else False | ||||
|             res[i][field] = value | ||||
|     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 akarpov.test_platform.views import from_create_view | ||||
| from akarpov.test_platform.views import form_create_view, form_view | ||||
| 
 | ||||
| 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.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.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.generators import generate_form_question | ||||
| 
 | ||||
| 
 | ||||
| class FromCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): | ||||
|  | @ -20,8 +21,27 @@ def get_context_data(self, **kwargs): | |||
| 
 | ||||
|     def form_valid(self, form): | ||||
|         form.instance.creator = self.request.user | ||||
|         print(parse_form_create(self.request.POST)) | ||||
|         return super().form_valid(form) | ||||
|         s = 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"}, | ||||
| ] | ||||
| 
 | ||||
| [[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]] | ||||
| name = "charset-normalizer" | ||||
| version = "3.0.1" | ||||
|  | @ -1348,6 +1368,21 @@ files = [ | |||
| [package.dependencies] | ||||
| 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]] | ||||
| name = "djangocms-admin-style" | ||||
| version = "3.2.3" | ||||
|  | @ -2921,6 +2956,18 @@ files = [ | |||
| [package.dependencies] | ||||
| 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]] | ||||
| name = "python-slugify" | ||||
| version = "7.0.0" | ||||
|  | @ -3793,4 +3840,4 @@ files = [ | |||
| [metadata] | ||||
| lock-version = "2.0" | ||||
| python-versions = "^3.11" | ||||
| content-hash = "47d4a706ed11e7da8599131523b32f44c6f9703559efd8b4f00245abd40a4a68" | ||||
| content-hash = "eb14567f6a4fa6a89dce0c68e386ca5d09e26a1e823a3d11eeaf387aae0eba9f" | ||||
|  |  | |||
|  | @ -69,6 +69,8 @@ amzqr = "^0.0.1" | |||
| django-chunked-upload = "^2.0.0" | ||||
| drf-chunked-upload = "^0.5.1" | ||||
| django-active-link = "^0.1.8" | ||||
| channels = "^4.0.0" | ||||
| django-upload-validator = "^1.1.6" | ||||
| 
 | ||||
| 
 | ||||
| [build-system] | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user