From 52dda2c9efcadd745c43329fe17be14459023a0b Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:05:08 +0000 Subject: [PATCH 01/15] WIP feature: Started adding quotas. Now how a working quotas list page --- assets/data/default_quota_limits.json | 60 +++++++++++++ backend/admin.py | 10 +++ backend/api/base/modal.py | 9 +- backend/api/quotas/fetch.py | 22 +++++ backend/api/quotas/urls.py | 13 +++ backend/api/urls.py | 1 + backend/data/default_quota_limits.py | 86 +++++++++++++++++++ .../0027_quotalimit_quotaoverrides.py | 49 +++++++++++ backend/models.py | 42 ++++++++- backend/signals.py | 25 ++++-- backend/urls.py | 3 + backend/utils.py | 1 - backend/views/core/quotas/view.py | 11 +++ frontend/templates/base/+left_drawer.html | 7 ++ .../modals/view_quota_limit_info.html | 39 +++++++++ .../templates/pages/quotas/_fetch_body.html | 30 +++++++ .../templates/pages/quotas/dashboard.html | 24 ++++++ frontend/templates/pages/quotas/list.html | 25 ++++++ 18 files changed, 447 insertions(+), 10 deletions(-) create mode 100644 assets/data/default_quota_limits.json create mode 100644 backend/api/quotas/fetch.py create mode 100644 backend/api/quotas/urls.py create mode 100644 backend/data/default_quota_limits.py create mode 100644 backend/migrations/0027_quotalimit_quotaoverrides.py create mode 100644 backend/views/core/quotas/view.py create mode 100644 frontend/templates/modals/view_quota_limit_info.html create mode 100644 frontend/templates/pages/quotas/_fetch_body.html create mode 100644 frontend/templates/pages/quotas/dashboard.html create mode 100644 frontend/templates/pages/quotas/list.html diff --git a/assets/data/default_quota_limits.json b/assets/data/default_quota_limits.json new file mode 100644 index 00000000..ca1d34b4 --- /dev/null +++ b/assets/data/default_quota_limits.json @@ -0,0 +1,60 @@ +{ + "invoices": { + "count": { + "name": "Creations per month", + "description": "Amount of invoice creations allowed per month", + "default_value": 100, + "adjustable": true + }, + "schedules": { + "name": "Schedules per month", + "description": "Amount of invoice scheduled sends allowed per month", + "default_value": 100, + "adjustable": true + }, + "access_codes": { + "name": "Access codes", + "description": " Amount of invoice access codes allowed per invoice", + "default_value": 3, + "adjustable": true + } + }, + "receipts": { + "count": { + "Receipts stored per month": { + "name": "Receipts stored per month", + "description": "Amount of receipts stored per month", + "default_value": 100, + "adjustable": true + } + } + }, + "clients": { + "count": { + "name": "Stored clients", + "description": "Amount of clients stored in total", + "default_value": 40, + "adjustable": true + } + }, + "teams": { + "count": { + "name": "Created teams", + "description": "Amount of teams created in total", + "default_value": 3, + "adjustable": true + }, + "joined": { + "name": "Joined teams", + "description": "Amount of teams joined in total", + "default_value": 5, + "adjustable": true + }, + "user_count": { + "name": "Users per team", + "description": "Amount of users per team", + "default_value": 10, + "adjustable": true + } + } +} diff --git a/backend/admin.py b/backend/admin.py index b936979c..a811326b 100644 --- a/backend/admin.py +++ b/backend/admin.py @@ -21,6 +21,8 @@ VerificationCodes, APIKey, InvoiceOnetimeSchedule, + QuotaLimit, + QuotaOverrides ) # from django.contrib.auth.models imp/ort User @@ -45,9 +47,17 @@ VerificationCodes, APIKey, InvoiceOnetimeSchedule, + QuotaOverrides ] ) + +class QuotaLimitAdmin(admin.ModelAdmin): + readonly_fields = ["name", "slug"] + + +admin.site.register(QuotaLimit, QuotaLimitAdmin) + # admin.site.unregister(User) fields = list(UserAdmin.fieldsets) fields[0] = (None, {"fields": ("username", "password", "logged_in_as_team", "awaiting_email_verification")}) diff --git a/backend/api/base/modal.py b/backend/api/base/modal.py index 24ad3606..fdb4dc90 100644 --- a/backend/api/base/modal.py +++ b/backend/api/base/modal.py @@ -1,7 +1,7 @@ from django.http import HttpRequest, HttpResponseBadRequest from django.shortcuts import render -from backend.models import UserSettings, Invoice, Team +from backend.models import UserSettings, Invoice, Team, QuotaLimit # Still working on @@ -51,6 +51,13 @@ def open_modal(request: HttpRequest, modal_name, context_type=None, context_valu context["invoice"] = invoice except Invoice.DoesNotExist: ... + elif context_type == "quota": + try: + quota = QuotaLimit.objects.prefetch_related("quota_overrides").get(slug=context_value) + context["quota"] = quota + context["current_limit"] = quota.get_quota_limit(user=request.user) + except QuotaLimit.DoesNotExist: + ... else: context[context_type] = context_value diff --git a/backend/api/quotas/fetch.py b/backend/api/quotas/fetch.py new file mode 100644 index 00000000..0642811c --- /dev/null +++ b/backend/api/quotas/fetch.py @@ -0,0 +1,22 @@ +from django.db.models import Q +from django.http import HttpRequest +from django.shortcuts import render, redirect + +from backend.models import QuotaLimit + + +def fetch_all_quotas(request: HttpRequest, group: str): + context = {} + if not request.htmx: + return redirect("quotas") + + search_text = request.GET.get("search") + + results = QuotaLimit.objects.filter(slug__startswith=group).prefetch_related("quota_overrides").order_by("-slug") + + if search_text: + results = results.filter(Q(name__icontains=search_text)) + + quota_list = zip(results, [q.get_quota_limit(user=request.user) for q in results]) + context.update({"quotas": quota_list}) + return render(request, "pages/quotas/_fetch_body.html", context) diff --git a/backend/api/quotas/urls.py b/backend/api/quotas/urls.py new file mode 100644 index 00000000..d546b77e --- /dev/null +++ b/backend/api/quotas/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from . import fetch + +urlpatterns = [ + path( + "fetch//", + fetch.fetch_all_quotas, + name="fetch", + ) +] + +app_name = "quotas" diff --git a/backend/api/urls.py b/backend/api/urls.py index 6a2d03d7..3eef0cc6 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -10,6 +10,7 @@ path("settings/", include("backend.api.settings.urls")), path("products/", include("backend.api.products.urls")), path("currency_converter/", include("backend.api.currency_converter.urls")), + path("quotas/", include("backend.api.quotas.urls")), path("hc/", include("backend.api.healthcheck.urls")), ] diff --git a/backend/data/default_quota_limits.py b/backend/data/default_quota_limits.py new file mode 100644 index 00000000..11742f01 --- /dev/null +++ b/backend/data/default_quota_limits.py @@ -0,0 +1,86 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class QuotaItem: + slug: str + name: str + description: str + default_value: int + adjustable: bool + + +@dataclass +class QuotaGroup: + name: str + items: List[QuotaItem] + + +default_quota_limits: List[QuotaGroup] = [ + QuotaGroup("invoices", [ + QuotaItem( + slug="count", + name="Creations per month", + description="Amount of invoices created per month", + default_value=100, + adjustable=True + ), + QuotaItem( + slug="schedules", + name="Schedules per month", + description="Amount of invoice scheduled sends allowed per month", + default_value=100, + adjustable=True + ), + QuotaItem( + slug="access_codes", + name="Created access codes", + description="Amount of invoice access codes allowed per invoice", + default_value=3, + adjustable=True + ) + ]), + QuotaGroup("receipts", [ + QuotaItem( + slug="count", + name="Created receipts", + description="Amount of receipts stored per month", + default_value=100, + adjustable=True + ) + ]), + + QuotaGroup("clients", [ + QuotaItem( + slug="count", + name="Created clients", + description="Amount of clients stored in total", + default_value=40, + adjustable=True + ) + ]), + QuotaGroup("teams", [ + QuotaItem( + slug="count", + name="Created teams", + description="Amount of teams created in total", + default_value=3, + adjustable=True + ), + QuotaItem( + slug="joined", + name="Joined teams", + description="Amount of teams joined in total", + default_value=5, + adjustable=True + ), + QuotaItem( + slug="user_count", + name="Users per team", + description="Amount of users per team", + default_value=10, + adjustable=True + ) + ]) +] diff --git a/backend/migrations/0027_quotalimit_quotaoverrides.py b/backend/migrations/0027_quotalimit_quotaoverrides.py new file mode 100644 index 00000000..b796b228 --- /dev/null +++ b/backend/migrations/0027_quotalimit_quotaoverrides.py @@ -0,0 +1,49 @@ +# Generated by Django 5.0.3 on 2024-03-30 22:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend", "0026_invoice_discount_amount_invoice_discount_percentage"), + ] + + operations = [ + migrations.CreateModel( + name="QuotaLimit", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.CharField(editable=False, max_length=100, unique=True)), + ("name", models.CharField(editable=False, max_length=100, unique=True)), + ("description", models.TextField(blank=True, max_length=500, null=True)), + ("value", models.IntegerField()), + ("updated_at", models.DateTimeField(auto_now=True)), + ("adjustable", models.BooleanField(default=True)), + ], + options={ + "verbose_name": "Quota Limit", + "verbose_name_plural": "Quota Limits", + }, + ), + migrations.CreateModel( + name="QuotaOverrides", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("value", models.IntegerField()), + ("updated_at", models.DateTimeField(auto_now=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "quota_limit", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="quota_overrides", to="backend.quotalimit"), + ), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "verbose_name": "Quota Override", + "verbose_name_plural": "Quota Overrides", + }, + ), + ] diff --git a/backend/models.py b/backend/models.py index 95438a9d..7d0dff78 100644 --- a/backend/models.py +++ b/backend/models.py @@ -544,9 +544,47 @@ class FeatureFlags(models.Model): value = models.BooleanField(default=False) updated_at = models.DateTimeField(auto_now=True) + class Meta: + verbose_name = "Feature Flag" + verbose_name_plural = "Feature Flags" + def __str__(self): return self.name + +class QuotaLimit(models.Model): + slug = models.CharField(max_length=100, unique=True, editable=False) + name = models.CharField(max_length=100, unique=True, editable=False) + description = models.TextField(max_length=500, null=True, blank=True) + value = models.IntegerField() + updated_at = models.DateTimeField(auto_now=True) + adjustable = models.BooleanField(default=True) + class Meta: - verbose_name = "Feature Flag" - verbose_name_plural = "Feature Flags" + verbose_name = "Quota Limit" + verbose_name_plural = "Quota Limits" + + def __str__(self): + return self.name + + def get_quota_limit(self, user: User): + try: + user_quota_override = self.quota_overrides.get(user=user) + return user_quota_override.value + except QuotaOverrides.DoesNotExist: + return self.value + + +class QuotaOverrides(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + quota_limit = models.ForeignKey(QuotaLimit, on_delete=models.CASCADE, related_name="quota_overrides") + value = models.IntegerField() + updated_at = models.DateTimeField(auto_now=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "Quota Override" + verbose_name_plural = "Quota Overrides" + + def __str__(self): + return f"{self.user}" diff --git a/backend/signals.py b/backend/signals.py index 2dc8d5b4..0485f8ce 100644 --- a/backend/signals.py +++ b/backend/signals.py @@ -1,6 +1,8 @@ from django.core.cache import cache from django.core.cache.backends.redis import RedisCacheClient +from backend.data.default_quota_limits import default_quota_limits + cache: RedisCacheClient = cache from django.core.files.storage import default_storage from django.db.models.signals import pre_save, post_delete, post_save, post_migrate @@ -8,7 +10,7 @@ from django.urls import reverse import settings.settings -from backend.models import UserSettings, Receipt, User, FeatureFlags, VerificationCodes +from backend.models import UserSettings, Receipt, User, FeatureFlags, VerificationCodes, QuotaLimit from settings.helpers import ARE_EMAILS_ENABLED, send_email @@ -65,7 +67,6 @@ def delete_receipt_image_on_delete(sender, instance: Receipt, **kwargs): {"name": "isInvoiceSchedulingEnabled", "default": False, "pk": 2}, ] - def insert_initial_data(**kwargs): for feature in feature_flags: try: @@ -76,6 +77,18 @@ def insert_initial_data(**kwargs): flag.value = feature.get("default") flag.save() + for group in default_quota_limits: + for item in group.items: + QuotaLimit.objects.get_or_create( + slug=f"{group.name}-{item.slug}", + defaults={ + "name": f"{item.name}", + "value": item.default_value, + "adjustable": item.adjustable, + "description": item.description + } + ) + post_migrate.connect(insert_initial_data) @@ -96,12 +109,12 @@ def send_welcome_email(sender, instance: User, created, **kwargs): if created: email_message = f""" Welcome to MyFinances{f", {instance.first_name}" if instance.first_name else ""}! - + We're happy to have you join us. We are still in development and are still working on the core mechanics. - If you find any bugs with our software, create a bug report on our + If you find any bugs with our software, create a bug report on our Github Issues (https://github.com/TreyWW/MyFinances/issues/new?assignees=&labels=bug&projects=&template=bug-report.md&title=%5BBUG%5D+) and we'll try to help debug the issue or squash the bug. - + Thank you for using MyFinances. """ if ARE_EMAILS_ENABLED: @@ -114,7 +127,7 @@ def send_welcome_email(sender, instance: User, created, **kwargs): email_message += f""" To start with, you must first **verify this email** so that we can link your account to this email. Click the link below to activate your account, no details are required, once pressed you're all set! - + Verify Link: {magic_link_url} """ diff --git a/backend/urls.py b/backend/urls.py index 3af88996..4b7c9e56 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -14,6 +14,7 @@ from backend.views.core.currency_converter import dashboard as cc_dashboard from backend.views.core.invoices.overview import manage_invoice from backend.views.core.other.index import index, dashboard +from backend.views.core.quotas.view import quotas_page, quotas_list url( r"^frontend/static/(?P.*)$", @@ -27,6 +28,8 @@ path("dashboard/", dashboard, name="dashboard"), path("dashboard/settings/", settings_v.view.settings_page, name="user settings"), path("dashboard/invoices/", include("backend.views.core.invoices.urls")), + path("dashboard/quotas/", quotas_page, name="quotas"), + path("dashboard/quotas//", quotas_list, name="quotas group"), path( "dashboard/invoice//", manage_invoice, diff --git a/backend/utils.py b/backend/utils.py index 7bfa2238..24406f49 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -5,7 +5,6 @@ from backend.models import FeatureFlags - def get_feature_status(feature, should_use_cache=True): if should_use_cache: key = f"myfinances:feature_flag:{feature}" diff --git a/backend/views/core/quotas/view.py b/backend/views/core/quotas/view.py new file mode 100644 index 00000000..58ab1332 --- /dev/null +++ b/backend/views/core/quotas/view.py @@ -0,0 +1,11 @@ +from django.http import HttpResponse, HttpRequest + +from django.shortcuts import render + + +def quotas_page(request: HttpRequest) -> HttpResponse: + return render(request, "pages/quotas/dashboard.html") + + +def quotas_list(request: HttpRequest, group: str) -> HttpResponse: + return render(request, "pages/quotas/list.html", {"group": group}) diff --git a/frontend/templates/base/+left_drawer.html b/frontend/templates/base/+left_drawer.html index af37b6a8..94b2b051 100644 --- a/frontend/templates/base/+left_drawer.html +++ b/frontend/templates/base/+left_drawer.html @@ -39,5 +39,12 @@ {% if request.resolver_match.url_name == i_url %}class="active"{% endif %}>Currency Converter {% endwith %} +
  • + {% with i_url="quotas" %} + Service Quotas + + {% endwith %} +
  • diff --git a/frontend/templates/modals/view_quota_limit_info.html b/frontend/templates/modals/view_quota_limit_info.html new file mode 100644 index 00000000..4f077895 --- /dev/null +++ b/frontend/templates/modals/view_quota_limit_info.html @@ -0,0 +1,39 @@ +{% component_block "modal" start_open="true" %} + {% fill "id" %}modal_view_quota_limit_info_{{ quota.id }}{% endfill %} + {% fill "title" %} +

    + {{ quota.name }} +

    + {% endfill %} + {% fill "content" %} +
    +

    Description

    +

    {{ quota.description }}

    + +
    +

    Applied account-level quota value

    +

    {{ current_limit }}

    +
    + +
    +

    Default quota value

    +

    {{ quota.value }}

    +
    + +
    +

    Adjustability

    +

    {{ quota.adjustable }}

    +
    +
    + + + {% endfill %} +{% endcomponent_block %} diff --git a/frontend/templates/pages/quotas/_fetch_body.html b/frontend/templates/pages/quotas/_fetch_body.html new file mode 100644 index 00000000..28af8085 --- /dev/null +++ b/frontend/templates/pages/quotas/_fetch_body.html @@ -0,0 +1,30 @@ + +{% for quota in quotas %} + + + + + + {{ quota.1 }} + + + {{ quota.0.value }} + + + {% if quota.0.adjustable %} + Account Level + {% else %} + Not adjustable + {% endif %} + + +{% empty %} + No Quotas Found +{% endfor %} + diff --git a/frontend/templates/pages/quotas/dashboard.html b/frontend/templates/pages/quotas/dashboard.html new file mode 100644 index 00000000..6597e8f5 --- /dev/null +++ b/frontend/templates/pages/quotas/dashboard.html @@ -0,0 +1,24 @@ +{% extends 'base/base.html' %} +{% block content %} + +{% endblock content %} diff --git a/frontend/templates/pages/quotas/list.html b/frontend/templates/pages/quotas/list.html new file mode 100644 index 00000000..f834dbec --- /dev/null +++ b/frontend/templates/pages/quotas/list.html @@ -0,0 +1,25 @@ +{% extends 'base/base.html' %} +{% block content %} +
    +

    Service Quotas

    + +
    + + + + + + + + + + + {% include 'components/table/skeleton_rows.html' with rows=3 cols=5 %} + +
    Quota NameApplied account quota valueDefault quota valueAdjustability
    +
    +
    +{% endblock content %} From dd1c7e0a34d9bd47e3cd9c778fbe12a373d6e9e9 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:07:16 +0000 Subject: [PATCH 02/15] WIP feature: Started adding quotas. Now how a working quotas list page --- frontend/templates/pages/quotas/_fetch_body.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/templates/pages/quotas/_fetch_body.html b/frontend/templates/pages/quotas/_fetch_body.html index 28af8085..dfdf3c4a 100644 --- a/frontend/templates/pages/quotas/_fetch_body.html +++ b/frontend/templates/pages/quotas/_fetch_body.html @@ -5,6 +5,8 @@ - {{ quota.1 }} + {{ quota.period_usage }} - {{ quota.0.value }} + {{ quota.quota_limit }} - {% if quota.0.adjustable %} + {{ quota.quota_object.value }} + + + {% if quota.quota_object.adjustable %} Account Level {% else %} Not adjustable diff --git a/frontend/templates/pages/quotas/list.html b/frontend/templates/pages/quotas/list.html index f834dbec..aa0c1a81 100644 --- a/frontend/templates/pages/quotas/list.html +++ b/frontend/templates/pages/quotas/list.html @@ -8,6 +8,7 @@

    Service Quotas

    Quota Name + Current usage Applied account quota value Default quota value Adjustability From eedaac75c211555635589f175ce8af6261cfd5bb Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 18:06:34 +0100 Subject: [PATCH 04/15] fix: Fixed receipt not being deleted on first delete --- backend/signals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/signals.py b/backend/signals.py index a435845b..20176e80 100644 --- a/backend/signals.py +++ b/backend/signals.py @@ -5,7 +5,7 @@ cache: RedisCacheClient = cache from django.core.files.storage import default_storage -from django.db.models.signals import pre_save, post_delete, post_save, post_migrate +from django.db.models.signals import pre_save, post_delete, post_save, post_migrate, pre_delete from django.dispatch import receiver from django.urls import reverse @@ -36,8 +36,8 @@ def set_profile_picture_to_none(sender, instance, **kwargs): instance.profile_picture.delete(save=False) -@receiver(post_delete, sender=Receipt) -def set_profile_picture_to_none(sender, instance, **kwargs): +@receiver(pre_delete, sender=Receipt) +def delete_old_receipts(sender, instance, **kwargs): # Check if the file exists in the storage if instance.image and default_storage.exists(instance.image.name): instance.image.delete(save=False) From 1cbbd6a018f85465550914c46a9ae3f2c99ef84b Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 18:07:00 +0100 Subject: [PATCH 05/15] WIP Feature: Added quotas for invoices and receipts --- backend/admin.py | 8 ++- backend/api/invoices/delete.py | 10 ++-- backend/api/receipts/delete.py | 4 +- backend/api/receipts/new.py | 7 ++- backend/decorators.py | 19 +++++-- backend/models.py | 57 +++++++++++++++---- backend/utils.py | 33 ++++++++++- backend/views/core/invoices/create.py | 10 +++- backend/views/core/invoices/manage_access.py | 14 ++++- frontend/templates/base/toast.html | 1 + .../invoices/manage_access/_table_row.html | 2 +- .../templates/pages/quotas/_fetch_body.html | 1 + .../templates/pages/quotas/dashboard.html | 5 ++ 13 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 frontend/templates/base/toast.html diff --git a/backend/admin.py b/backend/admin.py index 2669cfa7..2cb9813e 100644 --- a/backend/admin.py +++ b/backend/admin.py @@ -23,7 +23,9 @@ InvoiceOnetimeSchedule, QuotaLimit, QuotaOverrides, - QuotaUsage + QuotaUsage, + Receipt, + ReceiptDownloadToken ) # from django.contrib.auth.models imp/ort User @@ -49,7 +51,9 @@ APIKey, InvoiceOnetimeSchedule, QuotaOverrides, - QuotaUsage + QuotaUsage, + Receipt, + ReceiptDownloadToken ] ) diff --git a/backend/api/invoices/delete.py b/backend/api/invoices/delete.py index 0074dbcf..3cf644f8 100644 --- a/backend/api/invoices/delete.py +++ b/backend/api/invoices/delete.py @@ -5,7 +5,7 @@ from django.urls.exceptions import Resolver404 from django.views.decorators.http import require_http_methods -from backend.models import Invoice +from backend.models import Invoice, QuotaLimit @require_http_methods(["DELETE"]) @@ -20,16 +20,14 @@ def delete_invoice(request: HttpRequest): except Invoice.DoesNotExist: return JsonResponse({"message": "Invoice not found"}, status=404) - if not invoice.user.logged_in_as_team and invoice.user != request.user: + if not invoice.has_access(request.user): return JsonResponse({"message": "You do not have permission to delete this invoice"}, status=404) - if invoice.user.logged_in_as_team and invoice.organization != request.user.logged_in_as_team: - return JsonResponse({"message": "You do not have permission to delete this invoice"}, status=404) + QuotaLimit.delete_quota_usage("invoices-count", request.user, invoice.id, invoice.date_created) invoice.delete() if request.htmx: - print("should send msg") if not redirect: messages.success(request, "Invoice deleted") return render(request, "base/toasts.html") @@ -40,6 +38,6 @@ def delete_invoice(request: HttpRequest): response["HX-Location"] = redirect return response except Resolver404: - ... + return HttpResponseRedirect(reverse("dashboard")) return JsonResponse({"message": "Invoice successfully deleted"}, status=200) diff --git a/backend/api/receipts/delete.py b/backend/api/receipts/delete.py index 55ebe036..33a9a397 100644 --- a/backend/api/receipts/delete.py +++ b/backend/api/receipts/delete.py @@ -5,7 +5,6 @@ from django.views.decorators.http import require_http_methods from backend.models import Receipt -from django.db.models import Q @require_http_methods(["DELETE"]) @@ -20,6 +19,9 @@ def receipt_delete(request: HttpRequest, id: int): elif receipt.user != request.user: return JsonResponse(status=403, data={"message": "Forbidden"}) + # QuotaLimit.delete_quota_usage("receipts-count", request.user, receipt.id, receipt.date_uploaded) # Don't want to delete receipts + # from records because it does cost us PER receipt. So makes sense not to allow Upload, delete, upload .. etc + receipt.delete() messages.success(request, "Receipt deleted") return render( diff --git a/backend/api/receipts/new.py b/backend/api/receipts/new.py index 989c8694..9c5df029 100644 --- a/backend/api/receipts/new.py +++ b/backend/api/receipts/new.py @@ -1,14 +1,15 @@ from django.contrib import messages -from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpRequest, HttpResponseBadRequest from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.models import Receipt +from backend.decorators import quota_usage_check +from backend.models import Receipt, QuotaUsage @require_http_methods(["POST"]) +@quota_usage_check("receipts-count", api=True, htmx=True) @login_required def receipt_create(request: HttpRequest): if not request.htmx: @@ -42,12 +43,14 @@ def receipt_create(request: HttpRequest): total_price=total_price, ) + if request.user.logged_in_as_team: receipt.organization = request.user.logged_in_as_team else: receipt.user = request.user receipt.save() + QuotaUsage.create_str(request.user, "receipts-count", receipt.id) # r = requests.post( # "https://ocr.asprise.com/api/receipt", # data={ diff --git a/backend/decorators.py b/backend/decorators.py index c2ddb8eb..60c32ba0 100644 --- a/backend/decorators.py +++ b/backend/decorators.py @@ -4,6 +4,7 @@ from django.contrib import messages from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, render +from django.urls import reverse from backend.models import QuotaLimit from backend.utils import get_feature_status @@ -56,7 +57,11 @@ def wrapper(request, *args, **kwargs): elif api: return HttpResponse(status=403, content="This feature is currently disabled.") messages.error(request, "This feature is currently disabled.") - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + referer = request.META.get("HTTP_REFERER") + current_url = request.build_absolute_uri() + if referer != current_url: + return HttpResponseRedirect(referer) + return HttpResponseRedirect(reverse("dashboard")) return wrapper @@ -72,18 +77,20 @@ def wrapper(request, *args, **kwargs): except QuotaLimit.DoesNotExist: return view_func(request, *args, **kwargs) - print(quota_limit.strict_goes_above_limit(request.user, extra=extra_data)) - if not quota_limit.strict_goes_above_limit(request.user, extra=extra_data): return view_func(request, *args, **kwargs) if api and htmx: messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") - return render(request, "base/toasts.html") + return render(request, "base/toast.html") elif api: - return HttpResponse(status=403, content="fYou have reached the quota limit for this service '{quota_limit.slug}'") + return HttpResponse(status=403, content=f"You have reached the quota limit for this service '{quota_limit.slug}'") messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + referer = request.META.get("HTTP_REFERER") + current_url = request.build_absolute_uri() + if referer != current_url: + return HttpResponseRedirect(referer) + return HttpResponseRedirect(reverse("dashboard")) return wrapper diff --git a/backend/models.py b/backend/models.py index 310ec03d..ba89416c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,6 +1,6 @@ from datetime import datetime from decimal import Decimal -from typing import Optional +from typing import Optional, NoReturn, Union from uuid import uuid4 from django.contrib.auth.hashers import make_password, check_password @@ -596,21 +596,56 @@ def get_period_usage(self, user: User): return "Not available" def strict_goes_above_limit(self, user: User, extra: Optional[str | int] = None) -> bool: + current = self.strict_get_quotas(user, extra) + current = current.count() if current else None + return current >= self.get_quota_limit(user) if current else False + + def strict_get_quotas(self, user: User, extra: Optional[str | int] = None): current = None if self.limit_type == "forever": - current = self.quota_usage.filter(user=user, quota_limit=self).count() + current = self.quota_usage.filter(user=user, quota_limit=self) elif self.limit_type == "per_month": - current = self.quota_usage.filter(user=user, quota_limit=self, created_at__month=datetime.now().month).count() + current_month = timezone.now().month + current_year = timezone.now().year + current = self.quota_usage.filter(user=user, quota_limit=self, created_at__year=current_year, created_at__month=current_month) elif self.limit_type == "per_day": - current = self.quota_usage.filter(user=user, quota_limit=self, created_at__day=datetime.now().day).count() - elif self.limit_type == "per_client": - current = self.quota_usage.filter(user=user, quota_limit=self, - extra_data__exact=extra).count() - elif self.limit_type == "per_invoice": - current = self.quota_usage.filter(user=user, quota_limit=self, - extra_data__exact=extra).count() + current_day = timezone.now().day + current_month = timezone.now().month + current_year = timezone.now().year + current = self.quota_usage.filter(user=user, quota_limit=self, created_at__year=current_year, created_at__month=current_month, + created_at__day=current_day) + elif self.limit_type in ["per_client", "per_invoice"]: + current = self.quota_usage.filter(user=user, quota_limit=self, extra_data__exact=extra) + return current - return current >= self.get_quota_limit(user) if current else False + @classmethod + def delete_quota_usage(cls, quota_limit: Union[str, "QuotaLimit"], user: User, extra, timestamp=None) -> NoReturn: + quota_limit = cls.objects.get(slug=quota_limit) if isinstance(quota_limit, str) else quota_limit + + all_usages = quota_limit.strict_get_quotas(user, extra) + + if all_usages.count() > 1 and timestamp: + earliest: QuotaUsage = all_usages.filter(created_at__gte=timestamp).order_by("created_at").first() + latest: QuotaUsage = all_usages.filter(created_at__lte=timestamp).order_by("created_at").last() + + if earliest and latest: + time_until_soonest_obj = abs(earliest.created_at - timestamp) + time_since_most_recent_obj = abs(latest.created_at - timestamp) + if time_until_soonest_obj < time_since_most_recent_obj: + closest_obj = earliest + else: + closest_obj = latest + + if earliest and latest and closest_obj: + closest_obj.delete() + elif all_usages.count() > 1: + earliest = all_usages.order_by("created_at").first() + if earliest: + earliest.delete() + else: + first = all_usages.first() + if first: + first.delete() class QuotaOverrides(models.Model): diff --git a/backend/utils.py b/backend/utils.py index 24406f49..6b810604 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -1,9 +1,16 @@ +from typing import Optional + +from django.contrib import messages from django.core.cache import cache from django.core.cache.backends.redis import RedisCacheClient +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse cache: RedisCacheClient = cache -from backend.models import FeatureFlags +from backend.models import FeatureFlags, QuotaLimit + def get_feature_status(feature, should_use_cache=True): if should_use_cache: @@ -19,3 +26,27 @@ def get_feature_status(feature, should_use_cache=True): return value.value else: return False + + +def quota_usage_check_under( + request, limit: str | QuotaLimit, extra_data: Optional[str | int] = None, api=False, htmx=False +) -> bool | HttpResponse | HttpResponseRedirect: + try: + quota_limit = QuotaLimit.objects.get(slug=limit) if isinstance(limit, str) else limit + except QuotaLimit.DoesNotExist: + return True + + if not quota_limit.strict_goes_above_limit(request.user, extra=extra_data): + return True + + if api and htmx: + messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") + return render(request, "base/toast.html") + elif api: + return HttpResponse(status=403, content=f"You have reached the quota limit for this service '{quota_limit.slug}'") + messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") + referer = request.META.get("HTTP_REFERER") + current_url = request.build_absolute_uri() + if referer != current_url: + return HttpResponseRedirect(referer) + return HttpResponseRedirect(reverse("dashboard")) diff --git a/backend/views/core/invoices/create.py b/backend/views/core/invoices/create.py index d9fbfdc3..c02e99de 100644 --- a/backend/views/core/invoices/create.py +++ b/backend/views/core/invoices/create.py @@ -5,10 +5,15 @@ from django.shortcuts import render, redirect from django.views.decorators.http import require_http_methods -from backend.models import Invoice, InvoiceItem, Client, InvoiceProduct +from backend.decorators import quota_usage_check +from backend.models import Invoice, InvoiceItem, Client, InvoiceProduct, QuotaUsage +from backend.utils import quota_usage_check_under def invoice_page_get(request: HttpRequest): + check_usage = quota_usage_check_under(request, "invoices-count") + if not isinstance(check_usage, bool): + return check_usage context = { "clients": Client.objects.filter(user=request.user), "existing_products": InvoiceProduct.objects.filter(user=request.user), @@ -16,6 +21,7 @@ def invoice_page_get(request: HttpRequest): return render(request, "pages/invoices/create/create.html", context) +@quota_usage_check("invoices-count") def invoice_page_post(request: HttpRequest): invoice_items = [ InvoiceItem.objects.create(name=row[0], description=row[1], hours=row[2], price_per_hour=row[3]) @@ -78,6 +84,8 @@ def invoice_page_post(request: HttpRequest): invoice.save() invoice.items.set(invoice_items) + QuotaUsage.create_str(request.user, "invoices-count", invoice.id) + return redirect("invoices:dashboard") diff --git a/backend/views/core/invoices/manage_access.py b/backend/views/core/invoices/manage_access.py index 518f84ba..f6854309 100644 --- a/backend/views/core/invoices/manage_access.py +++ b/backend/views/core/invoices/manage_access.py @@ -2,7 +2,8 @@ from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, render -from backend.models import Invoice, InvoiceURL +from backend.decorators import quota_usage_check +from backend.models import Invoice, InvoiceURL, QuotaUsage, QuotaLimit def manage_access(request: HttpRequest, invoice_id): @@ -21,6 +22,7 @@ def manage_access(request: HttpRequest, invoice_id): ) +@quota_usage_check("invoices-access_codes", 1, api=True, htmx=True) def create_code(request: HttpRequest, invoice_id): if not request.htmx: return redirect("invoices:dashboard") @@ -36,6 +38,9 @@ def create_code(request: HttpRequest, invoice_id): code = InvoiceURL.objects.create(invoice=invoice, created_by=request.user) messages.success(request, "Successfully created code") + + QuotaUsage.create_str(request.user, "invoices-access_codes", invoice_id) + return render( request, "pages/invoices/manage_access/_table_row.html", @@ -49,13 +54,16 @@ def delete_code(request: HttpRequest, code): try: code_obj = InvoiceURL.objects.get(uuid=code) - invoice = Invoice.objects.get(id=code_obj.invoice.id, user=request.user) + invoice = Invoice.objects.get(id=code_obj.invoice.id) + if not invoice.has_access(request.user): + raise Invoice.DoesNotExist except (Invoice.DoesNotExist, InvoiceURL.DoesNotExist): return redirect("invoices:dashboard") + QuotaLimit.delete_quota_usage("invoices-access_codes", request.user, invoice.id, code_obj.created_on) + code_obj.delete() - # return HttpResponse("", status=200) messages.success(request, "Successfully deleted code") return render( request, diff --git a/frontend/templates/base/toast.html b/frontend/templates/base/toast.html new file mode 100644 index 00000000..5c5a96fc --- /dev/null +++ b/frontend/templates/base/toast.html @@ -0,0 +1 @@ +{% component "messages_list" %} \ No newline at end of file diff --git a/frontend/templates/pages/invoices/manage_access/_table_row.html b/frontend/templates/pages/invoices/manage_access/_table_row.html index 4410c10a..55537985 100644 --- a/frontend/templates/pages/invoices/manage_access/_table_row.html +++ b/frontend/templates/pages/invoices/manage_access/_table_row.html @@ -18,4 +18,4 @@ {% if added %}{% endif %} {% endif %} -{% component "messages_list" %} +{% component "messages_list" %} \ No newline at end of file diff --git a/frontend/templates/pages/quotas/_fetch_body.html b/frontend/templates/pages/quotas/_fetch_body.html index a181bb12..4309ae1a 100644 --- a/frontend/templates/pages/quotas/_fetch_body.html +++ b/frontend/templates/pages/quotas/_fetch_body.html @@ -33,3 +33,4 @@ No Quotas Found {% endfor %} +{% component "messages_list" %} \ No newline at end of file diff --git a/frontend/templates/pages/quotas/dashboard.html b/frontend/templates/pages/quotas/dashboard.html index 6597e8f5..02d52526 100644 --- a/frontend/templates/pages/quotas/dashboard.html +++ b/frontend/templates/pages/quotas/dashboard.html @@ -19,6 +19,11 @@

    Teams

    + +

    + Receipts +

    +
    {% endblock content %} From 43bcf6d60e7fcd451596f56a1cfa0c3a769a29ce Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:41:11 +0100 Subject: [PATCH 06/15] WIP Feature: Added quotas for teams --- backend/api/teams/create.py | 9 ++++-- backend/api/teams/invites.py | 31 +++++++++++++++---- backend/api/teams/leave.py | 1 + backend/data/default_quota_limits.py | 3 +- backend/models.py | 2 +- backend/views/core/settings/teams.py | 5 ++- components/+messages_list.html | 4 ++- .../modals/view_quota_limit_info.html | 2 +- .../templates/pages/quotas/_fetch_body.html | 10 +++--- frontend/templates/pages/quotas/list.html | 17 +++++++--- .../templates/partials/messages_list.html | 2 +- 11 files changed, 60 insertions(+), 26 deletions(-) diff --git a/backend/api/teams/create.py b/backend/api/teams/create.py index 75c74c79..14a77c0e 100644 --- a/backend/api/teams/create.py +++ b/backend/api/teams/create.py @@ -1,12 +1,12 @@ from django.http import HttpRequest -from django.shortcuts import render from django.views.decorators.http import require_POST from backend.decorators import * -from backend.models import Team +from backend.models import Team, QuotaUsage @require_POST +@quota_usage_check("teams-count", api=True, htmx=True) def create_team(request: HttpRequest): name = request.POST.get("name") @@ -15,6 +15,11 @@ def create_team(request: HttpRequest): return render(request, "partials/messages_list.html") team = Team.objects.create(name=name, leader=request.user) + + QuotaUsage.create_str(request.user, "teams-count", team.id) + QuotaUsage.create_str(request.user, "teams-joined", team.id) + + if not request.user.logged_in_as_team: request.user.logged_in_as_team = team request.user.save() diff --git a/backend/api/teams/invites.py b/backend/api/teams/invites.py index 75386025..cf146970 100644 --- a/backend/api/teams/invites.py +++ b/backend/api/teams/invites.py @@ -1,6 +1,4 @@ -from django.http import HttpRequest, HttpResponse -from django.shortcuts import render -from django.urls import reverse +from django.http import HttpRequest from backend.decorators import * from backend.models import Notification, Team, TeamInvitation, User @@ -29,6 +27,15 @@ def check_team_invitation_is_valid(request, invitation: TeamInvitation, code=Non valid = False messages.error(request, "Invitation has expired") + try: + quota_limit = QuotaLimit.objects.get(slug="teams-user_count") + if invitation.team.members.count() >= quota_limit.get_quota_limit(invitation.team.leader): + valid = False + messages.error(request, "Unfortunately this team is currently full") + except QuotaLimit.DoesNotExist: + valid = False + messages.error(request, "Something went wrong with fetching the quota limit") + if not valid: delete_notification(request.user, code) return False @@ -41,12 +48,14 @@ def send_user_team_invite(request: HttpRequest): team_id = request.POST.get("team_id") team = Team.objects.filter(leader=request.user, id=team_id).first() - def return_error_notif(request: HttpRequest, message: str): + def return_error_notif(request: HttpRequest, message: str, autohide=None): messages.error(request, message) - resp = render(request, "partials/messages_list.html", status=200) + context = {"autohide": False} if autohide == False else {} + resp = render(request, "partials/messages_list.html", context=context, status=200) resp["HX-Trigger-After-Swap"] = "invite_user_error" return resp + if not user_email: return return_error_notif(request, "Please enter a valid user email") @@ -61,6 +70,16 @@ def return_error_notif(request: HttpRequest, message: str): if user.teams_joined.exists(): return return_error_notif(request, "User already is in this team") + try: + quota_limit = QuotaLimit.objects.get(slug="teams-user_count") + if team.members.count() >= quota_limit.get_quota_limit(team.leader): + return return_error_notif(request, "Unfortunately your team has reached the maximum members limit. Go to the service quotas " + "page to request a higher number or kick some users to make space.", autohide=False) + except QuotaLimit.DoesNotExist: + return return_error_notif(request, "Something went wrong with fetching the quota limit") + + + invitation = TeamInvitation.objects.create(team=team, user=user, invited_by=request.user) # if EMAIL_SERVER_ENABLED and EMAIL_FROM_ADDRESS: @@ -96,7 +115,7 @@ def return_error_notif(request: HttpRequest, message: str): def accept_team_invite(request: HttpRequest, code): - invitation: TeamInvitation = TeamInvitation.objects.filter(code=code).first() + invitation: TeamInvitation = TeamInvitation.objects.filter(code=code).prefetch_related("team").first() if not check_team_invitation_is_valid(request, invitation, code): messages.error(request, "Invalid invite - Maybe it has expired?") diff --git a/backend/api/teams/leave.py b/backend/api/teams/leave.py index 4a31295c..bf140ac0 100644 --- a/backend/api/teams/leave.py +++ b/backend/api/teams/leave.py @@ -1,3 +1,4 @@ +from django.contrib import messages from django.http import HttpRequest, HttpResponse from django.shortcuts import render diff --git a/backend/data/default_quota_limits.py b/backend/data/default_quota_limits.py index 0ade9b97..869473e3 100644 --- a/backend/data/default_quota_limits.py +++ b/backend/data/default_quota_limits.py @@ -55,7 +55,6 @@ class QuotaGroup: adjustable=True ) ]), - QuotaGroup("clients", [ QuotaItem( slug="count", @@ -78,7 +77,7 @@ class QuotaGroup: QuotaItem( slug="joined", name="Joined teams", - description="Amount of teams joined in total", + description="Amount of teams that you have joined in total", default_value=5, period="forever", adjustable=True diff --git a/backend/models.py b/backend/models.py index ba89416c..55beab36 100644 --- a/backend/models.py +++ b/backend/models.py @@ -614,7 +614,7 @@ def strict_get_quotas(self, user: User, extra: Optional[str | int] = None): current_year = timezone.now().year current = self.quota_usage.filter(user=user, quota_limit=self, created_at__year=current_year, created_at__month=current_month, created_at__day=current_day) - elif self.limit_type in ["per_client", "per_invoice"]: + elif self.limit_type in ["per_client", "per_invoice", "per_team", "per_receipt"]: current = self.quota_usage.filter(user=user, quota_limit=self, extra_data__exact=extra) return current diff --git a/backend/views/core/settings/teams.py b/backend/views/core/settings/teams.py index 5f18b1e0..49eb2135 100644 --- a/backend/views/core/settings/teams.py +++ b/backend/views/core/settings/teams.py @@ -1,5 +1,3 @@ -from typing import Optional - from django.db.models import When, Case, BooleanField from django.http import HttpRequest from django.shortcuts import render @@ -12,7 +10,8 @@ def teams_dashboard(request: HttpRequest): users_team: Optional[Team] = request.user.logged_in_as_team if not users_team: - user_with_counts = User.objects.prefetch_related("teams_joined", "teams_leader_of").get(pk=request.user.pk) + user_with_counts = User.objects.prefetch_related("teams_joined", "teams_leader_of").get( + pk=request.user.pk) return render( request, "pages/settings/teams/main.html", diff --git a/components/+messages_list.html b/components/+messages_list.html index 844415ca..17df1c57 100644 --- a/components/+messages_list.html +++ b/components/+messages_list.html @@ -12,13 +12,15 @@ {{ message }} {% endautoescape %} + {% if autohide != False %} + {% endif %} {% endfor %} {% if with_js %} diff --git a/frontend/templates/modals/view_quota_limit_info.html b/frontend/templates/modals/view_quota_limit_info.html index 4f077895..7ba90e7b 100644 --- a/frontend/templates/modals/view_quota_limit_info.html +++ b/frontend/templates/modals/view_quota_limit_info.html @@ -2,7 +2,7 @@ {% fill "id" %}modal_view_quota_limit_info_{{ quota.id }}{% endfill %} {% fill "title" %}

    - {{ quota.name }} + {{ quota.name }} - ({{ quota.slug }})

    {% endfill %} {% fill "content" %} diff --git a/frontend/templates/pages/quotas/_fetch_body.html b/frontend/templates/pages/quotas/_fetch_body.html index 4309ae1a..484b6250 100644 --- a/frontend/templates/pages/quotas/_fetch_body.html +++ b/frontend/templates/pages/quotas/_fetch_body.html @@ -12,16 +12,16 @@ {{ quota.quota_object.name }} - + {{ quota.period_usage }} - + {{ quota.quota_limit }} - + {{ quota.quota_object.value }} - + {% if quota.quota_object.adjustable %} Account Level {% else %} @@ -33,4 +33,4 @@ No Quotas Found {% endfor %} -{% component "messages_list" %} \ No newline at end of file +{% component "messages_list" %} diff --git a/frontend/templates/pages/quotas/list.html b/frontend/templates/pages/quotas/list.html index aa0c1a81..a4a9beb6 100644 --- a/frontend/templates/pages/quotas/list.html +++ b/frontend/templates/pages/quotas/list.html @@ -1,16 +1,25 @@ {% extends 'base/base.html' %} {% block content %}
    -

    Service Quotas

    +
    + +

    Service Quotas

    +
    +
    +
    - - - + + + diff --git a/frontend/templates/partials/messages_list.html b/frontend/templates/partials/messages_list.html index bb637a64..dd30e4e3 100644 --- a/frontend/templates/partials/messages_list.html +++ b/frontend/templates/partials/messages_list.html @@ -1 +1 @@ -{% component "messages_list" %} +{% if autohide != False %}{% component "messages_list" %}{% else %}{% component "messages_list" autohide=False %}{% endif %} \ No newline at end of file From 53a8737f5f29197c5f0e766fc5b5cdb0a6db1fc2 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 21:14:13 +0100 Subject: [PATCH 07/15] WIP Feature: Added quotas for ... quotas --- backend/admin.py | 2 + backend/api/quotas/submit_request.py | 73 +++++++++++++++ backend/api/quotas/urls.py | 7 +- backend/data/default_quota_limits.py | 12 ++- backend/decorators.py | 2 +- ...ncreaserequest_quotaoverrides_and_more.py} | 24 ++++- backend/models.py | 25 ++++- backend/utils.py | 2 +- .../modals/view_quota_limit_info.html | 92 ++++++++++++++----- 9 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 backend/api/quotas/submit_request.py rename backend/migrations/{0027_quotalimit_quotaoverrides_quotausage.py => 0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py} (75%) diff --git a/backend/admin.py b/backend/admin.py index 2cb9813e..1f599845 100644 --- a/backend/admin.py +++ b/backend/admin.py @@ -24,6 +24,7 @@ QuotaLimit, QuotaOverrides, QuotaUsage, + QuotaIncreaseRequest, Receipt, ReceiptDownloadToken ) @@ -52,6 +53,7 @@ InvoiceOnetimeSchedule, QuotaOverrides, QuotaUsage, + QuotaIncreaseRequest, Receipt, ReceiptDownloadToken ] diff --git a/backend/api/quotas/submit_request.py b/backend/api/quotas/submit_request.py new file mode 100644 index 00000000..667bbf40 --- /dev/null +++ b/backend/api/quotas/submit_request.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass +from typing import Union + +from django.contrib import messages +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect, render + +from backend.models import QuotaIncreaseRequest, QuotaLimit, QuotaUsage +from backend.utils import quota_usage_check_under + + +def submit_request(request: HttpRequest, slug) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + + new_value = request.POST.get("new_value") + reason = request.POST.get("reason") + + try: + quota_limit = QuotaLimit.objects.get(slug=slug) + except QuotaLimit.DoesNotExist: + return error(request, "Failed to get the quota limit type") + + check_usage = quota_usage_check_under(request, "quota_increase-request", extra_data=quota_limit.id, api=True, htmx=True) + + if not isinstance(check_usage, bool): + return check_usage + + current = quota_limit.get_quota_limit(request.user) + + validate = validate_request(new_value, reason, current) + + if isinstance(validate, Error): + return error(request, validate.message) + + QuotaIncreaseRequest.objects.create( + user=request.user, + quota_limit=quota_limit, + new_value=new_value, + current_value=current + ) + + QuotaUsage.create_str(request.user, "quota_increase-request", quota_limit.id) + + messages.success(request, "Successfully submitted a quota increase request") + return render(request, "partials/messages_list.html") + + +@dataclass +class Error: + message: str + + +def error(request: HttpRequest, message: str) -> HttpResponse: + messages.error(request, message) + return render(request, "partials/messages_list.html") + + +def validate_request(new_value, reason, current) -> Union[True, Error]: + if not new_value: + return Error("Please enter a valid increase value") + + try: + new_value = int(new_value) + if new_value <= current: + raise ValueError + except ValueError: + return Error("Please enter a valid increase value that is above your current limit.") + + if len(reason) < 25: + return Error("Please enter a valid reason for the increase.") + + return True diff --git a/backend/api/quotas/urls.py b/backend/api/quotas/urls.py index d546b77e..107877cd 100644 --- a/backend/api/quotas/urls.py +++ b/backend/api/quotas/urls.py @@ -1,12 +1,17 @@ from django.urls import path -from . import fetch +from . import fetch, submit_request urlpatterns = [ path( "fetch//", fetch.fetch_all_quotas, name="fetch", + ), + path( + "submit_request//", + submit_request.submit_request, + name="submit_request" ) ] diff --git a/backend/data/default_quota_limits.py b/backend/data/default_quota_limits.py index 869473e3..adacc333 100644 --- a/backend/data/default_quota_limits.py +++ b/backend/data/default_quota_limits.py @@ -9,7 +9,7 @@ class QuotaItem: description: str default_value: int adjustable: bool - period: Literal["forever", "per_month", "per_day", "per_client", "per_invoice", "per_team"] + period: Literal["forever", "per_month", "per_day", "per_client", "per_invoice", "per_team", "per_quota"] @dataclass @@ -90,5 +90,15 @@ class QuotaGroup: period="per_team", adjustable=True ) + ]), + QuotaGroup("quota_increase", [ + QuotaItem( + slug="request", + name="Quota Increase Request", + description="Amount of increase requests allowed PER quota", + default_value=1, + period="per_quota", + adjustable=False + ) ]) ] diff --git a/backend/decorators.py b/backend/decorators.py index 60c32ba0..17e45ab8 100644 --- a/backend/decorators.py +++ b/backend/decorators.py @@ -82,7 +82,7 @@ def wrapper(request, *args, **kwargs): if api and htmx: messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") - return render(request, "base/toast.html") + return render(request, "partials/messages_list.html", {"autohide": False}) elif api: return HttpResponse(status=403, content=f"You have reached the quota limit for this service '{quota_limit.slug}'") messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") diff --git a/backend/migrations/0027_quotalimit_quotaoverrides_quotausage.py b/backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py similarity index 75% rename from backend/migrations/0027_quotalimit_quotaoverrides_quotausage.py rename to backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py index cb0552bc..e074936c 100644 --- a/backend/migrations/0027_quotalimit_quotaoverrides_quotausage.py +++ b/backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.3 on 2024-03-31 13:12 +# Generated by Django 5.0.3 on 2024-03-31 20:13 import django.db.models.deletion from django.conf import settings @@ -31,6 +31,7 @@ class Migration(migrations.Migration): ("per_client", "Per Client"), ("per_invoice", "Per Invoice"), ("per_team", "Per Team"), + ("per_quota", "Per Quota"), ("forever", "Forever"), ], default="per_month", @@ -43,6 +44,27 @@ class Migration(migrations.Migration): "verbose_name_plural": "Quota Limits", }, ), + migrations.CreateModel( + name="QuotaIncreaseRequest", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("new_value", models.IntegerField()), + ("current_value", models.IntegerField()), + ("updated_at", models.DateTimeField(auto_now=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "quota_limit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="quota_increase_requests", to="backend.quotalimit" + ), + ), + ], + options={ + "verbose_name": "Quota Increase Request", + "verbose_name_plural": "Quota Increase Requests", + }, + ), migrations.CreateModel( name="QuotaOverrides", fields=[ diff --git a/backend/models.py b/backend/models.py index 55beab36..b2c26ac8 100644 --- a/backend/models.py +++ b/backend/models.py @@ -561,6 +561,7 @@ class LimitTypes(models.TextChoices): PER_CLIENT = "per_client" PER_INVOICE = "per_invoice" PER_TEAM = "per_team" + PER_QUOTA = "per_quota" FOREVER = "forever" slug = models.CharField(max_length=100, unique=True, editable=False) @@ -614,7 +615,7 @@ def strict_get_quotas(self, user: User, extra: Optional[str | int] = None): current_year = timezone.now().year current = self.quota_usage.filter(user=user, quota_limit=self, created_at__year=current_year, created_at__month=current_month, created_at__day=current_day) - elif self.limit_type in ["per_client", "per_invoice", "per_team", "per_receipt"]: + elif self.limit_type in ["per_client", "per_invoice", "per_team", "per_receipt", "per_quota"]: current = self.quota_usage.filter(user=user, quota_limit=self, extra_data__exact=extra) return current @@ -623,6 +624,7 @@ def delete_quota_usage(cls, quota_limit: Union[str, "QuotaLimit"], user: User, e quota_limit = cls.objects.get(slug=quota_limit) if isinstance(quota_limit, str) else quota_limit all_usages = quota_limit.strict_get_quotas(user, extra) + closest_obj = None if all_usages.count() > 1 and timestamp: earliest: QuotaUsage = all_usages.filter(created_at__gte=timestamp).order_by("created_at").first() @@ -697,3 +699,24 @@ def get_usage(self, user: User, limit: str | QuotaLimit): return "Not Found" return self.objects.filter(user=user, quota_limit=ql).count() + + +class QuotaIncreaseRequest(models.Model): + class StatusTypes(models.TextChoices): + PENDING = "pending" + APPROVED = "approved" + REJECTED = "rejected" + + user = models.ForeignKey(User, on_delete=models.CASCADE) + quota_limit = models.ForeignKey(QuotaLimit, on_delete=models.CASCADE, related_name="quota_increase_requests") + new_value = models.IntegerField() + current_value = models.IntegerField() + updated_at = models.DateTimeField(auto_now=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "Quota Increase Request" + verbose_name_plural = "Quota Increase Requests" + + def __str__(self): + return f"{self.user}" diff --git a/backend/utils.py b/backend/utils.py index 6b810604..ff24ff0a 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -41,7 +41,7 @@ def quota_usage_check_under( if api and htmx: messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") - return render(request, "base/toast.html") + return render(request, "partials/messages_list.html", {"autohide": False}) elif api: return HttpResponse(status=403, content=f"You have reached the quota limit for this service '{quota_limit.slug}'") messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") diff --git a/frontend/templates/modals/view_quota_limit_info.html b/frontend/templates/modals/view_quota_limit_info.html index 7ba90e7b..f337db2a 100644 --- a/frontend/templates/modals/view_quota_limit_info.html +++ b/frontend/templates/modals/view_quota_limit_info.html @@ -6,34 +6,78 @@

    {% endfill %} {% fill "content" %} -
    -

    Description

    -

    {{ quota.description }}

    + + {% csrf_token %} +
    +

    Description

    +

    {{ quota.description }}

    -
    -

    Applied account-level quota value

    -

    {{ current_limit }}

    -
    +
    Quota NameCurrent usageApplied account quota valueDefault quota valueCurrent usageApplied account quota valueDefault quota value Adjustability
    + + + + + + + + + + + + + + + + + + + + +
    TypeValue
    Applied account-level quota value{{ current_limit }}
    Default quota value{{ quota.value }}
    Adjustable{{ quota.adjustable }}
    -
    -

    Default quota value

    -

    {{ quota.value }}

    -
    -
    -

    Adjustability

    -

    {{ quota.adjustable }}

    + {% if quota.adjustable %} +
    Submit Request
    +
    + + +
    +
    + + +
    + {% endif %}
    -
    - + + {% endfill %} {% endcomponent_block %} From c5d44549aad4f592e37e719aec1721738bc4220a Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:35:05 +0100 Subject: [PATCH 08/15] Feature: Added quotas --- assets/data/default_quota_limits.json | 60 --------- backend/api/quotas/requests.py | 124 ++++++++++++++++++ backend/api/quotas/submit_request.py | 73 ----------- backend/api/quotas/urls.py | 14 +- backend/data/default_quota_limits.py | 10 +- ...increaserequest_quotaoverrides_and_more.py | 10 +- backend/models.py | 1 + backend/signals.py | 6 +- backend/urls.py | 3 +- backend/views/core/quotas/view.py | 10 +- .../templates/pages/quotas/dashboard.html | 35 ++--- .../templates/pages/quotas/view_requests.html | 50 +++++++ 12 files changed, 240 insertions(+), 156 deletions(-) delete mode 100644 assets/data/default_quota_limits.json create mode 100644 backend/api/quotas/requests.py delete mode 100644 backend/api/quotas/submit_request.py create mode 100644 frontend/templates/pages/quotas/view_requests.html diff --git a/assets/data/default_quota_limits.json b/assets/data/default_quota_limits.json deleted file mode 100644 index ca1d34b4..00000000 --- a/assets/data/default_quota_limits.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "invoices": { - "count": { - "name": "Creations per month", - "description": "Amount of invoice creations allowed per month", - "default_value": 100, - "adjustable": true - }, - "schedules": { - "name": "Schedules per month", - "description": "Amount of invoice scheduled sends allowed per month", - "default_value": 100, - "adjustable": true - }, - "access_codes": { - "name": "Access codes", - "description": " Amount of invoice access codes allowed per invoice", - "default_value": 3, - "adjustable": true - } - }, - "receipts": { - "count": { - "Receipts stored per month": { - "name": "Receipts stored per month", - "description": "Amount of receipts stored per month", - "default_value": 100, - "adjustable": true - } - } - }, - "clients": { - "count": { - "name": "Stored clients", - "description": "Amount of clients stored in total", - "default_value": 40, - "adjustable": true - } - }, - "teams": { - "count": { - "name": "Created teams", - "description": "Amount of teams created in total", - "default_value": 3, - "adjustable": true - }, - "joined": { - "name": "Joined teams", - "description": "Amount of teams joined in total", - "default_value": 5, - "adjustable": true - }, - "user_count": { - "name": "Users per team", - "description": "Amount of users per team", - "default_value": 10, - "adjustable": true - } - } -} diff --git a/backend/api/quotas/requests.py b/backend/api/quotas/requests.py new file mode 100644 index 00000000..77ad1487 --- /dev/null +++ b/backend/api/quotas/requests.py @@ -0,0 +1,124 @@ +from dataclasses import dataclass +from typing import Union + +from django.contrib import messages +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods + +from backend.decorators import superuser_only +from backend.models import QuotaIncreaseRequest, QuotaLimit, QuotaUsage, QuotaOverrides +from backend.utils import quota_usage_check_under + + +def submit_request(request: HttpRequest, slug) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + + new_value = request.POST.get("new_value") + reason = request.POST.get("reason") + + try: + quota_limit = QuotaLimit.objects.get(slug=slug) + except QuotaLimit.DoesNotExist: + return error(request, "Failed to get the quota limit type") + + usage_per_item = quota_usage_check_under(request, "quota_increase-request", extra_data=quota_limit.id, api=True, htmx=True) + usage_per_month = quota_usage_check_under(request, "quota_increase-requests_per_month", extra_data=quota_limit.id, api=True, htmx=True) + + if not isinstance(usage_per_item, bool): + return usage_per_item + + if not isinstance(usage_per_month, bool): + return usage_per_month + + current = quota_limit.get_quota_limit(request.user) + + validate = validate_request(new_value, reason, current) + + if isinstance(validate, Error): + return error(request, validate.message) + + quota_increase_request = QuotaIncreaseRequest.objects.create( + user=request.user, + quota_limit=quota_limit, + new_value=new_value, + current_value=current + ) + + QuotaUsage.create_str(request.user, "quota_increase-request", quota_increase_request.id) + QuotaUsage.create_str(request.user, "quota_increase-requests_per_month", quota_increase_request.id) + + messages.success(request, "Successfully submitted a quota increase request") + return render(request, "partials/messages_list.html") + + +@dataclass +class Error: + message: str + + +def error(request: HttpRequest, message: str) -> HttpResponse: + messages.error(request, message) + return render(request, "partials/messages_list.html") + + +def validate_request(new_value, reason, current) -> Union[True, Error]: + if not new_value: + return Error("Please enter a valid increase value") + + try: + new_value = int(new_value) + if new_value <= current: + raise ValueError + except ValueError: + return Error("Please enter a valid increase value that is above your current limit.") + + if len(reason) < 25: + return Error("Please enter a valid reason for the increase.") + + return True + + +@superuser_only +@require_http_methods(["DELETE", "POST"]) +def approve_request(request: HttpRequest, request_id) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + try: + quota_request = QuotaIncreaseRequest.objects.get(id=request_id) + except QuotaIncreaseRequest.DoesNotExist: + return error(request, "Failed to get the quota increase request") + + QuotaOverrides.objects.filter(user=quota_request.user).delete() + + QuotaOverrides.objects.create( + user=quota_request.user, + value=quota_request.new_value, + quota_limit=quota_request.quota_limit, + ) + + quota_limit_for_increase = QuotaLimit.objects.get(slug="quota_increase-request") + QuotaUsage.objects.filter(user=quota_request.user, quota_limit=quota_limit_for_increase, extra_data=quota_request.id).delete() + quota_request.status = "approved" + quota_request.save() + + return HttpResponse(status=200) + + +@superuser_only +@require_http_methods(["DELETE", "POST"]) +def decline_request(request: HttpRequest, request_id) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + try: + quota_request = QuotaIncreaseRequest.objects.get(id=request_id) + except QuotaIncreaseRequest.DoesNotExist: + return error(request, "Failed to get the quota increase request") + + quota_limit_for_increase = QuotaLimit.objects.get(slug="quota_increase-request") + QuotaUsage.objects.filter(user=quota_request.user, quota_limit=quota_limit_for_increase, extra_data=quota_request.id).delete() + quota_request.status = "decline" + quota_request.save() + + return HttpResponse(status=200) diff --git a/backend/api/quotas/submit_request.py b/backend/api/quotas/submit_request.py deleted file mode 100644 index 667bbf40..00000000 --- a/backend/api/quotas/submit_request.py +++ /dev/null @@ -1,73 +0,0 @@ -from dataclasses import dataclass -from typing import Union - -from django.contrib import messages -from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect, render - -from backend.models import QuotaIncreaseRequest, QuotaLimit, QuotaUsage -from backend.utils import quota_usage_check_under - - -def submit_request(request: HttpRequest, slug) -> HttpResponse: - if not request.htmx: - return redirect("quotas") - - new_value = request.POST.get("new_value") - reason = request.POST.get("reason") - - try: - quota_limit = QuotaLimit.objects.get(slug=slug) - except QuotaLimit.DoesNotExist: - return error(request, "Failed to get the quota limit type") - - check_usage = quota_usage_check_under(request, "quota_increase-request", extra_data=quota_limit.id, api=True, htmx=True) - - if not isinstance(check_usage, bool): - return check_usage - - current = quota_limit.get_quota_limit(request.user) - - validate = validate_request(new_value, reason, current) - - if isinstance(validate, Error): - return error(request, validate.message) - - QuotaIncreaseRequest.objects.create( - user=request.user, - quota_limit=quota_limit, - new_value=new_value, - current_value=current - ) - - QuotaUsage.create_str(request.user, "quota_increase-request", quota_limit.id) - - messages.success(request, "Successfully submitted a quota increase request") - return render(request, "partials/messages_list.html") - - -@dataclass -class Error: - message: str - - -def error(request: HttpRequest, message: str) -> HttpResponse: - messages.error(request, message) - return render(request, "partials/messages_list.html") - - -def validate_request(new_value, reason, current) -> Union[True, Error]: - if not new_value: - return Error("Please enter a valid increase value") - - try: - new_value = int(new_value) - if new_value <= current: - raise ValueError - except ValueError: - return Error("Please enter a valid increase value that is above your current limit.") - - if len(reason) < 25: - return Error("Please enter a valid reason for the increase.") - - return True diff --git a/backend/api/quotas/urls.py b/backend/api/quotas/urls.py index 107877cd..d16b8986 100644 --- a/backend/api/quotas/urls.py +++ b/backend/api/quotas/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from . import fetch, submit_request +from . import fetch, requests urlpatterns = [ path( @@ -10,8 +10,18 @@ ), path( "submit_request//", - submit_request.submit_request, + requests.submit_request, name="submit_request" + ), + path( + "request//approve/", + requests.approve_request, + name="approve request" + ), + path( + "request//decline/", + requests.decline_request, + name="decline request" ) ] diff --git a/backend/data/default_quota_limits.py b/backend/data/default_quota_limits.py index adacc333..6e0f5dc7 100644 --- a/backend/data/default_quota_limits.py +++ b/backend/data/default_quota_limits.py @@ -95,10 +95,18 @@ class QuotaGroup: QuotaItem( slug="request", name="Quota Increase Request", - description="Amount of increase requests allowed PER quota", + description="Amount of increase requests allowed per quota", default_value=1, period="per_quota", adjustable=False + ), + QuotaItem( + slug="requests_per_month", + name="Quota Increase Requests per month", + description="Amount of increase requests allowed per month", + period="per_month", + default_value=1, + adjustable=False ) ]) ] diff --git a/backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py b/backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py index e074936c..5f486f65 100644 --- a/backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py +++ b/backend/migrations/0027_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.3 on 2024-03-31 20:13 +# Generated by Django 5.0.3 on 2024-03-31 20:27 import django.db.models.deletion from django.conf import settings @@ -52,6 +52,14 @@ class Migration(migrations.Migration): ("current_value", models.IntegerField()), ("updated_at", models.DateTimeField(auto_now=True)), ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField( + choices=[("pending", "Pending"), ("approved", "Approved"), ("rejected", "Rejected")], + default="pending", + max_length=20, + ), + ), ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ( "quota_limit", diff --git a/backend/models.py b/backend/models.py index b2c26ac8..dc03ffc5 100644 --- a/backend/models.py +++ b/backend/models.py @@ -713,6 +713,7 @@ class StatusTypes(models.TextChoices): current_value = models.IntegerField() updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True) + status = models.CharField(max_length=20, choices=StatusTypes.choices, default=StatusTypes.PENDING) class Meta: verbose_name = "Quota Increase Request" diff --git a/backend/signals.py b/backend/signals.py index 20176e80..5e9e3ab2 100644 --- a/backend/signals.py +++ b/backend/signals.py @@ -1,3 +1,5 @@ +import logging + from django.core.cache import cache from django.core.cache.backends.redis import RedisCacheClient @@ -79,7 +81,7 @@ def insert_initial_data(**kwargs): for group in default_quota_limits: for item in group.items: - QuotaLimit.objects.get_or_create( + _, created = QuotaLimit.objects.get_or_create( slug=f"{group.name}-{item.slug}", defaults={ "name": f"{item.name}", @@ -89,6 +91,8 @@ def insert_initial_data(**kwargs): "limit_type": item.period } ) + if created: + logging.info(f"Added QuotaLimit {item.name}") post_migrate.connect(insert_initial_data) diff --git a/backend/urls.py b/backend/urls.py index 4b7c9e56..44b01a1e 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -14,7 +14,7 @@ from backend.views.core.currency_converter import dashboard as cc_dashboard from backend.views.core.invoices.overview import manage_invoice from backend.views.core.other.index import index, dashboard -from backend.views.core.quotas.view import quotas_page, quotas_list +from backend.views.core.quotas.view import quotas_page, quotas_list, view_quota_increase_requests url( r"^frontend/static/(?P.*)$", @@ -30,6 +30,7 @@ path("dashboard/invoices/", include("backend.views.core.invoices.urls")), path("dashboard/quotas/", quotas_page, name="quotas"), path("dashboard/quotas//", quotas_list, name="quotas group"), + path("dashboard/admin/quota_requests/", view_quota_increase_requests, name="admin quota increase requests"), path( "dashboard/invoice//", manage_invoice, diff --git a/backend/views/core/quotas/view.py b/backend/views/core/quotas/view.py index 58ab1332..6ad4d2cd 100644 --- a/backend/views/core/quotas/view.py +++ b/backend/views/core/quotas/view.py @@ -1,7 +1,9 @@ from django.http import HttpResponse, HttpRequest - from django.shortcuts import render +from backend.decorators import superuser_only +from backend.models import QuotaIncreaseRequest + def quotas_page(request: HttpRequest) -> HttpResponse: return render(request, "pages/quotas/dashboard.html") @@ -9,3 +11,9 @@ def quotas_page(request: HttpRequest) -> HttpResponse: def quotas_list(request: HttpRequest, group: str) -> HttpResponse: return render(request, "pages/quotas/list.html", {"group": group}) + + +@superuser_only +def view_quota_increase_requests(request: HttpRequest) -> HttpResponse: + requests = QuotaIncreaseRequest.objects.filter(status="pending").order_by("-created_at") + return render(request, "pages/quotas/view_requests.html", {"requests": requests}) diff --git a/frontend/templates/pages/quotas/dashboard.html b/frontend/templates/pages/quotas/dashboard.html index 02d52526..eb8fafec 100644 --- a/frontend/templates/pages/quotas/dashboard.html +++ b/frontend/templates/pages/quotas/dashboard.html @@ -4,25 +4,28 @@

    Service Quotas

    diff --git a/frontend/templates/pages/quotas/view_requests.html b/frontend/templates/pages/quotas/view_requests.html new file mode 100644 index 00000000..cce725a8 --- /dev/null +++ b/frontend/templates/pages/quotas/view_requests.html @@ -0,0 +1,50 @@ +{% extends 'base/base.html' %} +{% block content %} +
    +

    View Requests

    + + + + + + + + + + + + + + + {% for request in requests %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    IDQuota ItemCurrent ValueNew ValueUser IDDateActions
    {{ request.id }}{{ request.quota_limit.name }}{{ request.current_value }}{{ request.new_value }}{{ request.user.id }}{{ request.created_at }} + + +
    + There are currently no submitted requests +
    +
    +{% endblock content %} From 419b213894051e7314292444670f3dd9f4a80931 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:43:46 +0100 Subject: [PATCH 09/15] Improved speed of migration signal --- backend/signals.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/backend/signals.py b/backend/signals.py index 5e9e3ab2..7869f836 100644 --- a/backend/signals.py +++ b/backend/signals.py @@ -81,17 +81,26 @@ def insert_initial_data(**kwargs): for group in default_quota_limits: for item in group.items: - _, created = QuotaLimit.objects.get_or_create( - slug=f"{group.name}-{item.slug}", - defaults={ - "name": f"{item.name}", - "value": item.default_value, - "adjustable": item.adjustable, - "description": item.description, - "limit_type": item.period - } - ) - if created: + existing = QuotaLimit.objects.filter(slug=f"{group.name}-{item.slug}").first() + if existing: + name, value, adjustable, description, limit_type = existing.name, existing.value, existing.adjustable, existing.description, existing.limit_type + existing.name = item.name + existing.value = item.default_value + existing.adjustable = item.adjustable + existing.description = item.description + existing.limit_type = item.period + if item.name != name or item.default_value != value or item.adjustable != adjustable or item.description != description or item.period != limit_type: + logging.info(f"Updated QuotaLimit {item.name}") + existing.save() + else: + QuotaLimit.objects.create( + name=item.name, + slug=f"{group.name}-{item.slug}", + value=item.default_value, + adjustable=item.adjustable, + description=item.description, + limit_type=item.period + ) logging.info(f"Added QuotaLimit {item.name}") From 691cb3f89c8e36b1fe2a9f21825510c591928656 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:47:38 +0100 Subject: [PATCH 10/15] WIP Feature: Button now closes modal if submit is valid --- frontend/templates/modals/view_quota_limit_info.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/templates/modals/view_quota_limit_info.html b/frontend/templates/modals/view_quota_limit_info.html index f337db2a..359915ec 100644 --- a/frontend/templates/modals/view_quota_limit_info.html +++ b/frontend/templates/modals/view_quota_limit_info.html @@ -7,7 +7,7 @@

    {% endfill %} {% fill "content" %}