Skip to content

Commit

Permalink
feature: quotas (#263)
Browse files Browse the repository at this point in the history
feature: Added service quota limits
  • Loading branch information
TreyWW authored Apr 1, 2024
1 parent 147cafb commit c81329b
Show file tree
Hide file tree
Showing 32 changed files with 1,048 additions and 42 deletions.
18 changes: 18 additions & 0 deletions backend/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
VerificationCodes,
APIKey,
InvoiceOnetimeSchedule,
QuotaLimit,
QuotaOverrides,
QuotaUsage,
QuotaIncreaseRequest,
Receipt,
ReceiptDownloadToken,
)

# from django.contrib.auth.models imp/ort User
Expand All @@ -45,9 +51,21 @@
VerificationCodes,
APIKey,
InvoiceOnetimeSchedule,
QuotaOverrides,
QuotaUsage,
QuotaIncreaseRequest,
Receipt,
ReceiptDownloadToken,
]
)


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")})
Expand Down
14 changes: 12 additions & 2 deletions backend/api/base/modal.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -51,10 +51,20 @@ 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, quota_limit=quota)
usage = quota.strict_get_quotas(user=request.user, quota_limit=quota)
context["quota_usage"] = usage.count() if usage != "Not Available" else "Not available"
print(context["quota_usage"])
except QuotaLimit.DoesNotExist:
...
else:
context[context_type] = context_value

return render(request, template_name, context)
except Exception as e:
except ValueError as e:
print(f"Something went wrong with loading modal {modal_name}. Error: {e}")
return HttpResponseBadRequest("Something went wrong")
10 changes: 4 additions & 6 deletions backend/api/invoices/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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")
Expand All @@ -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)
30 changes: 30 additions & 0 deletions backend/api/quotas/fetch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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", "quota_usage").order_by("-slug")

if search_text:
results = results.filter(Q(name__icontains=search_text))

quotas = [
{
"quota_limit": ql.get_quota_limit(request.user),
"period_usage": ql.get_period_usage(request.user),
"quota_object": ql,
}
for ql in results
]

context.update({"quotas": quotas})
return render(request, "pages/quotas/_fetch_body.html", context)
123 changes: 123 additions & 0 deletions backend/api/quotas/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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_per_quota", 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_per_quota", 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


@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)
16 changes: 16 additions & 0 deletions backend/api/quotas/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.urls import path

from . import fetch, requests

urlpatterns = [
path(
"fetch/<str:group>/",
fetch.fetch_all_quotas,
name="fetch",
),
path("submit_request/<slug:slug>/", requests.submit_request, name="submit_request"),
path("request/<int:request_id>/approve/", requests.approve_request, name="approve request"),
path("request/<int:request_id>/decline/", requests.decline_request, name="decline request"),
]

app_name = "quotas"
4 changes: 3 additions & 1 deletion backend/api/receipts/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions backend/api/receipts/new.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -48,6 +49,7 @@ def receipt_create(request: HttpRequest):
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={
Expand Down
8 changes: 6 additions & 2 deletions backend/api/teams/create.py
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -15,6 +15,10 @@ 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()
Expand Down
Loading

0 comments on commit c81329b

Please sign in to comment.