Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: quotas #263

Merged
merged 16 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading