add qr api. processing, minor bug fixes

This commit is contained in:
Alexander Karpov 2023-01-11 01:06:08 +03:00
parent 19e72eaaf3
commit 5cc34d055e
28 changed files with 2721 additions and 2378 deletions

View File

@ -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})

View File

@ -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 }}

View 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 %}

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class ToolsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "akarpov.tools"

View File

View 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

View 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

View 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()

View File

@ -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
View 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"]

View 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 = []

View 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,
),
),
],
),
]

View File

View 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}"

View File

View 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

View File

@ -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")]

View File

@ -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()

View File

@ -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
View 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",
),
]

View File

@ -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)

View File

@ -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
View 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

View File

@ -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"))]),
),
]

View File

@ -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"},

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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]