mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-28 12:43:44 +03:00
added promocodes, senetry
This commit is contained in:
parent
a9937fc88a
commit
1aa39d1d20
|
@ -5,3 +5,4 @@ REDIS_CACHE=rediscache://localhost:6379/1
|
|||
USE_DOCKER=no
|
||||
EMAIL_HOST=127.0.0.1
|
||||
EMAIL_PORT=1025
|
||||
SENTRY_DSN=
|
||||
|
|
|
@ -4,6 +4,15 @@ My collection of apps and tools
|
|||
|
||||
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
|
||||
|
||||
```shell
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
TopFolderView,
|
||||
delete_file_view,
|
||||
delete_folder_view,
|
||||
file_download_view,
|
||||
file_report_list,
|
||||
file_table,
|
||||
file_update,
|
||||
|
@ -34,6 +35,7 @@
|
|||
path("api/chunked_upload/", ChunkedUploadView.as_view(), name="api_chunked_upload"),
|
||||
path("api/folder/create/", folder_create, name="folder_create"),
|
||||
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/folder/create/<str:slug>", folder_create, name="sub_folder_create"),
|
||||
path("api/folder/delete/<str:slug>", delete_folder_view, name="folder_delete"),
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
from django.views.generic import (
|
||||
CreateView,
|
||||
DetailView,
|
||||
|
@ -342,3 +343,14 @@ def get_redirect_url(self, *args, **kwargs):
|
|||
|
||||
|
||||
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()
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
<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: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>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
{% 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>
|
||||
<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 %}
|
||||
<a class="btn btn-danger fs-6" href="{% url 'files:delete' slug=file.slug %}"><i class="bi bi-trash"></i> Delete</a>
|
||||
{% endif %}
|
||||
|
|
32
akarpov/templates/tools/promocodes/activate.html
Normal file
32
akarpov/templates/tools/promocodes/activate.html
Normal 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 %}
|
0
akarpov/tools/promocodes/__init__.py
Normal file
0
akarpov/tools/promocodes/__init__.py
Normal file
6
akarpov/tools/promocodes/admin.py
Normal file
6
akarpov/tools/promocodes/admin.py
Normal 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)
|
6
akarpov/tools/promocodes/apps.py
Normal file
6
akarpov/tools/promocodes/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PromocodesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "akarpov.tools.promocodes"
|
5
akarpov/tools/promocodes/forms.py
Normal file
5
akarpov/tools/promocodes/forms.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class PromoCodeForm(forms.Form):
|
||||
promocode = forms.CharField(max_length=250, required=True)
|
103
akarpov/tools/promocodes/migrations/0001_initial.py
Normal file
103
akarpov/tools/promocodes/migrations/0001_initial.py
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
akarpov/tools/promocodes/migrations/__init__.py
Normal file
0
akarpov/tools/promocodes/migrations/__init__.py
Normal file
37
akarpov/tools/promocodes/models.py
Normal file
37
akarpov/tools/promocodes/models.py
Normal 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}"
|
64
akarpov/tools/promocodes/services.py
Normal file
64
akarpov/tools/promocodes/services.py
Normal 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
|
9
akarpov/tools/promocodes/urls.py
Normal file
9
akarpov/tools/promocodes/urls.py
Normal 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"),
|
||||
]
|
26
akarpov/tools/promocodes/views.py
Normal file
26
akarpov/tools/promocodes/views.py
Normal 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()
|
|
@ -3,5 +3,8 @@
|
|||
app_name = "tools"
|
||||
urlpatterns = [
|
||||
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")),
|
||||
]
|
||||
|
|
|
@ -149,6 +149,7 @@
|
|||
"akarpov.test_platform",
|
||||
"akarpov.tools.shortener",
|
||||
"akarpov.tools.qr",
|
||||
"akarpov.tools.promocodes",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = (
|
||||
|
@ -441,7 +442,6 @@
|
|||
|
||||
# django-rest-framework
|
||||
# -------------------------------------------------------------------------------
|
||||
# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
|
@ -450,12 +450,8 @@
|
|||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
||||
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 = {
|
||||
"TITLE": "akarpov API",
|
||||
"SCHEMA_PATH_PREFIX": "/api/v[0-9]",
|
||||
|
@ -467,6 +463,7 @@
|
|||
{"url": "https://akarpov.ru", "description": "Production server"},
|
||||
],
|
||||
}
|
||||
|
||||
# CKEDITOR
|
||||
# ------------------------------------------------------------------------------
|
||||
CKEDITOR_UPLOAD_PATH = "uploads/"
|
||||
|
@ -546,3 +543,23 @@
|
|||
"map.provider": "openstreetmap",
|
||||
"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
1095
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user