added promocodes, senetry

This commit is contained in:
Alexander Karpov 2023-06-21 17:02:53 +03:00
parent a9937fc88a
commit 1aa39d1d20
20 changed files with 1017 additions and 423 deletions

View File

@ -5,3 +5,4 @@ REDIS_CACHE=rediscache://localhost:6379/1
USE_DOCKER=no USE_DOCKER=no
EMAIL_HOST=127.0.0.1 EMAIL_HOST=127.0.0.1
EMAIL_PORT=1025 EMAIL_PORT=1025
SENTRY_DSN=

View File

@ -4,6 +4,15 @@ My collection of apps and tools
Writen in Python 3.11 and Django 4.2 Writen in Python 3.11 and Django 4.2
### local run via docker
```shell
$ python3 manage.py migrate
$ python3 manage.py runserver
$ celery -A config.celery_app worker --loglevel=info
```
### local run via docker ### local run via docker
```shell ```shell

View File

@ -6,6 +6,7 @@
TopFolderView, TopFolderView,
delete_file_view, delete_file_view,
delete_folder_view, delete_folder_view,
file_download_view,
file_report_list, file_report_list,
file_table, file_table,
file_update, file_update,
@ -34,6 +35,7 @@
path("api/chunked_upload/", ChunkedUploadView.as_view(), name="api_chunked_upload"), path("api/chunked_upload/", ChunkedUploadView.as_view(), name="api_chunked_upload"),
path("api/folder/create/", folder_create, name="folder_create"), path("api/folder/create/", folder_create, name="folder_create"),
path("api/file/report/<str:slug>", report_file, name="file_report"), path("api/file/report/<str:slug>", report_file, name="file_report"),
path("api/file/download/<str:slug>", file_download_view, name="file_download"),
path("api/file/delete/<str:slug>", delete_file_view, name="delete"), path("api/file/delete/<str:slug>", delete_file_view, name="delete"),
path("api/folder/create/<str:slug>", folder_create, name="sub_folder_create"), path("api/folder/create/<str:slug>", folder_create, name="sub_folder_create"),
path("api/folder/delete/<str:slug>", delete_folder_view, name="folder_delete"), path("api/folder/delete/<str:slug>", delete_folder_view, name="folder_delete"),

View File

@ -6,6 +6,7 @@
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.views import View
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DetailView, DetailView,
@ -342,3 +343,14 @@ def get_redirect_url(self, *args, **kwargs):
delete_folder_view = DeleteFolderView.as_view() delete_folder_view = DeleteFolderView.as_view()
class FileDownloadView(View):
def get(self, request, slug):
file = get_object_or_404(File, slug=slug)
file.downloads += 1
file.save(update_fields=["downloads"])
return HttpResponseRedirect(file.file.url)
file_download_view = FileDownloadView.as_view()

View File

@ -84,6 +84,7 @@
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1"> <ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
<li><a class="dropdown-item {% active_link 'users:update' %}" href="{% url 'users:update' %}">Settings</a></li> <li><a class="dropdown-item {% active_link 'users:update' %}" href="{% url 'users:update' %}">Settings</a></li>
<li><a class="dropdown-item {% active_link 'users:detail' request.user.username %}" href="{% url 'users:detail' request.user.username %}">Profile</a></li> <li><a class="dropdown-item {% active_link 'users:detail' request.user.username %}" href="{% url 'users:detail' request.user.username %}">Profile</a></li>
<li><a class="dropdown-item {% active_link 'tools:qr:create' %}" href="{% url 'tools:promocodes:activate' %}">Activate promocode</a></li>
<li> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </li>

View File

@ -58,7 +58,7 @@
{% endif %} {% endif %}
<a class="text-danger mt-2" style="text-decoration: none" href="{% url 'files:file_report' slug=file.slug %}"><i class="bi bi-flag-fill"></i>report file</a> <a class="text-danger mt-2" style="text-decoration: none" href="{% url 'files:file_report' slug=file.slug %}"><i class="bi bi-flag-fill"></i>report file</a>
<div class="mt-4 text-center justify-content-sm-evenly justify-content-md-start gap-3 align-items-md-start align-items-sm-center d-flex"> <div class="mt-4 text-center justify-content-sm-evenly justify-content-md-start gap-3 align-items-md-start align-items-sm-center d-flex">
<a class="btn btn-success fs-6" href="{{ file.file.url }}" download><i class="bi bi-download"></i> Download</a> <a class="btn btn-success fs-6" href="{% url 'files:file_download' slug=file.slug %}" download><i class="bi bi-download"></i> Download</a>
{% if has_perm %} {% if has_perm %}
<a class="btn btn-danger fs-6" href="{% url 'files:delete' slug=file.slug %}"><i class="bi bi-trash"></i> Delete</a> <a class="btn btn-danger fs-6" href="{% url 'files:delete' slug=file.slug %}"><i class="bi bi-trash"></i> Delete</a>
{% endif %} {% endif %}

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
{% if message %}
{% if status %}
<div class="alert alert-dismissible alert-success">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% else %}
<div class="alert alert-dismissible alert-error">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endif %}
<form class="pt-2" enctype="multipart/form-data" method="POST" id="designer-form">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{{ field|as_crispy_field }}
{% endfor %}
<div class="mt-4 flex justify-end space-x-4">
<button class="btn btn-secondary" type="submit" id="submit">
Activate
</button>
</div>
</form>
{% endblock %}

View File

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from akarpov.tools.promocodes.models import PromoCode, PromoCodeActivation
admin.site.register(PromoCode)
admin.site.register(PromoCodeActivation)

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PromocodesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "akarpov.tools.promocodes"

View File

@ -0,0 +1,5 @@
from django import forms
class PromoCodeForm(forms.Form):
promocode = forms.CharField(max_length=250, required=True)

View File

@ -0,0 +1,103 @@
# Generated by Django 4.2.1 on 2023-06-21 13:36
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="PromoCode",
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",
),
),
("promo", models.CharField(max_length=250, unique=True)),
(
"type",
models.CharField(
choices=[
("single", "can be activated only one time, by one user"),
(
"multiuser",
"can be activated many times, but only one time for one user",
),
("multiple", "can be activated multiple times"),
]
),
),
("name", models.CharField(max_length=250)),
("app_name", models.CharField(max_length=250)),
("model", models.CharField(max_length=250)),
("field", models.CharField(max_length=250)),
("value", models.IntegerField()),
("message", models.CharField(max_length=250)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="PromoCodeActivation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("activated", models.DateTimeField(auto_now_add=True)),
(
"promocode",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="activations",
to="promocodes.promocode",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="promocode_activations",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@ -0,0 +1,37 @@
from django.db import models
from model_utils.models import TimeStampedModel
class PromoCode(TimeStampedModel):
class PromoCodeType(models.TextChoices):
single = "single", "can be activated only one time, by one user"
multiuser = (
"multiuser",
"can be activated many times, but only one time for one user",
)
multiple = "multiple", "can be activated multiple times"
promo = models.CharField(max_length=250, unique=True)
type = models.CharField(choices=PromoCodeType.choices)
name = models.CharField(max_length=250)
app_name = models.CharField(max_length=250)
model = models.CharField(max_length=250)
field = models.CharField(max_length=250)
value = models.IntegerField()
message = models.CharField(max_length=250)
def __str__(self):
return self.name
class PromoCodeActivation(models.Model):
activated = models.DateTimeField(auto_now_add=True)
promocode = models.ForeignKey(
"PromoCode", related_name="activations", on_delete=models.CASCADE
)
user = models.ForeignKey(
"users.User", related_name="promocode_activations", on_delete=models.CASCADE
)
def __str__(self):
return f"{self.promocode} activation by {self.user}"

View File

@ -0,0 +1,64 @@
import structlog
from django.apps import apps
from akarpov.tools.promocodes.models import PromoCode, PromoCodeActivation
from akarpov.users.models import User
logger = structlog.get_logger(__name__)
def activate_promocode(code: str, user: User) -> (str, bool):
try:
promo = PromoCode.objects.get(promo=code)
except PromoCode.DoesNotExist:
return "Promocode doesn't exist", False
if promo.type == PromoCode.PromoCodeType.single:
if PromoCodeActivation.objects.filter(promocode=promo).exists():
return "Promocode is already activated", False
elif promo.type == PromoCode.PromoCodeType.multiuser:
if PromoCodeActivation.objects.filter(promocode=promo, user=user).exists():
return "Promocode is already activated", False
try:
model = apps.get_model(app_label=promo.app_name, model_name=promo.model)
except LookupError:
logger.error(
f"can't activate promocode {code} for {promo.model} {promo.app_name} {promo.field}"
)
return "Somthing went wrong, we are already working on it", False
if not hasattr(model, promo.field):
logger.error(
f"can't activate promocode {code} for {promo.model} {promo.app_name} {promo.field}"
)
return "Somthing went wrong, we are already working on it", False
if model is User:
try:
setattr(user, promo.field, getattr(user, promo.field) + promo.value)
user.save()
PromoCodeActivation.objects.create(promocode=promo, user=user)
return promo.message, True
except Exception as e:
logger.error(
f"can't activate promocode {code} for {promo.model} {promo.app_name} {promo.field}, {e}"
)
return "Somthing went wrong, we are already working on it", False
else:
try:
usr_field = ""
if hasattr(model, "user"):
usr_field = "user"
elif hasattr(model, "creator"):
usr_field = "creator"
elif hasattr(model, "owner"):
usr_field = "owner"
obj = model.objects.filter({usr_field: user}).last()
setattr(obj, promo.field, getattr(obj, promo.field) + promo.value)
obj.save()
PromoCodeActivation.objects.create(promocode=promo, user=user)
return promo.message, True
except Exception as e:
logger.error(
f"can't activate promocode {code} for {promo.model} {promo.app_name} {promo.field}, {e}"
)
return "Somthing went wrong, we are already working on it", False

View File

@ -0,0 +1,9 @@
from django.urls import path
from akarpov.tools.promocodes.views import activate_promo_code
app_name = "promocodes"
urlpatterns = [
path("", activate_promo_code, name="activate"),
]

View File

@ -0,0 +1,26 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import generic
from akarpov.tools.promocodes.forms import PromoCodeForm
from akarpov.tools.promocodes.services import activate_promocode
class ActivatePromoCodeView(LoginRequiredMixin, generic.FormView):
form_class = PromoCodeForm
template_name = "tools/promocodes/activate.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["message"] = ""
context["status"] = False
return context
def form_valid(self, form):
msg, status = activate_promocode(form.data["promocode"], self.request.user)
context = self.get_context_data(form=form)
context["message"] = msg
context["status"] = status
return self.render_to_response(context=context)
activate_promo_code = ActivatePromoCodeView.as_view()

View File

@ -3,5 +3,8 @@
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(
"promocodes/", include("akarpov.tools.promocodes.urls", namespace="promocodes")
),
path("shortener/", include("akarpov.tools.shortener.urls", namespace="shortener")), path("shortener/", include("akarpov.tools.shortener.urls", namespace="shortener")),
] ]

View File

@ -149,6 +149,7 @@
"akarpov.test_platform", "akarpov.test_platform",
"akarpov.tools.shortener", "akarpov.tools.shortener",
"akarpov.tools.qr", "akarpov.tools.qr",
"akarpov.tools.promocodes",
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = ( INSTALLED_APPS = (
@ -441,7 +442,6 @@
# django-rest-framework # django-rest-framework
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
@ -450,12 +450,8 @@
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
} }
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
CORS_URLS_REGEX = r"^/api/.*$" CORS_URLS_REGEX = r"^/api/.*$"
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
SPECTACULAR_SETTINGS = { SPECTACULAR_SETTINGS = {
"TITLE": "akarpov API", "TITLE": "akarpov API",
"SCHEMA_PATH_PREFIX": "/api/v[0-9]", "SCHEMA_PATH_PREFIX": "/api/v[0-9]",
@ -467,6 +463,7 @@
{"url": "https://akarpov.ru", "description": "Production server"}, {"url": "https://akarpov.ru", "description": "Production server"},
], ],
} }
# CKEDITOR # CKEDITOR
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
CKEDITOR_UPLOAD_PATH = "uploads/" CKEDITOR_UPLOAD_PATH = "uploads/"
@ -546,3 +543,23 @@
"map.provider": "openstreetmap", "map.provider": "openstreetmap",
"search.provider": "nominatim", "search.provider": "nominatim",
} }
# SENTRY
# ------------------------------------------------------------------------------
dsn = env("SENTRY_DSN", default="")
if dsn:
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=dsn,
traces_sample_rate=1.0,
integrations=[
DjangoIntegration(
transaction_style="url",
middleware_spans=True,
signals_spans=True,
cache_spans=True,
),
],
)

1095
poetry.lock generated

File diff suppressed because it is too large Load Diff