mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2025-02-21 18:20:36 +03:00
Compare commits
8 Commits
273ef6d606
...
33125578a9
Author | SHA1 | Date | |
---|---|---|---|
|
33125578a9 | ||
90f15db5e3 | |||
|
29f78393f4 | ||
e4bfd5ca07 | |||
59fc828097 | |||
403fb8ffa5 | |||
f6f15d3979 | |||
f59df63dd4 |
|
@ -6,3 +6,6 @@ USE_DOCKER=no
|
||||||
EMAIL_HOST=127.0.0.1
|
EMAIL_HOST=127.0.0.1
|
||||||
EMAIL_PORT=1025
|
EMAIL_PORT=1025
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
EMAIL_PASSWORD=
|
||||||
|
EMAIL_USER=
|
||||||
|
EMAIL_USE_SSL=false
|
||||||
|
|
|
@ -53,3 +53,4 @@ $ mypy --config-file setup.cfg akarpov
|
||||||
- short link generator
|
- short link generator
|
||||||
- about me app
|
- about me app
|
||||||
- gallery
|
- gallery
|
||||||
|
- notifications
|
||||||
|
|
|
@ -10,8 +10,18 @@
|
||||||
app_name = "music"
|
app_name = "music"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("playlists/", ListCreatePlaylistAPIView.as_view()),
|
path(
|
||||||
path("playlists/<str:slug>", RetrieveUpdateDestroyPlaylistAPIView.as_view()),
|
"playlists/", ListCreatePlaylistAPIView.as_view(), name="list_create_playlist"
|
||||||
path("song/", ListCreateSongAPIView.as_view()),
|
),
|
||||||
path("song/<str:slug>", RetrieveUpdateDestroySongAPIView.as_view()),
|
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 -->
|
<!-- Latest compiled and minified Bootstrap CSS -->
|
||||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
<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">
|
<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>
|
<script src="https://kit.fontawesome.com/32fd82c823.js" crossorigin="anonymous"></script>
|
||||||
<!-- Your stuff: Third-party CSS libraries go here -->
|
<!-- Your stuff: Third-party CSS libraries go here -->
|
||||||
|
@ -71,6 +76,7 @@
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdown">
|
<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: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>
|
<li><a class="dropdown-item {% active_link 'tools:shortener:create' %}" href="{% url 'tools:shortener:create' %}">URL shortcuter</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</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' %}">
|
<form class="form-horizontal" enctype="multipart/form-data" method="post" action="{% url 'users:update' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ 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="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button type="submit" class="btn btn-primary">Update</button>
|
<button type="submit" class="btn btn-primary">Update</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% 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 django.urls import include, path
|
||||||
|
|
||||||
|
from akarpov.tools.api.views import RetrieveAPIUrlAPIView
|
||||||
|
|
||||||
app_name = "tools"
|
app_name = "tools"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("<str:path>", RetrieveAPIUrlAPIView.as_view(), name="path"),
|
||||||
path("qr/", include("akarpov.tools.qr.api.urls", namespace="qr")),
|
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"
|
app_name = "tools"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("qr/", include("akarpov.tools.qr.urls", namespace="qr")),
|
path("qr/", include("akarpov.tools.qr.urls", namespace="qr")),
|
||||||
|
path("uuid/", include("akarpov.tools.uuidtools.urls", namespace="uuid")),
|
||||||
path(
|
path(
|
||||||
"promocodes/", include("akarpov.tools.promocodes.urls", namespace="promocodes")
|
"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):
|
class UserPublicInfoSerializer(serializers.ModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(
|
url = serializers.HyperlinkedIdentityField(
|
||||||
view_name="api:users:user_retrieve_username_api", lookup_field="username"
|
view_name="api:users:get", lookup_field="username"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
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 = models.BigIntegerField(
|
||||||
"Left file upload(in bites)", default=0, validators=[MinValueValidator(0)]
|
"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):
|
def get_absolute_url(self):
|
||||||
"""Get url for user's detail view.
|
"""Get url for user's detail view.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import pytest
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from pytest_lambda import lambda_fixture, static_fixture
|
from pytest_lambda import lambda_fixture, static_fixture
|
||||||
from rest_framework import status
|
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
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
assert not user.check_password(new_password)
|
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 (
|
from akarpov.users.views import (
|
||||||
user_detail_view,
|
user_detail_view,
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
app_name = "users"
|
app_name = "users"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("redirect/", view=user_redirect_view, name="redirect"),
|
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("update/", view=user_update_view, name="update"),
|
||||||
path("history/", view=user_history_view, name="history"),
|
path("history/", view=user_history_view, name="history"),
|
||||||
path("history/delete", view=user_history_delete_view, name="history_delete"),
|
path("history/delete", view=user_history_delete_view, name="history_delete"),
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
from akarpov.users.models import UserHistory
|
from akarpov.users.models import UserHistory
|
||||||
from akarpov.users.services.history import create_history_warning_note
|
from akarpov.users.services.history import create_history_warning_note
|
||||||
|
from akarpov.users.themes.models import Theme
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -26,11 +27,24 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||||
success_message = _("Information successfully updated")
|
success_message = _("Information successfully updated")
|
||||||
|
|
||||||
def get_success_url(self):
|
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()
|
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):
|
def get_object(self):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
|
@ -4,4 +4,4 @@ set -o errexit
|
||||||
set -o nounset
|
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:
|
entryPoint:
|
||||||
to: web-secure
|
to: web-secure
|
||||||
|
|
||||||
web-secure:
|
|
||||||
# https
|
|
||||||
address: ":443"
|
|
||||||
|
|
||||||
flower:
|
flower:
|
||||||
address: ":5555"
|
address: ":5555"
|
||||||
|
|
||||||
|
@ -29,27 +25,6 @@ certificatesResolvers:
|
||||||
entryPoint: web
|
entryPoint: web
|
||||||
|
|
||||||
http:
|
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:
|
middlewares:
|
||||||
csrf:
|
csrf:
|
||||||
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
|
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import environ
|
import environ
|
||||||
import structlog
|
import structlog
|
||||||
|
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||||
|
|
||||||
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
|
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
|
||||||
# akarpov/
|
# akarpov/
|
||||||
|
@ -81,7 +82,7 @@
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||||
"CONFIG": {
|
"CONFIG": {
|
||||||
"hosts": [("127.0.0.1", 6379)],
|
"hosts": [env("REDIS_URL")],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -157,6 +158,7 @@
|
||||||
"akarpov.gallery",
|
"akarpov.gallery",
|
||||||
"akarpov.tools.qr",
|
"akarpov.tools.qr",
|
||||||
"akarpov.pipeliner",
|
"akarpov.pipeliner",
|
||||||
|
"akarpov.users.themes",
|
||||||
"akarpov.notifications",
|
"akarpov.notifications",
|
||||||
"akarpov.test_platform",
|
"akarpov.test_platform",
|
||||||
"akarpov.tools.shortener",
|
"akarpov.tools.shortener",
|
||||||
|
@ -307,6 +309,18 @@
|
||||||
)
|
)
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout
|
# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout
|
||||||
EMAIL_TIMEOUT = 5
|
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
|
# ADMIN
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
@ -470,7 +484,7 @@
|
||||||
"SERVE_INCLUDE_SCHEMA": False,
|
"SERVE_INCLUDE_SCHEMA": False,
|
||||||
"SERVERS": [
|
"SERVERS": [
|
||||||
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
|
{"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,
|
signals_spans=True,
|
||||||
cache_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
|
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
|
||||||
INSTALLED_APPS += ["django_extensions"] # noqa F405
|
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
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
SHORTENER_REDIRECT_TO = "https://dev2.akarpov.ru"
|
SHORTENER_REDIRECT_TO = env("SHORTENER_REDIRECT_TO", default="http://127.0.0.1:8000")
|
||||||
SHORTENER_HOST = "https://dev.akarpov.ru"
|
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 = {extras = ["lookups"], version = "^3.6.1"}
|
||||||
spacy-transformers = "^1.2.5"
|
spacy-transformers = "^1.2.5"
|
||||||
extract-msg = "0.28.7"
|
extract-msg = "0.28.7"
|
||||||
pytest-factoryboy = "2.3.1"
|
pytest-factoryboy = "2.6.0"
|
||||||
pytest-xdist = "^3.3.1"
|
pytest-xdist = "^3.3.1"
|
||||||
pytest-mock = "^3.11.1"
|
pytest-mock = "^3.11.1"
|
||||||
pytest-asyncio = "^0.21.1"
|
pytest-asyncio = "^0.21.1"
|
||||||
|
@ -109,6 +109,7 @@ pytest-lambda = "^2.2.0"
|
||||||
pgvector = "^0.2.2"
|
pgvector = "^0.2.2"
|
||||||
pycld2 = "^0.41"
|
pycld2 = "^0.41"
|
||||||
textract = "^1.6.5"
|
textract = "^1.6.5"
|
||||||
|
uuid6 = "^2023.5.2"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user