mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-25 16:23: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>`
|
||||
|
||||
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