mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 13:16:33 +03:00
add qr api. processing, minor bug fixes
This commit is contained in:
parent
19e72eaaf3
commit
5cc34d055e
|
@ -6,6 +6,7 @@
|
|||
|
||||
from akarpov.users.models import User
|
||||
from akarpov.utils.files import user_file_upload_mixin
|
||||
from utils.string import cleanhtml
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
|
@ -39,6 +40,7 @@ def get_comments(self):
|
|||
return self.comments.all()
|
||||
|
||||
def h_tags(self):
|
||||
# TODO: add caching here
|
||||
tags = (
|
||||
Tag.objects.all()
|
||||
.annotate(num_posts=Count("posts"))
|
||||
|
@ -50,6 +52,16 @@ def h_tags(self):
|
|||
def h_tag(self):
|
||||
return self.h_tags().first()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
# TODO: add caching here
|
||||
return cleanhtml(self.body)
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
body = self.text
|
||||
return body[:100] + "..." if len(body) > 100 else ""
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("blog:post", kwargs={"slug": self.slug})
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% load humanize %}
|
||||
{% load static %}
|
||||
{% load humanize static %}
|
||||
|
||||
{% block title %}posts on akarpov{% endblock %}
|
||||
|
||||
|
@ -16,7 +15,7 @@
|
|||
<strong style="color: {{ post.h_tag.color }}" class="d-inline-block mb-2">{{ post.h_tag.name }}</strong>
|
||||
<h3 class="mb-0">{{ post.title }}</h3>
|
||||
<p class="card-text"><small class="text-muted">{{ post.edited | naturaltime }}</small></p>
|
||||
<p class="card-text mb-auto">This is a wider card with supporting text below as a natural lead-in to additional content.</p>
|
||||
<p class="card-text mb-auto">{{ post.summary }}</p>
|
||||
<a href="{% url 'blog:post' post.slug %}" class="stretched-link"></a>
|
||||
<p class="card-text mt-4">{{ post.get_rating }}
|
||||
<i class="bi bi-eye ms-3"></i>{{ post.post_views }}
|
||||
|
|
20
akarpov/templates/tools/qr/create.html
Normal file
20
akarpov/templates/tools/qr/create.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Qr generator on akarpov.ru{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form class="pt-2" enctype="multipart/form-data" method="POST" id="designer-form">
|
||||
{% csrf_token %}
|
||||
{{ form.media }}
|
||||
{% for field in form %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endfor %}
|
||||
<div class="mt-4 flex justify-end space-x-4">
|
||||
<button class="btn btn-secondary" type="submit" id="submit">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ToolsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "akarpov.tools"
|
0
akarpov/tools/qr/api/__init__.py
Normal file
0
akarpov/tools/qr/api/__init__.py
Normal file
20
akarpov/tools/qr/api/serializers.py
Normal file
20
akarpov/tools/qr/api/serializers.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from akarpov.tools.qr.models import QR
|
||||
from akarpov.tools.qr.services import simple
|
||||
|
||||
|
||||
class QRSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = QR
|
||||
fields = ["id", "body", "image"]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"body": {"write_only": True},
|
||||
"image": {"read_only": True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
user = self.context["request"].user.is_authenticated if self.context["request"].user else None
|
||||
qr = simple.run(words=validated_data["body"], user=user)
|
||||
return qr
|
9
akarpov/tools/qr/api/urls.py
Normal file
9
akarpov/tools/qr/api/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from rest_framework.routers import SimpleRouter
|
||||
|
||||
from .views import QRViewSet
|
||||
|
||||
router = SimpleRouter()
|
||||
router.register(r"", QRViewSet, basename="")
|
||||
|
||||
app_name = "qr"
|
||||
urlpatterns = router.urls
|
21
akarpov/tools/qr/api/views.py
Normal file
21
akarpov/tools/qr/api/views.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
from akarpov.tools.qr.models import QR
|
||||
from .serializers import QRSerializer
|
||||
|
||||
|
||||
class QRViewSet(generics.ListCreateAPIView, generics.RetrieveAPIView, GenericViewSet):
|
||||
serializer_class = QRSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get_object(self):
|
||||
print(self.kwargs)
|
||||
return get_object_or_404(QR, pk=self.kwargs["pk"])
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_authenticated:
|
||||
return QR.objects.filter(user=self.request.user)
|
||||
return QR.objects.none()
|
|
@ -1,6 +1,7 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class QRConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
verbose_name = _("QR generator")
|
||||
name = "akarpov.tools.qr"
|
||||
|
|
11
akarpov/tools/qr/forms.py
Normal file
11
akarpov/tools/qr/forms.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django import forms
|
||||
|
||||
from akarpov.tools.qr.models import QR
|
||||
|
||||
|
||||
class QRForm(forms.ModelForm):
|
||||
body = forms.CharField()
|
||||
|
||||
class Meta:
|
||||
model = QR
|
||||
fields = ["body"]
|
12
akarpov/tools/qr/migrations/0001_initial.py
Normal file
12
akarpov/tools/qr/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-07 14:36
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = []
|
44
akarpov/tools/qr/migrations/0002_initial.py
Normal file
44
akarpov/tools/qr/migrations/0002_initial.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-10 20:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("qr", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="QR",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("body", models.TextField()),
|
||||
("image", models.ImageField(upload_to="")),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="generated_qrs",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
akarpov/tools/qr/migrations/__init__.py
Normal file
0
akarpov/tools/qr/migrations/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class QR(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
body = models.TextField()
|
||||
image = models.ImageField()
|
||||
user = models.ForeignKey(
|
||||
"users.User", related_name="generated_qrs", blank=True, on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"qr {self.body}"
|
0
akarpov/tools/qr/services/__init__.py
Normal file
0
akarpov/tools/qr/services/__init__.py
Normal file
35
akarpov/tools/qr/services/simple.py
Normal file
35
akarpov/tools/qr/services/simple.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import os
|
||||
|
||||
from amzqr import amzqr
|
||||
from django.core.files import File
|
||||
|
||||
from akarpov.tools.qr.models import QR
|
||||
from akarpov.utils.generators import generate_charset
|
||||
from akarpov.users.models import User
|
||||
|
||||
|
||||
def run(words: str, path: str = "/tmp/", user: User = None) -> QR:
|
||||
version, level, qr_name = amzqr.run(
|
||||
words,
|
||||
version=1,
|
||||
level="H",
|
||||
picture=None,
|
||||
colorized=False,
|
||||
contrast=1.0,
|
||||
brightness=1.0,
|
||||
save_name=generate_charset(4) + ".png",
|
||||
save_dir=path,
|
||||
)
|
||||
qr = QR(body=words)
|
||||
if user:
|
||||
qr.user = user
|
||||
|
||||
path = qr_name
|
||||
with open(path, "rb") as f:
|
||||
qr.image.save(
|
||||
qr_name.split("/")[-1],
|
||||
File(f),
|
||||
save=False,
|
||||
)
|
||||
os.remove(path)
|
||||
return qr
|
|
@ -0,0 +1,6 @@
|
|||
from django.urls import path
|
||||
|
||||
from akarpov.tools.qr.views import qr_create_view
|
||||
|
||||
app_name = "qr"
|
||||
urlpatterns = [path("", qr_create_view, name="create")]
|
|
@ -0,0 +1,18 @@
|
|||
from django.views.generic import CreateView
|
||||
|
||||
from akarpov.tools.qr.forms import QRForm
|
||||
from akarpov.tools.qr.models import QR
|
||||
|
||||
|
||||
class QRCreateView(CreateView):
|
||||
model = QR
|
||||
form_class = QRForm
|
||||
|
||||
template_name = "tools/qr/create.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user if self.request.user else None
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
qr_create_view = QRCreateView.as_view()
|
|
@ -0,0 +1,4 @@
|
|||
from django.urls import path, include
|
||||
|
||||
app_name = "tools"
|
||||
urlpatterns = [path("qr/", include("akarpov.tools.qr.urls", namespace="qr"))]
|
26
akarpov/users/api/urls.py
Normal file
26
akarpov/users/api/urls.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django.urls import path
|
||||
from .views import (
|
||||
UserListViewSet,
|
||||
UserRetireUpdateSelfViewSet,
|
||||
UserRetrieveIdViewSet,
|
||||
UserRetrieveViewSet,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", UserListViewSet.as_view(), name="user_list_api"),
|
||||
path(
|
||||
"self/",
|
||||
UserRetireUpdateSelfViewSet.as_view(),
|
||||
name="user_get_update_delete_self_api",
|
||||
),
|
||||
path(
|
||||
"id/<int:pk>",
|
||||
UserRetrieveIdViewSet.as_view(),
|
||||
name="user_retrieve_id_api",
|
||||
),
|
||||
path(
|
||||
"<str:username>",
|
||||
UserRetrieveViewSet.as_view(),
|
||||
name="user_retrieve_username_api",
|
||||
),
|
||||
]
|
|
@ -87,15 +87,3 @@ class UserRetireUpdateSelfViewSet(generics.RetrieveUpdateDestroyAPIView):
|
|||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
return self.partial_update(request, *args, **kwargs)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
return self.destroy(request, *args, **kwargs)
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
from akarpov.users.models import User
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_user_create(user: User):
|
||||
password = "123"
|
||||
user.set_password(password)
|
||||
assert user.check_password(password)
|
||||
class TestUser:
|
||||
@pytest.fixture
|
||||
def user_with_code(self, user_factory):
|
||||
return user_factory(user_code="1234")
|
||||
|
||||
def test_send_code(self, mailoutbox, user_factory):
|
||||
user_with_code.send_code()
|
||||
|
||||
assert len(mailoutbox) == 1
|
||||
m = mailoutbox[0]
|
||||
assert list(m.to) == [user_with_code.email]
|
||||
assert "1234" in m.body
|
||||
|
|
8
akarpov/utils/string.py
Normal file
8
akarpov/utils/string.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import re
|
||||
|
||||
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
|
||||
|
||||
|
||||
def cleanhtml(raw_html):
|
||||
cleantext = re.sub(CLEANR, "", raw_html)
|
||||
return cleantext
|
|
@ -2,11 +2,7 @@
|
|||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
|
||||
from akarpov.users.api.views import (
|
||||
UserListViewSet,
|
||||
UserRegisterViewSet,
|
||||
UserRetireUpdateSelfViewSet,
|
||||
UserRetrieveIdViewSet,
|
||||
UserRetrieveViewSet,
|
||||
)
|
||||
|
||||
urlpatterns_v1 = [
|
||||
|
@ -23,26 +19,11 @@
|
|||
),
|
||||
path(
|
||||
"users/",
|
||||
include(
|
||||
[
|
||||
path("", UserListViewSet.as_view(), name="user_list_api"),
|
||||
path(
|
||||
"self/",
|
||||
UserRetireUpdateSelfViewSet.as_view(),
|
||||
name="user_get_update_delete_self_api",
|
||||
include("akarpov.users.api.urls"),
|
||||
),
|
||||
path(
|
||||
"id/<int:pk>",
|
||||
UserRetrieveIdViewSet.as_view(),
|
||||
name="user_retrieve_id_api",
|
||||
),
|
||||
path(
|
||||
"<str:username>",
|
||||
UserRetrieveViewSet.as_view(),
|
||||
name="user_retrieve_username_api",
|
||||
),
|
||||
]
|
||||
),
|
||||
"tools/",
|
||||
include([path("qr/", include("akarpov.tools.qr.api.urls"))]),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -135,12 +135,7 @@
|
|||
# "allauth.socialaccount.providers.yandex",
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
"akarpov.users",
|
||||
"akarpov.blog",
|
||||
"akarpov.pipeliner"
|
||||
# Your stuff: custom apps go here
|
||||
]
|
||||
LOCAL_APPS = ["akarpov.users", "akarpov.blog", "akarpov.pipeliner", "akarpov.tools.qr"]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = (
|
||||
DJANGO_APPS + LOCAL_APPS + THIRD_PARTY_APPS + HEALTH_CHECKS + ALL_AUTH_PROVIDERS
|
||||
|
@ -453,9 +448,10 @@
|
|||
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "akarpov API",
|
||||
"SCHEMA_PATH_PREFIX": "/api/v[0-9]",
|
||||
"DESCRIPTION": "Documentation of API endpoints of akarpov",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SERVERS": [
|
||||
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
|
||||
{"url": "https://akarpov.ru", "description": "Production server"},
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
path(settings.ADMIN_URL, admin.site.urls),
|
||||
# User management
|
||||
path("users/", include("akarpov.users.urls", namespace="users")),
|
||||
path("tools/", include("akarpov.tools.urls", namespace="tools")),
|
||||
path("ckeditor/", include("ckeditor_uploader.urls")),
|
||||
path("accounts/", include("allauth.urls")),
|
||||
path("", include("akarpov.blog.urls", namespace="blog")),
|
||||
|
@ -32,8 +33,8 @@
|
|||
# API base url
|
||||
path("api/", include("config.api_router")),
|
||||
# DRF auth token
|
||||
path("api_schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||
path("api_rschema/", SpectacularAPIView.as_view(), name="api-redoc-schema"),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-redoc-schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
SpectacularSwaggerView.as_view(url_name="api-schema"),
|
||||
|
|
4747
poetry.lock
generated
4747
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -65,6 +65,7 @@ django-extra-settings = "^0.7.0"
|
|||
psycopg2-binary = "^2.9.5"
|
||||
django-cms = "^3.11.1"
|
||||
django-sekizai = "^4.0.0"
|
||||
amzqr = "^0.0.1"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue
Block a user