mirror of
				https://github.com/Alexander-D-Karpov/akarpov
				synced 2025-10-31 20:17:25 +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