mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2025-02-20 07:20:31 +03:00
Compare commits
9 Commits
b800e3854c
...
ab49c8261e
Author | SHA1 | Date | |
---|---|---|---|
|
ab49c8261e | ||
|
33125578a9 | ||
90f15db5e3 | |||
|
29f78393f4 | ||
e4bfd5ca07 | |||
59fc828097 | |||
403fb8ffa5 | |||
f6f15d3979 | |||
f59df63dd4 |
|
@ -6,3 +6,6 @@ USE_DOCKER=no
|
|||
EMAIL_HOST=127.0.0.1
|
||||
EMAIL_PORT=1025
|
||||
SENTRY_DSN=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_USER=
|
||||
EMAIL_USE_SSL=false
|
||||
|
|
|
@ -53,3 +53,4 @@ $ mypy --config-file setup.cfg akarpov
|
|||
- short link generator
|
||||
- about me app
|
||||
- gallery
|
||||
- notifications
|
||||
|
|
|
@ -10,8 +10,18 @@
|
|||
app_name = "music"
|
||||
|
||||
urlpatterns = [
|
||||
path("playlists/", ListCreatePlaylistAPIView.as_view()),
|
||||
path("playlists/<str:slug>", RetrieveUpdateDestroyPlaylistAPIView.as_view()),
|
||||
path("song/", ListCreateSongAPIView.as_view()),
|
||||
path("song/<str:slug>", RetrieveUpdateDestroySongAPIView.as_view()),
|
||||
path(
|
||||
"playlists/", ListCreatePlaylistAPIView.as_view(), name="list_create_playlist"
|
||||
),
|
||||
path(
|
||||
"playlists/<str:slug>",
|
||||
RetrieveUpdateDestroyPlaylistAPIView.as_view(),
|
||||
name="retrieve_update_delete_playlist",
|
||||
),
|
||||
path("song/", ListCreateSongAPIView.as_view(), name="list_create_song"),
|
||||
path(
|
||||
"song/<str:slug>",
|
||||
RetrieveUpdateDestroySongAPIView.as_view(),
|
||||
name="retrieve_update_delete_song",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
|
||||
<!-- Latest compiled and minified Bootstrap CSS -->
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if request.user.theme %}
|
||||
<link href="{{ request.user.theme.file.url }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css">
|
||||
<script src="https://kit.fontawesome.com/32fd82c823.js" crossorigin="anonymous"></script>
|
||||
<!-- Your stuff: Third-party CSS libraries go here -->
|
||||
|
@ -71,6 +76,7 @@
|
|||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdown">
|
||||
<li><a class="dropdown-item {% active_link 'tools:qr:create' %}" href="{% url 'tools:qr:create' %}">QR generator</a></li>
|
||||
<li><a class="dropdown-item {% active_link 'tools:uuid:main' %}" href="{% url 'tools:uuid:main' %}">UUID tools</a></li>
|
||||
<li><a class="dropdown-item {% active_link 'tools:shortener:create' %}" href="{% url 'tools:shortener:create' %}">URL shortcuter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
58
akarpov/templates/tools/uuid/main.html
Normal file
58
akarpov/templates/tools/uuid/main.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row align-items-center justify-content-center d-flex">
|
||||
<div class="col-lg-12 d-flex align-items-stretch m-3">
|
||||
<div style="width: 100%" class="card text-center">
|
||||
<form class="card-body row justify-content-end ml-5 mr-5" method="get">
|
||||
<div class="col-lg-11 col-sm-10">
|
||||
<label for="uuid">Lookup uuid info</label><input {% if uuid %}value="{{ uuid }}" {% endif %}name="uuid" class="form-control" id="uuid" type="text" placeholder="insert uuid" />
|
||||
</div>
|
||||
<div class="col-lg-1 col-sm-2">
|
||||
<button class="btn btn-success mt-4" type="submit"><i class="bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% if data %}
|
||||
<div class="col-lg-10">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">name</th>
|
||||
<th scope="col">value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, val in data.items %}
|
||||
<tr>
|
||||
<th scope="row">{{ key }}</th>
|
||||
<td>{{ val }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for token, version in tokens %}
|
||||
<div class="col-lg-5 d-flex align-items-stretch justify-content-center m-3">
|
||||
<div class="card text-center ml-1">
|
||||
<div class="card-header">
|
||||
<h4 class="bg-gray-300 ">{{ token }}<button class="btn" data-clipboard-text="{{ token }}">
|
||||
<i style="font-size: 0.8em" class="bi bi-clipboard ml-2"></i>
|
||||
</button></h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
Generated: {{ now }}, Version: {{ version }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
new ClipboardJS('.btn');
|
||||
</script>
|
||||
{% endblock %}
|
16
akarpov/templates/users/themes/create.html
Normal file
16
akarpov/templates/users/themes/create.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Create Theme{% endblock %}
|
||||
|
||||
{% 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-primary">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -8,10 +8,34 @@
|
|||
<form class="form-horizontal" enctype="multipart/form-data" method="post" action="{% url 'users:update' %}">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
{# Themes block #}
|
||||
<p class="mt-3 ml-3">Theme:</p>
|
||||
<div class="row">
|
||||
<label class="col-6 col-sm-4 col-md-3 gl-mb-5 text-center">
|
||||
<div style="background-color: white; height: 48px; border-radius: 4px; min-width: 112px; margin-bottom: 8px;"></div>
|
||||
<input {% if not request.user.theme %}checked{% endif %} type="radio" value="0" name="theme" id="user_theme_id_0">
|
||||
Default
|
||||
</label>
|
||||
{% for theme in themes %}
|
||||
<label class="col-6 col-sm-4 col-md-3 gl-mb-5 text-center">
|
||||
<div style="background-color: {{ theme.color }}; height: 48px; border-radius: 4px; min-width: 112px; margin-bottom: 8px;"></div>
|
||||
<input {% if request.user.theme_id == theme.id %}checked{% endif %} type="radio" value="{{ theme.id }}" name="theme" id="user_theme_id_{{ theme.id }}">
|
||||
{{ theme.name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% if request.user.is_superuser %}
|
||||
<label class="col-6 col-sm-4 col-md-3 gl-mb-5 text-center">
|
||||
<div style="background-color: white; height: 48px; border-radius: 4px; min-width: 112px; margin-bottom: 8px;"></div>
|
||||
<a href="{% url 'users:themes:create' %}">Create new</a>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
6
akarpov/tools/api/serializers.py
Normal file
6
akarpov/tools/api/serializers.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class URLPathSerializer(serializers.Serializer):
|
||||
path = serializers.URLField()
|
||||
kwargs = serializers.DictField(help_text="{'slug': 'str', 'pk': 'int'}")
|
68
akarpov/tools/api/services.py
Normal file
68
akarpov/tools/api/services.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from functools import lru_cache
|
||||
|
||||
from config import urls as urls_conf
|
||||
|
||||
urls = None
|
||||
|
||||
|
||||
def get_urls(urllist, name="") -> (list, list):
|
||||
res = []
|
||||
res_short = []
|
||||
for entry in urllist:
|
||||
if hasattr(entry, "url_patterns"):
|
||||
if entry.namespace != "admin":
|
||||
rres, rres_short = get_urls(
|
||||
entry.url_patterns,
|
||||
name + entry.namespace + ":" if entry.namespace else name,
|
||||
)
|
||||
res += rres
|
||||
res_short += rres_short
|
||||
else:
|
||||
res.append(
|
||||
(
|
||||
name + entry.pattern.name if entry.pattern.name else "",
|
||||
str(entry.pattern),
|
||||
)
|
||||
)
|
||||
res_short.append(
|
||||
(
|
||||
entry.pattern.name,
|
||||
str(entry.pattern),
|
||||
)
|
||||
)
|
||||
return res, res_short
|
||||
|
||||
|
||||
@lru_cache
|
||||
def urlpattern_to_js(pattern: str) -> (str, dict):
|
||||
if pattern.startswith("^"):
|
||||
return pattern
|
||||
res = ""
|
||||
kwargs = {}
|
||||
for p in pattern.split("<"):
|
||||
if ">" in p:
|
||||
rec = ""
|
||||
pn = p.split(">")
|
||||
k = pn[0].split(":")
|
||||
if len(k) == 1:
|
||||
rec = "{" + k[0] + "}"
|
||||
kwargs[k[0]] = "any"
|
||||
elif len(k) == 2:
|
||||
rec = "{" + k[1] + "}"
|
||||
kwargs[k[1]] = k[0]
|
||||
res += rec + pn[-1]
|
||||
else:
|
||||
res += p
|
||||
|
||||
return res, kwargs
|
||||
|
||||
|
||||
def get_api_path_by_url(name: str) -> tuple[str, dict] | None:
|
||||
global urls
|
||||
if not urls:
|
||||
urls, urls_short = get_urls(urls_conf.urlpatterns)
|
||||
urls = dict(urls_short) | dict(urls)
|
||||
|
||||
if name in urls:
|
||||
return urlpattern_to_js(urls[name])
|
||||
return None
|
|
@ -1,7 +1,10 @@
|
|||
from django.urls import include, path
|
||||
|
||||
from akarpov.tools.api.views import RetrieveAPIUrlAPIView
|
||||
|
||||
app_name = "tools"
|
||||
|
||||
urlpatterns = [
|
||||
path("<str:path>", RetrieveAPIUrlAPIView.as_view(), name="path"),
|
||||
path("qr/", include("akarpov.tools.qr.api.urls", namespace="qr")),
|
||||
]
|
||||
|
|
18
akarpov/tools/api/views.py
Normal file
18
akarpov/tools/api/views.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
from akarpov.tools.api.serializers import URLPathSerializer
|
||||
from akarpov.tools.api.services import get_api_path_by_url
|
||||
|
||||
|
||||
class RetrieveAPIUrlAPIView(generics.GenericAPIView):
|
||||
serializer_class = URLPathSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
path, k_args = get_api_path_by_url(self.kwargs["path"])
|
||||
if not path:
|
||||
raise NotFound
|
||||
return Response(data={"path": path, "kwargs": k_args})
|
|
@ -3,6 +3,7 @@
|
|||
app_name = "tools"
|
||||
urlpatterns = [
|
||||
path("qr/", include("akarpov.tools.qr.urls", namespace="qr")),
|
||||
path("uuid/", include("akarpov.tools.uuidtools.urls", namespace="uuid")),
|
||||
path(
|
||||
"promocodes/", include("akarpov.tools.promocodes.urls", namespace="promocodes")
|
||||
),
|
||||
|
|
0
akarpov/tools/uuidtools/__init__.py
Normal file
0
akarpov/tools/uuidtools/__init__.py
Normal file
5
akarpov/tools/uuidtools/apps.py
Normal file
5
akarpov/tools/uuidtools/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UuidtoolsConfig(AppConfig):
|
||||
name = "akarpov.tools.uuidtools"
|
5
akarpov/tools/uuidtools/forms.py
Normal file
5
akarpov/tools/uuidtools/forms.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class UUIDForm(forms.Form):
|
||||
token = forms.UUIDField()
|
0
akarpov/tools/uuidtools/migrations/__init__.py
Normal file
0
akarpov/tools/uuidtools/migrations/__init__.py
Normal file
56
akarpov/tools/uuidtools/services.py
Normal file
56
akarpov/tools/uuidtools/services.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
def uuid1_time_to_datetime(time: int):
|
||||
return datetime.datetime(1582, 10, 15) + datetime.timedelta(microseconds=time // 10)
|
||||
|
||||
|
||||
version_info = {
|
||||
1: """UUID Version 1 (Time-Based): The current time and the specific MAC address of the computer generating the
|
||||
UUID are used to construct IDs in UUID version 1. This means that even if they generate UUIDs simultaneously,
|
||||
separate machines are probably going to produce different ones. If your clock also knew your computer’s unique
|
||||
number, it would display a different number each time you glanced at it depending on the time and who you were.
|
||||
When generating a UUID that is specific to a certain computer and linked to the moment it was generated,
|
||||
this version is frequently utilised.""",
|
||||
2: """V2 UUIDs include the MAC address of the generator, lossy timestamp, and an account ID such as user ID or
|
||||
group ID on the local computer. Because of the information included in the UUID, there is limited space for
|
||||
randomness. The clock section of the UUID only advances every 429.47 seconds (~7 minutes). During any 7 minute
|
||||
period, there are only 64 available different UUIDs! """,
|
||||
3: """UUID Version 3 (Name-Based, Using MD5):The UUID version 3 uses MD5 hashing to create IDs.
|
||||
It uses the MD5 technique to combine the “namespace UUID” (a unique UUID) and the name you supply to get a unique
|
||||
identification. Imagine it as a secret recipe book where you add a secret ingredient (namespace UUID) to a standard
|
||||
ingredient (name), and when you combine them using a specific method (MD5), you get a unique dish (UUID).
|
||||
When you need to consistently produce UUIDs based on particular names, this variant is frequently utilised.""",
|
||||
4: """UUID Version 4 (Random): UUID version 4 uses random integers to create identifiers. It doesn’t depend on
|
||||
any particular details like names or times. Instead, it simply generates a slew of random numbers and characters.
|
||||
Imagine shaking a dice-filled box and then examining the face-up numbers that came out. It resembles receiving an
|
||||
unpredictable combination each time. When you just require a single unique identification without any kind of
|
||||
pattern or order, this version is fantastic.""",
|
||||
5: """UUID Version 5 (Name-Based, Using SHA-1): Similar to version 3, UUID version 5 generates identifiers using
|
||||
the SHA-1 algorithm rather than the MD5 technique. Similar to version 3, you provide it a “namespace UUID” and a
|
||||
name, and it uses the SHA-1 technique to combine them to get a unique identification. Consider it similar to
|
||||
baking a cake: you need a special pan (namespace UUID), your recipe (name), and a particular baking technique (
|
||||
SHA-1). No matter how many times you cook this recipe, you will always end up with a unique cake (UUID). Similar
|
||||
to version 3, UUID version 5 is frequently used in situations where a consistent and distinctive identity based
|
||||
on particular names is required. The better encryption capabilities of SHA-1 make its use preferred over MD5 in
|
||||
terms of security.""",
|
||||
}
|
||||
|
||||
|
||||
def decode_uuid(token: str) -> dict:
|
||||
data = {"token": token}
|
||||
try:
|
||||
uuid = UUID(token)
|
||||
except ValueError:
|
||||
return {"message": "not a valid UUID token"}
|
||||
data["version"] = f"{uuid.version}, {uuid.variant}"
|
||||
data["hex"] = uuid.hex
|
||||
data["bytes"] = bin(uuid.int)[2:]
|
||||
data["num"] = uuid.int
|
||||
if uuid.version == 1:
|
||||
data["time"] = uuid1_time_to_datetime(uuid.time)
|
||||
|
||||
if uuid.version in version_info:
|
||||
data["info"] = version_info[uuid.version]
|
||||
return data
|
9
akarpov/tools/uuidtools/urls.py
Normal file
9
akarpov/tools/uuidtools/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "uuidtools"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.MainView.as_view(), name="main"),
|
||||
]
|
28
akarpov/tools/uuidtools/views.py
Normal file
28
akarpov/tools/uuidtools/views.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import uuid
|
||||
|
||||
import uuid6
|
||||
from django.utils.timezone import now
|
||||
from django.views import generic
|
||||
|
||||
from akarpov.tools.uuidtools.services import decode_uuid
|
||||
|
||||
|
||||
class MainView(generic.TemplateView):
|
||||
template_name = "tools/uuid/main.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if "uuid" in self.request.GET:
|
||||
data = decode_uuid(str(self.request.GET["uuid"]))
|
||||
context["data"] = data
|
||||
context["uuid"] = self.request.GET["uuid"]
|
||||
|
||||
context["tokens"] = [
|
||||
(uuid.uuid4(), 4),
|
||||
(uuid6.uuid6(), 6),
|
||||
(uuid6.uuid8(), 8),
|
||||
(uuid.uuid1(), 1),
|
||||
]
|
||||
context["now"] = now()
|
||||
return context
|
|
@ -25,7 +25,7 @@ def validate_token(self, token):
|
|||
|
||||
class UserPublicInfoSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="api:users:user_retrieve_username_api", lookup_field="username"
|
||||
view_name="api:users:get", lookup_field="username"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
23
akarpov/users/migrations/0012_user_theme.py
Normal file
23
akarpov/users/migrations/0012_user_theme.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.6 on 2023-10-25 06:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("themes", "0001_initial"),
|
||||
("users", "0011_alter_userhistory_options_userhistory_created"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="theme",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="themes.theme",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -27,6 +27,7 @@ class User(AbstractUser, BaseImageModel, ShortLinkModel):
|
|||
left_file_upload = models.BigIntegerField(
|
||||
"Left file upload(in bites)", default=0, validators=[MinValueValidator(0)]
|
||||
)
|
||||
theme = models.ForeignKey("themes.Theme", null=True, on_delete=models.SET_NULL)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get url for user's detail view.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import pytest
|
||||
from django.urls import reverse_lazy
|
||||
from pytest_lambda import lambda_fixture, static_fixture
|
||||
from rest_framework import status
|
||||
|
@ -27,3 +28,74 @@ def test_return_err_if_data_is_invalid(
|
|||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
user.refresh_from_db()
|
||||
assert not user.check_password(new_password)
|
||||
|
||||
|
||||
class TestUserListRetrieve:
|
||||
url = static_fixture(reverse_lazy("api:users:list"))
|
||||
url_retrieve = static_fixture(
|
||||
reverse_lazy("api:users:get", kwargs={"username": "TestUser"})
|
||||
)
|
||||
user = lambda_fixture(
|
||||
lambda user_factory: user_factory(password="P@ssw0rd", username="TestUser")
|
||||
)
|
||||
|
||||
def test_user_list_site_users(self, api_user_client, url, user):
|
||||
response = api_user_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 1
|
||||
assert response.json()["results"][0]["username"] == user.username
|
||||
|
||||
def test_user_retrieve_by_username(self, api_user_client, url_retrieve, user):
|
||||
response = api_user_client.get(url_retrieve)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["username"] == user.username
|
||||
assert response.json()["id"] == user.id
|
||||
|
||||
def test_user_retrieve_by_id(self, api_user_client, user):
|
||||
response = api_user_client.get(
|
||||
reverse_lazy("api:users:get_by_id", kwargs={"pk": user.id})
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["username"] == user.username
|
||||
assert response.json()["id"] == user.id
|
||||
|
||||
|
||||
class TestUserSelfRetrieve:
|
||||
url = static_fixture(reverse_lazy("api:users:self"))
|
||||
user = lambda_fixture(lambda user_factory: user_factory(password="P@ssw0rd"))
|
||||
|
||||
def test_user_self_retrieve(self, api_user_client, url, user):
|
||||
response = api_user_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["username"] == user.username
|
||||
assert response.json()["id"] == user.id
|
||||
|
||||
def test_user_self_update_put(self, api_user_client, url, user):
|
||||
response = api_user_client.put(url, {"username": "NewUsername"})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["username"] == "NewUsername"
|
||||
assert response.json()["id"] == user.id
|
||||
user.refresh_from_db()
|
||||
assert user.username == "NewUsername"
|
||||
|
||||
def test_user_self_update_patch(self, api_user_client, url, user):
|
||||
response = api_user_client.patch(url, {"username": "NewUsername"})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["username"] == "NewUsername"
|
||||
assert response.json()["id"] == user.id
|
||||
user.refresh_from_db()
|
||||
assert user.username == "NewUsername"
|
||||
|
||||
def test_user_self_delete(self, api_user_client, url, user):
|
||||
response = api_user_client.delete(url)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert response.content == b""
|
||||
with pytest.raises(user.DoesNotExist):
|
||||
user.refresh_from_db()
|
||||
|
|
0
akarpov/users/themes/__init__.py
Normal file
0
akarpov/users/themes/__init__.py
Normal file
8
akarpov/users/themes/admin.py
Normal file
8
akarpov/users/themes/admin.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from akarpov.users.themes.models import Theme
|
||||
|
||||
|
||||
@admin.register(Theme)
|
||||
class ThemeAdmin(admin.ModelAdmin):
|
||||
...
|
6
akarpov/users/themes/apps.py
Normal file
6
akarpov/users/themes/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ThemesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "akarpov.users.themes"
|
9
akarpov/users/themes/forms.py
Normal file
9
akarpov/users/themes/forms.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django import forms
|
||||
|
||||
from akarpov.users.themes.models import Theme
|
||||
|
||||
|
||||
class ThemeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Theme
|
||||
fields = ["name", "file", "color"]
|
35
akarpov/users/themes/migrations/0001_initial.py
Normal file
35
akarpov/users/themes/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 4.2.6 on 2023-10-25 06:37
|
||||
|
||||
import colorfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Theme",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=250)),
|
||||
("file", models.FileField(upload_to="themes/")),
|
||||
(
|
||||
"color",
|
||||
colorfield.fields.ColorField(
|
||||
default="#FFFFFF", image_field=None, max_length=18, samples=None
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
akarpov/users/themes/migrations/__init__.py
Normal file
0
akarpov/users/themes/migrations/__init__.py
Normal file
11
akarpov/users/themes/models.py
Normal file
11
akarpov/users/themes/models.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from colorfield.fields import ColorField
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Theme(models.Model):
|
||||
name = models.CharField(max_length=250)
|
||||
file = models.FileField(upload_to="themes/")
|
||||
color = ColorField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
0
akarpov/users/themes/tests.py
Normal file
0
akarpov/users/themes/tests.py
Normal file
8
akarpov/users/themes/urls.py
Normal file
8
akarpov/users/themes/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
|
||||
from akarpov.users.themes.views import CreateFormView
|
||||
|
||||
app_name = "themes"
|
||||
urlpatterns = [
|
||||
path("create", CreateFormView.as_view(), name="create"),
|
||||
]
|
13
akarpov/users/themes/views.py
Normal file
13
akarpov/users/themes/views.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.views import generic
|
||||
|
||||
from akarpov.common.views import SuperUserRequiredMixin
|
||||
from akarpov.users.themes.models import Theme
|
||||
|
||||
|
||||
class CreateFormView(generic.CreateView, SuperUserRequiredMixin):
|
||||
model = Theme
|
||||
fields = ["name", "file", "color"]
|
||||
template_name = "users/themes/create.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return ""
|
|
@ -1,4 +1,4 @@
|
|||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from akarpov.users.views import (
|
||||
user_detail_view,
|
||||
|
@ -11,6 +11,7 @@
|
|||
app_name = "users"
|
||||
urlpatterns = [
|
||||
path("redirect/", view=user_redirect_view, name="redirect"),
|
||||
path("themes/", include("akarpov.users.themes.urls", namespace="themes")),
|
||||
path("update/", view=user_update_view, name="update"),
|
||||
path("history/", view=user_history_view, name="history"),
|
||||
path("history/delete", view=user_history_delete_view, name="history_delete"),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
from akarpov.users.models import UserHistory
|
||||
from akarpov.users.services.history import create_history_warning_note
|
||||
from akarpov.users.themes.models import Theme
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -26,11 +27,24 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|||
success_message = _("Information successfully updated")
|
||||
|
||||
def get_success_url(self):
|
||||
assert (
|
||||
self.request.user.is_authenticated
|
||||
) # for mypy to know that the user is authenticated
|
||||
return self.request.user.get_absolute_url()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["themes"] = Theme.objects.all()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
data = self.request.POST
|
||||
if "theme" in data:
|
||||
if data["theme"] == "0":
|
||||
self.object.theme = None
|
||||
else:
|
||||
try:
|
||||
self.object.theme = Theme.objects.get(id=data["theme"])
|
||||
except Theme.DoesNotExist:
|
||||
...
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
|
|
@ -4,4 +4,4 @@ set -o errexit
|
|||
set -o nounset
|
||||
|
||||
|
||||
watchfiles celery.__main__.main --args '-A config.celery_app worker -l INFO'
|
||||
celery -A config.celery_app worker --loglevel=info -c 5
|
||||
|
|
|
@ -11,10 +11,6 @@ entryPoints:
|
|||
entryPoint:
|
||||
to: web-secure
|
||||
|
||||
web-secure:
|
||||
# https
|
||||
address: ":443"
|
||||
|
||||
flower:
|
||||
address: ":5555"
|
||||
|
||||
|
@ -29,27 +25,6 @@ certificatesResolvers:
|
|||
entryPoint: web
|
||||
|
||||
http:
|
||||
routers:
|
||||
web-secure-router:
|
||||
rule: "Host(`akarpov.ru`) || Host(`www.akarpov.ru`)"
|
||||
entryPoints:
|
||||
- web-secure
|
||||
middlewares:
|
||||
- csrf
|
||||
service: django
|
||||
tls:
|
||||
# https://docs.traefik.io/master/routing/routers/#certresolver
|
||||
certResolver: letsencrypt
|
||||
|
||||
flower-secure-router:
|
||||
rule: "Host(`akarpov.ru`)"
|
||||
entryPoints:
|
||||
- flower
|
||||
service: flower
|
||||
tls:
|
||||
# https://docs.traefik.io/master/routing/routers/#certresolver
|
||||
certResolver: letsencrypt
|
||||
|
||||
middlewares:
|
||||
csrf:
|
||||
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import environ
|
||||
import structlog
|
||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
|
||||
# akarpov/
|
||||
|
@ -81,7 +82,7 @@
|
|||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [("127.0.0.1", 6379)],
|
||||
"hosts": [env("REDIS_URL")],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -157,6 +158,7 @@
|
|||
"akarpov.gallery",
|
||||
"akarpov.tools.qr",
|
||||
"akarpov.pipeliner",
|
||||
"akarpov.users.themes",
|
||||
"akarpov.notifications",
|
||||
"akarpov.test_platform",
|
||||
"akarpov.tools.shortener",
|
||||
|
@ -307,6 +309,18 @@
|
|||
)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout
|
||||
EMAIL_TIMEOUT = 5
|
||||
EMAIL_HOST_PASSWORD = env(
|
||||
"EMAIL_PASSWORD",
|
||||
default="",
|
||||
)
|
||||
EMAIL_HOST_USER = env(
|
||||
"EMAIL_USER",
|
||||
default="",
|
||||
)
|
||||
EMAIL_USE_SSL = env(
|
||||
"EMAIL_USE_SSL",
|
||||
default=False,
|
||||
)
|
||||
|
||||
# ADMIN
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -470,7 +484,7 @@
|
|||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SERVERS": [
|
||||
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
|
||||
{"url": "https://akarpov.ru", "description": "Production server"},
|
||||
{"url": "https://new.akarpov.ru", "description": "Production server"},
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -574,5 +588,6 @@
|
|||
signals_spans=True,
|
||||
cache_spans=True,
|
||||
),
|
||||
CeleryIntegration(monitor_beat_tasks=True, propagate_traces=True),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -52,14 +52,9 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
|
||||
INSTALLED_APPS += ["django_extensions"] # noqa F405
|
||||
# Celery
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-eager-propagates
|
||||
CELERY_TASK_EAGER_PROPAGATES = True
|
||||
|
||||
|
||||
# SHORTENER
|
||||
# ------------------------------------------------------------------------------
|
||||
SHORTENER_REDIRECT_TO = "https://dev2.akarpov.ru"
|
||||
SHORTENER_HOST = "https://dev.akarpov.ru"
|
||||
SHORTENER_REDIRECT_TO = env("SHORTENER_REDIRECT_TO", default="http://127.0.0.1:8000")
|
||||
SHORTENER_HOST = env("SHORTENER_HOST", default="http://127.0.0.1:8000")
|
||||
|
|
2128
poetry.lock
generated
2128
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -101,7 +101,7 @@ requests = ">=2.25"
|
|||
spacy = {extras = ["lookups"], version = "^3.6.1"}
|
||||
spacy-transformers = "^1.2.5"
|
||||
extract-msg = "0.28.7"
|
||||
pytest-factoryboy = "2.3.1"
|
||||
pytest-factoryboy = "2.6.0"
|
||||
pytest-xdist = "^3.3.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
|
@ -109,6 +109,7 @@ pytest-lambda = "^2.2.0"
|
|||
pgvector = "^0.2.2"
|
||||
pycld2 = "^0.41"
|
||||
textract = "^1.6.5"
|
||||
uuid6 = "^2023.5.2"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue
Block a user