Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
538ac27
Datanar 2.5.3
Gray-Advantage Mar 9, 2025
9b11ce5
Merge pull request #63 from Gray-Advantage/dev
Gray-Advantage Aug 30, 2026
f3b4e85
Merge pull request #64 from Gray-Advantage/dev
Gray-Advantage Aug 30, 2026
812302e
Merge pull request #67 from Gray-Advantage/dev
Gray-Advantage Aug 30, 2026
b26c527
Merge pull request #69 from Gray-Advantage/dev
Gray-Advantage Aug 30, 2026
9672288
Merge pull request #70 from Gray-Advantage/dev
Gray-Advantage Aug 31, 2026
a8219df
Merge pull request #71 from Gray-Advantage/dev
Gray-Advantage Aug 31, 2026
02b5ff8
Merge pull request #72 from Gray-Advantage/dev
Gray-Advantage Aug 31, 2026
c64dfaa
Merge pull request #73 from Gray-Advantage/dev
Gray-Advantage Sep 3, 2026
46cccde
ci: деплой на Mizar — порт SSH из секрета, forced command на сервере
Gray-Advantage Sep 11, 2026
5470753
chore: update dependencies and set Python 3.11 as the minimum require…
brandonzorn Oct 3, 2026
659fad6
feat: remove long_link column from redirects list in admin panel
brandonzorn Oct 4, 2026
2d64dba
feat: redirect list views now return a QuerySet instead of a list[str]
brandonzorn Oct 4, 2026
3e68dfb
feat(core): add base models
brandonzorn Oct 4, 2026
3c7d10b
feat(redirects): use UserLinkModel as the base class for Redirect
brandonzorn Oct 4, 2026
2bcba5d
feat(redirects): add the Redirect.updated_at column to the list in ad…
brandonzorn Oct 4, 2026
05817d7
feat: make all other models inherit from TimeStampedModel
brandonzorn Oct 6, 2026
0c7f18e
refactor: replace string field names with class attributes
brandonzorn Oct 6, 2026
8f57ff7
refactor: reformat js static files
brandonzorn Oct 6, 2026
72ee655
chore: bump chart.js 4.5.0 -> 4.5.1
brandonzorn Oct 6, 2026
c2c565d
refactor: reformat static js files
brandonzorn Oct 6, 2026
4b8e3eb
chore: remove python 3.10 tests and add 3.14
brandonzorn Oct 6, 2026
e08ebd1
feat: reformat and improve profile.js error handling
brandonzorn Oct 6, 2026
bd52dd7
fix: use new field name in ClickManager
brandonzorn Oct 9, 2026
7216ccf
fix: add missing fields to dashboard.blockeddomain fixtures
brandonzorn Nov 2, 2026
5c745c3
fix: convert links QuerySet[Redirect] to QuerySet[str]
brandonzorn Nov 2, 2026
36ad79e
build: update python image
brandonzorn Nov 2, 2026
f4bf197
fix: resolve exceptions when password is empty and check if the link …
brandonzorn Nov 2, 2026
c6cc761
chore: bump requirements and replace psycopg2 with psycopg3
brandonzorn Nov 2, 2026
e167544
fix: use proper django postgresql engine
brandonzorn Nov 3, 2026
1aeadd8
feat: remove api version from urls
brandonzorn Nov 3, 2026
305573c
chore: add python 3.15 support
brandonzorn Nov 3, 2026
8beb11a
chore: update requirements
brandonzorn Nov 4, 2026
889fcce
fix: check ownership permissions before deleting a redirect
brandonzorn Nov 4, 2026
3390fc0
fix: allow updating only selected fields in RedirectManager.get_by_sh…
brandonzorn Nov 4, 2026
f83d95e
fix: use REMOTE_ADDR if HTTP_X_REAL_IP is empty, and pass all redirec…
brandonzorn Nov 4, 2026
adb6809
feat: update api version
brandonzorn Nov 4, 2026
7a5e345
feat: add updated_at field to RedirectSerializer
brandonzorn Nov 4, 2026
de2ed55
feat: remove the 'delete redirect' button for unauthorized users
brandonzorn Nov 4, 2026
3fb83c8
fix: ignore ValueError when passing an invalid url to is_link_shortener
brandonzorn Nov 4, 2026
e1ebffa
feat: Datanar 2.6.6
brandonzorn Nov 4, 2026
fe267c5
fix: remove incompatible flake8-dunder-all plugin
brandonzorn Nov 4, 2026
e36315a
fix: add updated at to valid fields
brandonzorn Nov 4, 2026
15e8d4d
feat: update en locale
brandonzorn Nov 5, 2026
1b18159
feat: update ru locale
brandonzorn Nov 5, 2026
8d2d999
feat: update uk locale
brandonzorn Nov 5, 2026
762bac7
feat: update bg locale
brandonzorn Nov 5, 2026
93df3f9
feat: update de locale
brandonzorn Nov 5, 2026
a91cb78
feat: update es locale
brandonzorn Nov 5, 2026
d6f1c57
feat: update fr locale
brandonzorn Nov 5, 2026
64461f4
feat: update ja locale
brandonzorn Nov 5, 2026
4feb848
fix: use created_at for Click model
brandonzorn Nov 6, 2026
d39fa62
refactor: add type hints to statistic.views
brandonzorn Nov 6, 2026
084c0e1
fix: add Click.created_at field to for_short_link_by_all_time
brandonzorn Nov 6, 2026
de46bf9
fix: check ownership permissions before downloading statistics
brandonzorn Nov 7, 2026
6905096
fix: generate short links with random base62 instead of sha256+sqids …
brandonzorn Nov 7, 2026
6fbc279
fix: remove self import in settings
brandonzorn Nov 7, 2026
6bfc217
fix: check post method to avoid recursion
brandonzorn Nov 8, 2026
690e7d3
refactor: remove default get argument
brandonzorn Nov 8, 2026
0a5b481
refactor: use request object from arguments
brandonzorn Nov 8, 2026
971c482
feat: add PeriodConverter
brandonzorn Nov 8, 2026
647e9af
feat: use Period enum in urlpath and optimized xlsx export
brandonzorn Nov 8, 2026
bf42bdb
fix: convert method to uppercase before comparing
brandonzorn Nov 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Datanar

on:
push:
branches: [ "main", "dev", "artem", "sergey", "vladimir", "for_merge" ]
branches: [ "main", "dev", "artem", "sergey" ]
pull_request:
branches: [ "main", "dev", "artem", "sergey", "vladimir", "for_merge" ]
branches: [ "main", "dev", "artem", "sergey", "vladimir" ]

permissions:
contents: read
Expand Down Expand Up @@ -54,7 +54,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
python-version: [ "3.10", "3.11", "3.12", "3.13" ]
python-version: [ "3.11", "3.12", "3.13", "3.14" ]
name: django-tests (${{ matrix.os }}, py${{ matrix.python-version }})
steps:
- uses: actions/checkout@v4
Expand All @@ -79,12 +79,12 @@ jobs:
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
# Ключ на сервере ограничен forced command: что бы ни было в script,
# выполнится только /usr/local/bin/deploy-datanar (git reset на
# origin/main, compose up --build, проверка живости). Сам script
# нужен action-у формально.
script: |
cd ~/datanar
docker compose down
git reset --hard origin/main
git pull origin main
docker compose up --build -d
docker system prune -a -f --volumes
true
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13.15-alpine
FROM python:3.13.17-alpine

RUN apk add --no-cache gettext

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
- [Docker](https://www.docker.com/) - развёртывание сайта на удалённом сервере
- [Prism](https://prismjs.com/) - подсветка синтаксиса кода
- [Segno](https://github.com/heuer/segno/) - генерация QR-кодов
- [Sqids](https://sqids.org/) - для помощи в генерации сокращённых ссылок

### База данных
Функциональная структура базы данных следующая:
Expand Down
2 changes: 2 additions & 0 deletions datanar/api/tests/test_correct_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def setUp(self) -> None:
Redirect.validity_days.field.name,
Redirect.validity_clicks.field.name,
Redirect.created_at.field.name,
Redirect.updated_at.field.name,
Redirect.create_method.field.name,
Redirect.is_active.field.name,
Redirect.deactivated_at.field.name,
Expand All @@ -42,6 +43,7 @@ def setUp(self) -> None:
Redirect.validity_days.field.name,
Redirect.validity_clicks.field.name,
Redirect.created_at.field.name,
Redirect.updated_at.field.name,
}

def test_create_new_token_context(self) -> None:
Expand Down
11 changes: 6 additions & 5 deletions datanar/core/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Http404,
HttpRequest,
HttpResponse,
HttpResponseBase,
HttpResponseRedirect,
)
from django.views.generic import View
Expand Down Expand Up @@ -68,9 +69,9 @@ def post(
request: HttpRequest,
*args: Any,
**kwargs: Any,
) -> HttpResponse:
method = request.POST.get("_method", None)
if method:
) -> "HttpResponseBase":
method = request.POST.get("_method", "")
if method.upper() in ("DELETE", "PATCH", "PUT"):
request.method = method
return super().dispatch(request, *args, **kwargs)
return super().post(request, *args, **kwargs)
Expand All @@ -82,7 +83,7 @@ def dispatch(
request: HttpRequest,
*args: Any,
**kwargs: Any,
) -> HttpResponse:
if not self.request.user.is_staff:
) -> "HttpResponseBase":
if not request.user.is_staff:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
115 changes: 115 additions & 0 deletions datanar/core/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
__all__ = ("TimeStampedModel", "UserLinkModel", "UserStampedModel")

from datetime import timedelta

from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _


class TimeStampedModel(models.Model):
created_at = models.DateTimeField(
_("Creation time"),
help_text=_("When the object was created."),
auto_now_add=True,
)
updated_at = models.DateTimeField(
_("Update time"),
help_text=_("When the object was last updated."),
auto_now=True,
)

class Meta:
abstract = True


class UserStampedModel(TimeStampedModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_("user"),
help_text=_("user_who_create_bond"),
null=True,
)

class CreateMethod(models.TextChoices):
API = "API", _("API")
WEB = "WEB", _("Web")
WEB_FILE = "WEB+FILE", _("Web (file)")

create_method = models.CharField(
_("create_method"),
help_text=_("method_which_redirect_was_created"),
max_length=20,
choices=CreateMethod.choices,
default=CreateMethod.WEB,
)

class Meta:
abstract = True


class UserLinkModel(UserStampedModel):
short_link = models.SlugField(
_("short_link"),
help_text=_("shorten_link_to_redirect_to_resource"),
max_length=50,
unique=True,
allow_unicode=True,
)
validity_days = models.PositiveIntegerField(
_("valid_day_number"),
help_text=_("how_many_day_link_will_be_valid"),
default=90,
null=True,
blank=True,
)
validity_clicks = models.PositiveIntegerField(
_("valid_click_number"),
help_text=_("how_many_click_on_link_will_be_valid"),
default=None,
null=True,
blank=True,
)
is_active = models.BooleanField(
_("active"),
help_text=_("is_active"),
default=True,
)
deactivated_at = models.DateTimeField(
_("deactivation_time"),
help_text=_("when_bond_was_deactivated"),
default=None,
null=True,
blank=True,
)

class Meta:
abstract = True

def is_clicks_exceeded(self, clicks_count: int) -> bool:
if not self.validity_clicks:
return False
return clicks_count >= self.validity_clicks

def is_expired(self) -> bool:
if not self.validity_days:
return False
return (
self.created_at + timedelta(days=self.validity_days)
< timezone.now()
)

def is_deactivation_expired(self) -> bool:
if self.is_active or self.deactivated_at is None:
return False
return (self.deactivated_at + timedelta(days=10)) < timezone.now()

def reactivate(self) -> None:
self.is_active = True
self.deactivated_at = None

def deactivate(self) -> None:
self.is_active = False
self.deactivated_at = timezone.now()
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.2.17 on 2026-10-06 19:09

import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("dashboard", "0002_alter_blockeddomain_options"),
]

operations = [
migrations.AddField(
model_name="blockeddomain",
name="created_at",
field=models.DateTimeField(
auto_now_add=True,
default=django.utils.timezone.now,
help_text="When the object was created.",
verbose_name="Creation time",
),
preserve_default=False,
),
migrations.AddField(
model_name="blockeddomain",
name="updated_at",
field=models.DateTimeField(
auto_now=True,
help_text="When the object was last updated.",
verbose_name="Update time",
),
),
]
11 changes: 8 additions & 3 deletions datanar/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from django.db import models
from django.utils.translation import gettext_lazy as _

from core.models import TimeStampedModel


class BlockedDomainManager(models.Manager):
def upgrade_regex(self, domain_regex: str) -> str:
Expand All @@ -27,15 +29,18 @@ def upgrade_regex(self, domain_regex: str) -> str:
return domain_regex

def is_blocked(self, url: str) -> bool:
if is_link_shortener(url):
return True
try:
if is_link_shortener(url):
return True
except ValueError:
pass
for domain in self.all():
if re.fullmatch(self.upgrade_regex(domain.domain_regex), url):
return True
return False


class BlockedDomain(models.Model):
class BlockedDomain(TimeStampedModel):
domain_regex = models.CharField(
_("domain_regex"),
help_text=_("domain_regex_that_will_be_prohibited_from_shortening"),
Expand Down
11 changes: 5 additions & 6 deletions datanar/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,15 @@ class AllLinksView(
paginate_by = 6

def get_queryset(self) -> "QuerySet":
redirects = (
Redirect.objects.all()
.only(Redirect.short_link.field.name)
.order_by("-created_at")
return Redirect.objects.only(
Redirect.short_link.field.name,
).order_by(
f"-{Redirect.created_at.field.name}",
)
return [redirect.short_link for redirect in redirects]

def delete(self, request: HttpRequest) -> HttpResponse:
Redirect.objects.filter(
short_link=request.POST.get("short_link"),
short_link=request.POST.get(Redirect.short_link.field.name),
).delete()
return HttpResponseRedirect(request.get_full_path())

Expand Down
9 changes: 4 additions & 5 deletions datanar/datanar/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
import sys

from decouple import config
from django.conf import settings
from django.utils.translation import gettext_lazy as _

BASE_DIR = Path(__file__).resolve().parent.parent

VERSION = "2.6.5"
API_VERSION = "1.2.0"
VERSION = "2.6.6"
API_VERSION = "1.2.1"

SECRET_KEY = config(
"DATANAR_SECRET_KEY",
Expand Down Expand Up @@ -105,7 +104,7 @@
"tz_detect.middleware.TimezoneMiddleware",
]

if settings.DEBUG and not TEST:
if DEBUG and not TEST:
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ["127.0.0.1"]
Expand Down Expand Up @@ -155,7 +154,7 @@
if not USE_FILE_DATABASE:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": config("DATANAR_DATABASE_NAME"),
"USER": config("DATANAR_DATABASE_USER"),
"PASSWORD": config("DATANAR_DATABASE_PASSWORD"),
Expand Down
2 changes: 1 addition & 1 deletion datanar/datanar/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
urlpatterns = [
path("", include("homepage.urls")),
path("admin/", admin.site.urls),
path("api/v1/", include("api.urls")),
path("api/", include("api.urls")),
path("auth/", include("users.urls")),
path("auth/", include("allauth.urls")),
path("dashboard/", include("dashboard.urls")),
Expand Down
Loading
Loading