mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-11 02:26:36 +03:00
added shortener app
This commit is contained in:
parent
899f837e7f
commit
222138193b
|
@ -0,0 +1,59 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-15 18:20
|
||||
|
||||
from django.db import migrations
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("files", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="basefile",
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="folder",
|
||||
options={},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="basefile",
|
||||
name="created",
|
||||
field=model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="basefile",
|
||||
name="modified",
|
||||
field=model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="folder",
|
||||
name="created",
|
||||
field=model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="folder",
|
||||
name="modified",
|
||||
field=model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -7,7 +7,7 @@
|
|||
SlugField,
|
||||
TextField,
|
||||
)
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
from akarpov.utils.files import user_file_upload_mixin
|
||||
|
||||
|
|
0
akarpov/shortener/__init__.py
Normal file
0
akarpov/shortener/__init__.py
Normal file
0
akarpov/shortener/admin.py
Normal file
0
akarpov/shortener/admin.py
Normal file
13
akarpov/shortener/apps.py
Normal file
13
akarpov/shortener/apps.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ShortenerConfig(AppConfig):
|
||||
name = "akarpov.shortener"
|
||||
verbose_name = _("Link shortener")
|
||||
|
||||
def ready(self):
|
||||
try:
|
||||
import akarpov.shortener.signals # noqa F401
|
||||
except ImportError:
|
||||
pass
|
90
akarpov/shortener/migrations/0001_initial.py
Normal file
90
akarpov/shortener/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-15 18:20
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Link",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("source", models.URLField()),
|
||||
("slug", models.SlugField()),
|
||||
("enabled", models.BooleanField(default=True)),
|
||||
("viewed", models.IntegerField(default=0)),
|
||||
(
|
||||
"creator",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="links",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LinkViewMeta",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("viewed", models.DateTimeField(auto_now_add=True)),
|
||||
("ip", models.GenericIPAddressField()),
|
||||
("user_agent", models.CharField(max_length=200)),
|
||||
(
|
||||
"link",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="views",
|
||||
to="shortener.link",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
akarpov/shortener/migrations/__init__.py
Normal file
0
akarpov/shortener/migrations/__init__.py
Normal file
29
akarpov/shortener/models.py
Normal file
29
akarpov/shortener/models.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from django.db import models
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
|
||||
class Link(TimeStampedModel):
|
||||
source = models.URLField(blank=False)
|
||||
slug = models.SlugField()
|
||||
|
||||
creator = models.ForeignKey(
|
||||
"users.User", related_name="links", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
viewed = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return f"link to {self.source}"
|
||||
|
||||
|
||||
class LinkViewMeta(models.Model):
|
||||
# TODO: move to mem, delete within 7 days
|
||||
link = models.ForeignKey(Link, related_name="views", on_delete=models.CASCADE)
|
||||
|
||||
viewed = models.DateTimeField(auto_now_add=True)
|
||||
ip = models.GenericIPAddressField()
|
||||
user_agent = models.CharField(max_length=200)
|
||||
|
||||
def __str__(self):
|
||||
return f"view on {self.link.source}"
|
28
akarpov/shortener/services.py
Normal file
28
akarpov/shortener/services.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.conf import settings
|
||||
|
||||
from akarpov.shortener.models import Link
|
||||
from akarpov.utils.generators import generate_charset, get_pk_from_uuid, get_str_uuid
|
||||
|
||||
lenght = settings.SHORTENER_SLUG_LENGTH
|
||||
|
||||
|
||||
def generate_slug(pk: int) -> str:
|
||||
if settings.SHORTENER_ADD_SLUG:
|
||||
slug = generate_charset(lenght)
|
||||
return slug + get_str_uuid(pk)
|
||||
return get_str_uuid(pk)
|
||||
|
||||
|
||||
def get_link_from_slug(slug: str, check_whole=False) -> Link | bool:
|
||||
if settings.SHORTENER_ADD_SLUG and not check_whole:
|
||||
payload = slug[lenght:]
|
||||
pk = get_pk_from_uuid(payload)
|
||||
try:
|
||||
return Link.objects.get(pk=pk)
|
||||
except Link.DoesNotExist:
|
||||
return get_link_from_slug(slug, check_whole=True)
|
||||
pk = get_pk_from_uuid(slug)
|
||||
try:
|
||||
return Link.objects.get(pk=pk)
|
||||
except Link.DoesNotExist:
|
||||
return False
|
12
akarpov/shortener/signals.py
Normal file
12
akarpov/shortener/signals.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from akarpov.shortener.models import Link
|
||||
from akarpov.shortener.services import generate_slug
|
||||
|
||||
|
||||
@receiver(post_save, sender=Link)
|
||||
def link_on_create(sender, instance: Link, created, **kwargs):
|
||||
if created:
|
||||
instance.slug = generate_slug(instance.id)
|
||||
instance.save(update_fields=["slug"])
|
0
akarpov/shortener/tests.py
Normal file
0
akarpov/shortener/tests.py
Normal file
0
akarpov/shortener/views.py
Normal file
0
akarpov/shortener/views.py
Normal file
80
akarpov/utils/consts.py
Normal file
80
akarpov/utils/consts.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
URL_BASE = {
|
||||
"a": 0,
|
||||
"b": 1,
|
||||
"c": 2,
|
||||
"d": 3,
|
||||
"e": 4,
|
||||
"f": 5,
|
||||
"g": 6,
|
||||
"h": 7,
|
||||
"i": 8,
|
||||
"j": 9,
|
||||
"k": 10,
|
||||
"l": 11,
|
||||
"m": 12,
|
||||
"n": 13,
|
||||
"o": 14,
|
||||
"p": 15,
|
||||
"q": 16,
|
||||
"r": 17,
|
||||
"s": 18,
|
||||
"t": 19,
|
||||
"u": 20,
|
||||
"v": 21,
|
||||
"w": 22,
|
||||
"x": 23,
|
||||
"y": 24,
|
||||
"z": 25,
|
||||
"A": 26,
|
||||
"B": 27,
|
||||
"C": 28,
|
||||
"D": 29,
|
||||
"E": 30,
|
||||
"F": 31,
|
||||
"G": 32,
|
||||
"H": 33,
|
||||
"I": 34,
|
||||
"J": 35,
|
||||
"K": 36,
|
||||
"L": 37,
|
||||
"M": 38,
|
||||
"N": 39,
|
||||
"O": 40,
|
||||
"P": 41,
|
||||
"Q": 42,
|
||||
"R": 43,
|
||||
"S": 44,
|
||||
"T": 45,
|
||||
"U": 46,
|
||||
"V": 47,
|
||||
"W": 48,
|
||||
"X": 49,
|
||||
"Y": 50,
|
||||
"Z": 51,
|
||||
"0": 52,
|
||||
"1": 53,
|
||||
"2": 54,
|
||||
"3": 55,
|
||||
"4": 56,
|
||||
"5": 57,
|
||||
"6": 58,
|
||||
"7": 59,
|
||||
"8": 60,
|
||||
"9": 61,
|
||||
";": 62,
|
||||
",": 63,
|
||||
":": 64,
|
||||
"@": 65,
|
||||
"&": 66,
|
||||
"+": 67,
|
||||
"-": 68,
|
||||
"_": 69,
|
||||
".": 70,
|
||||
"!": 71,
|
||||
"~": 72,
|
||||
"*": 73,
|
||||
"'": 74,
|
||||
"(": 75,
|
||||
")": 76,
|
||||
"#": 77,
|
||||
}
|
|
@ -3,6 +3,11 @@
|
|||
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
|
||||
from akarpov.utils.consts import URL_BASE
|
||||
from akarpov.utils.nums import to_base
|
||||
|
||||
URL_CHARACTERS = string.ascii_letters + string.digits + ";,:@&+-_.!~*'()#"
|
||||
|
||||
|
||||
class TokenGenerator(PasswordResetTokenGenerator):
|
||||
"""token processor for user"""
|
||||
|
@ -16,6 +21,19 @@ def generate_charset(length: int) -> str:
|
|||
return "".join(random.choice(string.ascii_letters) for _ in range(length))
|
||||
|
||||
|
||||
def get_str_uuid(pk: int) -> str:
|
||||
return to_base(pk, list(URL_CHARACTERS))
|
||||
|
||||
|
||||
def get_pk_from_uuid(slug: str) -> int:
|
||||
res = 0
|
||||
for i, el in enumerate(slug[::-1]):
|
||||
if el not in URL_BASE:
|
||||
raise ValueError
|
||||
res += URL_BASE[el] * 78**i
|
||||
return res
|
||||
|
||||
|
||||
def _rand255():
|
||||
return random.randint(0, 255)
|
||||
|
||||
|
|
27
akarpov/utils/nums.py
Normal file
27
akarpov/utils/nums.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
def number_to_base(n: int, b: int) -> list[int]:
|
||||
if n == 0:
|
||||
return [0]
|
||||
digits = []
|
||||
while n:
|
||||
digits.append(int(n % b))
|
||||
n //= b
|
||||
return digits[::-1]
|
||||
|
||||
|
||||
def to_base(n: int, base: list) -> str:
|
||||
if n == 0:
|
||||
return ""
|
||||
|
||||
b = len(base)
|
||||
res = ""
|
||||
|
||||
while n:
|
||||
res += base[int(n % b)]
|
||||
n //= b
|
||||
return res[::-1]
|
||||
|
||||
|
||||
def from_base(expr: str, base: int) -> int:
|
||||
return sum(
|
||||
[int(character) * base**index for index, character in enumerate(expr[::-1])]
|
||||
)
|
|
@ -140,6 +140,7 @@
|
|||
"akarpov.users",
|
||||
"akarpov.blog",
|
||||
"akarpov.files",
|
||||
"akarpov.shortener",
|
||||
"akarpov.pipeliner",
|
||||
"akarpov.tools.qr",
|
||||
]
|
||||
|
@ -507,3 +508,9 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
DRF_CHUNKED_UPLOAD_ABSTRACT_MODEL = False
|
||||
DRF_CHUNKED_UPLOAD_PATH = "uploads/chucked/"
|
||||
|
||||
|
||||
# SHORTENER
|
||||
# ------------------------------------------------------------------------------
|
||||
SHORTENER_ADD_SLUG = True
|
||||
SHORTENER_SLUG_LENGTH = 3
|
||||
|
|
Loading…
Reference in New Issue
Block a user