mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 07:26:33 +03:00
added shortcutter view meta
This commit is contained in:
parent
f0dd4e2b27
commit
5ab4501783
7
akarpov/static/js/clipboard.min.js
vendored
Normal file
7
akarpov/static/js/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -42,6 +42,7 @@
|
|||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row vh-100 overflow-auto">
|
||||
{# TODO: add cache for sidebar #}
|
||||
<div class="sidebar col-12 col-lg-2 col-sm-3 col-md-2 col-xl-1 px-sm-2 px-0 bg-dark d-flex sticky-top">
|
||||
<div class="d-flex flex-sm-column flex-row flex-grow-1 align-items-center align-items-sm-start px-3 pt-2 text-white">
|
||||
<a href="/" class="d-flex align-items-center pb-sm-3 mb-md-0 me-md-auto text-white text-wrap text-decoration-none">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block meta %}
|
||||
{% autoescape off %}
|
||||
|
@ -51,7 +52,7 @@
|
|||
<p>File is private</p>
|
||||
{% else %}
|
||||
<p>File is public{% if file.short_link %},
|
||||
<a href="{{ file.short_link.get_absolute_url }}">short link</a><button class="btn" data-clipboard-text="{{ request.get_host }}{{ file.short_link.get_absolute_url }}">
|
||||
<a href="{{ file.get_short_link }}">short link</a><button class="btn" data-clipboard-text="{{ request.get_host }}{{ file.get_short_link }}">
|
||||
<i style="font-size: 0.8em" class="bi bi-clipboard ml-2"></i>
|
||||
</button>
|
||||
{% endif %}</p>
|
||||
|
@ -68,7 +69,7 @@
|
|||
{{ preview_content }}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
new ClipboardJS('.btn');
|
||||
</script>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<form class="form-horizontal" enctype="multipart/form-data" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn btn-success">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
53
akarpov/templates/tools/shortener/list_create.html
Normal file
53
akarpov/templates/tools/shortener/list_create.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<form class="form-horizontal" enctype="multipart/form-data" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn btn-success">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row m-2 gap-5">
|
||||
{% for link in object_list %}
|
||||
<div class="col-auto card text-center {% if card.enabled == False %} opacity-25 {% endif %}">
|
||||
<div class="m-3">
|
||||
<h5 class="card-title">{{ link.full_source }}</h5>
|
||||
|
||||
<p class="mb-5">Viewed: {{ link.viewed }}</p>
|
||||
|
||||
<p>Created: {{ link.created | time:"H:i"}} {{ link.created | date:"d.m.Y"}}</p>
|
||||
<p>Updated: {{ link.modified | time:"H:i"}} {{ link.modified | date:"d.m.Y"}}</p>
|
||||
<a href="{{ link.get_absolute_url }}" class="stretched-link"></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="btn-group" role="group" aria-label="Item pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="btn btn-outline-warning">«</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page_number in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == page_number %}
|
||||
<button class="btn btn-outline-warning active">
|
||||
<span>{{ page_number }} <span class="sr-only">(current)</span></span>
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="?page={{ page_number }}" class="btn btn-outline-warning">
|
||||
{{ page_number }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="btn btn-outline-warning">»</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,6 +1,39 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{{ link }}
|
||||
<div class="m-2">
|
||||
<h4>Link to: <a class="fs-4" href="{{ link.source }}">{{ link.full_source }}</a></h4>
|
||||
<p>{{ request.get_host }}{% url 'short_url' slug=link.slug %} <button class="btn" data-clipboard-text="{{ request.get_host }}{% url 'short_url' slug=link.slug %}">
|
||||
<i style="font-size: 0.8em" class="bi bi-clipboard ml-2"></i>
|
||||
</button></p>
|
||||
<p>Viewed: {{ link.viewed }} times</p>
|
||||
<p>Recent views:</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">IP</th>
|
||||
<th scope="col">User Agent</th>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for view in views %}
|
||||
<tr>
|
||||
<td>{{ view.ip }}</td>
|
||||
<td>{{ view.user_agent }}</td>
|
||||
<td>{{ view.viewed | time:"H:i:s"}} {{ view.viewed | date:"d.m.Y"}}</td>
|
||||
<td>{% if view.user %}<a href="{{ view.user.get_absolute_url }}">{{ view.user.username }}</a>{% else %} - {% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
new ClipboardJS('.btn');
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.3 on 2023-08-04 13:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shortener", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="link",
|
||||
options={"ordering": ["-modified"]},
|
||||
),
|
||||
]
|
26
akarpov/tools/shortener/migrations/0003_linkviewmeta_user.py
Normal file
26
akarpov/tools/shortener/migrations/0003_linkviewmeta_user.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 4.2.3 on 2023-08-05 08:28
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("shortener", "0002_alter_link_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="linkviewmeta",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="link_views",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.3 on 2023-08-05 08:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shortener", "0003_linkviewmeta_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="linkviewmeta",
|
||||
options={"ordering": ["-viewed"]},
|
||||
),
|
||||
]
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
class Link(TimeStampedModel):
|
||||
source = models.URLField(blank=False)
|
||||
slug = models.SlugField(db_index=True)
|
||||
slug = models.SlugField(db_index=True, unique=False)
|
||||
creator = models.ForeignKey(
|
||||
"users.User", related_name="links", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
@ -17,12 +17,23 @@ class Link(TimeStampedModel):
|
|||
|
||||
viewed = models.IntegerField(default=0)
|
||||
|
||||
@property
|
||||
def full_source(self):
|
||||
return (
|
||||
"https://akarpov.ru" + self.source
|
||||
if self.source.startswith("/")
|
||||
else self.source
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("short_url", kwargs={"slug": self.slug})
|
||||
return reverse("tools:shortener:view", kwargs={"slug": self.slug})
|
||||
|
||||
def __str__(self):
|
||||
return f"link to {self.source}"
|
||||
|
||||
class Meta:
|
||||
ordering = ["-modified"]
|
||||
|
||||
|
||||
class LinkViewMeta(models.Model):
|
||||
# TODO: move to mem, delete within 7 days
|
||||
|
@ -31,6 +42,12 @@ class LinkViewMeta(models.Model):
|
|||
viewed = models.DateTimeField(auto_now_add=True)
|
||||
ip = models.GenericIPAddressField()
|
||||
user_agent = models.CharField(max_length=200)
|
||||
user = models.ForeignKey(
|
||||
"users.User", related_name="link_views", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-viewed"]
|
||||
|
||||
def __str__(self):
|
||||
return f"view on {self.link.source}"
|
||||
|
@ -38,6 +55,7 @@ def __str__(self):
|
|||
|
||||
def create_model_link(sender, instance, created, **kwargs):
|
||||
# had to move to models due to circular import
|
||||
# TODO: add link create to celery
|
||||
if created:
|
||||
link = Link(source=instance.get_absolute_url())
|
||||
if hasattr(instance, "private"):
|
||||
|
@ -112,7 +130,7 @@ def __init_subclass__(cls, **kwargs):
|
|||
|
||||
@abstractmethod
|
||||
def get_absolute_url(self):
|
||||
...
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def get_short_link(self) -> str:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from functools import lru_cache
|
||||
from secrets import compare_digest
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -28,3 +29,12 @@ def get_link_from_slug(slug: str, check_whole=True) -> Link | bool:
|
|||
return link
|
||||
except Link.DoesNotExist:
|
||||
return False
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_cached_link_source(slug: str) -> tuple[bool, bool] | tuple[str, int]:
|
||||
# TODO: add TTL here or update cache on link update
|
||||
link = get_link_from_slug(slug)
|
||||
if link:
|
||||
return link.source, link.pk
|
||||
return False, False
|
||||
|
|
15
akarpov/tools/shortener/tasks.py
Normal file
15
akarpov/tools/shortener/tasks.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from celery import shared_task
|
||||
|
||||
from akarpov.tools.shortener.models import Link, LinkViewMeta
|
||||
|
||||
|
||||
@shared_task
|
||||
def save_view_meta(pk, ip, user_agent, user_id):
|
||||
link = Link.objects.get(pk=pk)
|
||||
meta = LinkViewMeta(link=link, ip=ip, user_agent=user_agent)
|
||||
if user_id:
|
||||
meta.user_id = user_id
|
||||
meta.save()
|
||||
link.viewed += 1
|
||||
link.save(update_fields=["viewed"])
|
||||
return
|
|
@ -1,10 +1,11 @@
|
|||
from django.urls import path
|
||||
|
||||
from akarpov.tools.shortener.views import short_link_create_view
|
||||
from akarpov.tools.shortener.views import link_detail_view, short_link_create_view
|
||||
|
||||
app_name = "shortener"
|
||||
|
||||
urlpatterns = [
|
||||
path("", short_link_create_view, name="create"),
|
||||
path("<str:slug>", link_detail_view, name="view"),
|
||||
path("revoked", short_link_create_view, name="revoked"),
|
||||
]
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.http import HttpResponseNotFound, HttpResponseRedirect
|
||||
from django.views.generic import CreateView, DetailView, TemplateView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponseNotFound, HttpResponseRedirect
|
||||
from django.views.generic import CreateView, DetailView, ListView, TemplateView
|
||||
from ipware import get_client_ip
|
||||
|
||||
from akarpov.tools.shortener.forms import LinkForm
|
||||
from akarpov.tools.shortener.models import Link
|
||||
from akarpov.tools.shortener.services import get_link_from_slug
|
||||
from akarpov.tools.shortener.services import get_cached_link_source, get_link_from_slug
|
||||
from akarpov.tools.shortener.tasks import save_view_meta
|
||||
|
||||
|
||||
class ShortLinkCreateView(CreateView):
|
||||
class ShortLinkCreateListView(CreateView, ListView):
|
||||
model = Link
|
||||
form_class = LinkForm
|
||||
paginate_by = 50
|
||||
|
||||
template_name = "tools/shortener/create.html"
|
||||
template_name = "tools/shortener/list_create.html"
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_authenticated:
|
||||
return Link.objects.filter(creator=self.request.user)
|
||||
return Link.objects.none()
|
||||
|
||||
def form_valid(self, form):
|
||||
if self.request.user.is_authenticated:
|
||||
|
@ -19,21 +27,27 @@ def form_valid(self, form):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
short_link_create_view = ShortLinkCreateView.as_view()
|
||||
short_link_create_view = ShortLinkCreateListView.as_view()
|
||||
|
||||
|
||||
class LinkDetailView(DetailView):
|
||||
|
||||
template_name = "tools/shortener/view.html"
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
link = get_link_from_slug(self.kwargs["slug"])
|
||||
if not link:
|
||||
raise ObjectDoesNotExist
|
||||
raise Http404
|
||||
if self.request.user.is_superuser:
|
||||
return link
|
||||
if link.creator and link.creator != self.request.user:
|
||||
raise PermissionDenied
|
||||
return link
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["views"] = kwargs["object"].views.all().prefetch_related("user")
|
||||
return context
|
||||
|
||||
|
||||
link_detail_view = LinkDetailView.as_view()
|
||||
|
||||
|
@ -46,8 +60,21 @@ class LinkRevokedView(TemplateView):
|
|||
|
||||
|
||||
def redirect_view(request, slug):
|
||||
# TODO: move to faster framework, like FastApi
|
||||
link = get_link_from_slug(slug)
|
||||
# TODO: move to faster framework, like FastAPI
|
||||
link, pk = get_cached_link_source(slug)
|
||||
if not link:
|
||||
return HttpResponseNotFound("such link doesn't exist or has been revoked")
|
||||
return HttpResponseRedirect(link.source)
|
||||
ip, is_routable = get_client_ip(request)
|
||||
if request.user.is_authenticated:
|
||||
user_id = request.user.id
|
||||
else:
|
||||
user_id = None
|
||||
save_view_meta.apply_async(
|
||||
kwargs={
|
||||
"pk": pk,
|
||||
"ip": ip,
|
||||
"user_agent": request.META["HTTP_USER_AGENT"],
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
return HttpResponseRedirect(link)
|
||||
|
|
7
clipboard.min.js
vendored
Normal file
7
clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
poetry.lock
generated
15
poetry.lock
generated
|
@ -2860,10 +2860,13 @@ files = [
|
|||
{file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"},
|
||||
{file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"},
|
||||
{file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
|
||||
|
@ -2872,6 +2875,7 @@ files = [
|
|||
{file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
|
||||
|
@ -2891,6 +2895,7 @@ files = [
|
|||
{file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"},
|
||||
|
@ -2900,6 +2905,7 @@ files = [
|
|||
{file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"},
|
||||
|
@ -2909,6 +2915,7 @@ files = [
|
|||
{file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"},
|
||||
|
@ -2918,6 +2925,7 @@ files = [
|
|||
{file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
|
||||
|
@ -2928,13 +2936,16 @@ files = [
|
|||
{file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
|
||||
{file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
|
||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
|
||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
|
||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
|
||||
{file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
|
||||
|
@ -3534,6 +3545,7 @@ files = [
|
|||
{file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"},
|
||||
{file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"},
|
||||
{file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"},
|
||||
{file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"},
|
||||
{file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"},
|
||||
{file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"},
|
||||
{file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"},
|
||||
|
@ -3543,6 +3555,7 @@ files = [
|
|||
{file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"},
|
||||
{file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"},
|
||||
{file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"},
|
||||
{file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"},
|
||||
{file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"},
|
||||
{file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"},
|
||||
{file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"},
|
||||
|
@ -5891,4 +5904,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "d06b2a1ccbc8c661df8bc425ccab27516a6dfea1b550976d1338292b47ce8f83"
|
||||
content-hash = "7ba29eabdec9961c09b653bd8b10f0f2dfd8cab294233495ea61c1f6d4462b08"
|
||||
|
|
|
@ -93,6 +93,7 @@ tablib = "^3.4.0"
|
|||
django-location-field = "^2.7.0"
|
||||
pydantic = "^2.0.2"
|
||||
channels-redis = "^4.1.0"
|
||||
django-ipware = "^5.0.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue
Block a user