mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2025-02-22 22:10:33 +03:00
Compare commits
1 Commits
33125578a9
...
273ef6d606
Author | SHA1 | Date | |
---|---|---|---|
|
273ef6d606 |
|
@ -6,6 +6,3 @@ 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,4 +53,3 @@ $ mypy --config-file setup.cfg akarpov
|
||||||
- short link generator
|
- short link generator
|
||||||
- about me app
|
- about me app
|
||||||
- gallery
|
- gallery
|
||||||
- notifications
|
|
||||||
|
|
|
@ -10,18 +10,8 @@
|
||||||
app_name = "music"
|
app_name = "music"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path("playlists/", ListCreatePlaylistAPIView.as_view()),
|
||||||
"playlists/", ListCreatePlaylistAPIView.as_view(), name="list_create_playlist"
|
path("playlists/<str:slug>", RetrieveUpdateDestroyPlaylistAPIView.as_view()),
|
||||||
),
|
path("song/", ListCreateSongAPIView.as_view()),
|
||||||
path(
|
path("song/<str:slug>", RetrieveUpdateDestroySongAPIView.as_view()),
|
||||||
"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,11 +21,6 @@
|
||||||
|
|
||||||
<!-- 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 -->
|
||||||
|
@ -76,7 +71,6 @@
|
||||||
</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>
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,16 +0,0 @@
|
||||||
{% 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,34 +8,10 @@
|
||||||
<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 %}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
|
|
||||||
class URLPathSerializer(serializers.Serializer):
|
|
||||||
path = serializers.URLField()
|
|
||||||
kwargs = serializers.DictField(help_text="{'slug': 'str', 'pk': 'int'}")
|
|
|
@ -1,68 +0,0 @@
|
||||||
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,10 +1,7 @@
|
||||||
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")),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
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,7 +3,6 @@
|
||||||
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")
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class UuidtoolsConfig(AppConfig):
|
|
||||||
name = "akarpov.tools.uuidtools"
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django import forms
|
|
||||||
|
|
||||||
|
|
||||||
class UUIDForm(forms.Form):
|
|
||||||
token = forms.UUIDField()
|
|
|
@ -1,56 +0,0 @@
|
||||||
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
|
|
|
@ -1,9 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = "uuidtools"
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("", views.MainView.as_view(), name="main"),
|
|
||||||
]
|
|
|
@ -1,28 +0,0 @@
|
||||||
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:get", lookup_field="username"
|
view_name="api:users:user_retrieve_username_api", lookup_field="username"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
# 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,7 +27,6 @@ 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,4 +1,3 @@
|
||||||
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
|
||||||
|
@ -28,74 +27,3 @@ 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()
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from akarpov.users.themes.models import Theme
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Theme)
|
|
||||||
class ThemeAdmin(admin.ModelAdmin):
|
|
||||||
...
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ThemesConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "akarpov.users.themes"
|
|
|
@ -1,9 +0,0 @@
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from akarpov.users.themes.models import Theme
|
|
||||||
|
|
||||||
|
|
||||||
class ThemeForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Theme
|
|
||||||
fields = ["name", "file", "color"]
|
|
|
@ -1,35 +0,0 @@
|
||||||
# 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
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from akarpov.users.themes.views import CreateFormView
|
|
||||||
|
|
||||||
app_name = "themes"
|
|
||||||
urlpatterns = [
|
|
||||||
path("create", CreateFormView.as_view(), name="create"),
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
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 include, path
|
from django.urls import path
|
||||||
|
|
||||||
from akarpov.users.views import (
|
from akarpov.users.views import (
|
||||||
user_detail_view,
|
user_detail_view,
|
||||||
|
@ -11,7 +11,6 @@
|
||||||
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,7 +7,6 @@
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
@ -27,24 +26,11 @@ 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
|
||||||
|
|
||||||
|
|
||||||
celery -A config.celery_app worker --loglevel=info -c 5
|
watchfiles celery.__main__.main --args '-A config.celery_app worker -l INFO'
|
||||||
|
|
|
@ -11,6 +11,10 @@ entryPoints:
|
||||||
entryPoint:
|
entryPoint:
|
||||||
to: web-secure
|
to: web-secure
|
||||||
|
|
||||||
|
web-secure:
|
||||||
|
# https
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
flower:
|
flower:
|
||||||
address: ":5555"
|
address: ":5555"
|
||||||
|
|
||||||
|
@ -25,6 +29,27 @@ 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,7 +5,6 @@
|
||||||
|
|
||||||
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/
|
||||||
|
@ -82,7 +81,7 @@
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||||
"CONFIG": {
|
"CONFIG": {
|
||||||
"hosts": [env("REDIS_URL")],
|
"hosts": [("127.0.0.1", 6379)],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -158,7 +157,6 @@
|
||||||
"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",
|
||||||
|
@ -309,18 +307,6 @@
|
||||||
)
|
)
|
||||||
# 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
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
@ -484,7 +470,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://new.akarpov.ru", "description": "Production server"},
|
{"url": "https://akarpov.ru", "description": "Production server"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,6 +574,5 @@
|
||||||
signals_spans=True,
|
signals_spans=True,
|
||||||
cache_spans=True,
|
cache_spans=True,
|
||||||
),
|
),
|
||||||
CeleryIntegration(monitor_beat_tasks=True, propagate_traces=True),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,9 +52,14 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# 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 = env("SHORTENER_REDIRECT_TO", default="http://127.0.0.1:8000")
|
SHORTENER_REDIRECT_TO = "https://dev2.akarpov.ru"
|
||||||
SHORTENER_HOST = env("SHORTENER_HOST", default="http://127.0.0.1:8000")
|
SHORTENER_HOST = "https://dev.akarpov.ru"
|
||||||
|
|
2052
poetry.lock
generated
2052
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -109,7 +109,6 @@ 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