feature/CRUD for Rate Limiting

This commit is contained in:
Marko Milić 2025-10-28 12:24:25 +01:00
parent 2cec411e4a
commit 5d918eac98
6 changed files with 1003 additions and 226 deletions

View File

@ -182,3 +182,169 @@
font-size: 12px;
padding: 6px 12px;
}
/* Rate Limiting CRUD Interface Styles */
#rateLimitForm {
margin-bottom: 30px;
}
#rateLimitForm .panel-heading {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
#rateLimitForm .panel-title {
color: #495057;
font-weight: bold;
font-size: 16px;
}
#addRateLimitBtn {
background-color: #28a745;
border-color: #28a745;
font-weight: bold;
}
#addRateLimitBtn:hover {
background-color: #218838;
border-color: #1e7e34;
}
/* Rate limits table styling */
#rateLimitsList .table {
margin-bottom: 0;
}
#rateLimitsList .table th {
background-color: #f8f9fa;
color: #495057;
font-weight: bold;
font-size: 12px;
text-transform: uppercase;
border-top: none;
}
#rateLimitsList .table td {
font-size: 13px;
vertical-align: middle;
}
/* Action buttons styling */
#rateLimitsList .btn-sm {
font-size: 11px;
padding: 4px 8px;
margin-right: 5px;
}
#rateLimitsList .btn-primary {
background-color: #007bff;
border-color: #007bff;
}
#rateLimitsList .btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
#rateLimitsList .btn-danger {
background-color: #dc3545;
border-color: #dc3545;
}
#rateLimitsList .btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
/* Form styling */
#rateLimitFormElement .form-group label {
font-weight: bold;
color: #495057;
font-size: 13px;
}
#rateLimitFormElement .form-control {
font-size: 13px;
border-radius: 3px;
}
#rateLimitFormElement .btn {
margin-right: 10px;
}
/* Empty state styling */
.alert-info {
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}
.alert-info .glyphicon {
margin-right: 8px;
}
/* Responsive table */
@media (max-width: 1200px) {
#rateLimitsList .table-responsive {
font-size: 12px;
}
#rateLimitsList .table th,
#rateLimitsList .table td {
padding: 8px 4px;
}
#rateLimitsList .btn-sm {
font-size: 10px;
padding: 3px 6px;
margin: 1px;
display: block;
width: 100%;
margin-bottom: 2px;
}
}
@media (max-width: 768px) {
#rateLimitForm .col-xs-6 {
margin-bottom: 10px;
}
#rateLimitsList .table {
font-size: 11px;
}
#addRateLimitBtn {
width: 100%;
margin-bottom: 15px;
}
}
/* Animation for form show/hide */
#rateLimitForm {
transition: all 0.3s ease-in-out;
}
/* Highlight new/updated rows */
.table tr.highlight {
background-color: #d4edda;
transition: background-color 2s ease-out;
}
/* Loading states */
.btn[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
/* Panel improvements */
.panel-default > .panel-heading {
background-image: none;
background-color: #f8f9fa;
}
.panel-default {
border-color: #dee2e6;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
}

View File

@ -2,8 +2,8 @@ $(document).ready(function ($) {
// Handle datetime-local inputs for rate limiting
function initializeDateTimeFields() {
// Set default values for datetime fields if they're empty
var fromDateField = $("#id_from_date");
var toDateField = $("#id_to_date");
var fromDateField = $("#from_date");
var toDateField = $("#to_date");
// If fields are empty, set default values
if (!fromDateField.val()) {
@ -27,33 +27,35 @@ $(document).ready(function ($) {
var toDate = $("[data-to-date]").data("to-date");
if (fromDate && fromDate !== "1099-12-31T23:00:00Z") {
$("#id_from_date").val(convertISOToLocalDateTime(fromDate));
$("#from_date").val(convertISOToLocalDateTime(fromDate));
}
if (toDate && toDate !== "1099-12-31T23:00:00Z") {
$("#id_to_date").val(convertISOToLocalDateTime(toDate));
$("#to_date").val(convertISOToLocalDateTime(toDate));
}
}
// Form validation
function validateRateLimitingForm() {
$("form").on("submit", function (e) {
$("#rateLimitFormElement").on("submit", function (e) {
var hasError = false;
var errorMessage = "";
// Check if any limit values are negative (except -1 which means unlimited)
$('input[type="number"]').each(function () {
var value = parseInt($(this).val());
if (value < -1) {
hasError = true;
errorMessage +=
"Rate limit values must be -1 (unlimited) or positive numbers.\n";
return false;
}
});
$(this)
.find('input[type="number"]')
.each(function () {
var value = parseInt($(this).val());
if (isNaN(value) || value < -1) {
hasError = true;
errorMessage +=
"Rate limit values must be -1 (unlimited) or positive numbers.\n";
return false;
}
});
// Check date range
var fromDate = new Date($("#id_from_date").val());
var toDate = new Date($("#id_to_date").val());
var fromDate = new Date($("#from_date").val());
var toDate = new Date($("#to_date").val());
if (fromDate && toDate && fromDate > toDate) {
hasError = true;
@ -65,6 +67,52 @@ $(document).ready(function ($) {
e.preventDefault();
return false;
}
// Handle form submission via AJAX
e.preventDefault();
submitRateLimitForm();
});
}
// Submit rate limit form via AJAX
function submitRateLimitForm() {
var form = $("#rateLimitFormElement");
var formData = new FormData(form[0]);
var submitBtn = $("#submitBtn");
var originalText = submitBtn.text();
// Disable submit button and show loading
submitBtn.prop("disabled", true).text("Saving...");
$.ajax({
url: window.location.pathname,
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"X-CSRFToken": $("[name=csrfmiddlewaretoken]").val(),
},
success: function (response) {
if (response.success) {
// Hide form and reload page to show updated data
hideRateLimitForm();
window.location.reload();
} else {
alert("Error: " + (response.error || "Unknown error occurred"));
}
},
error: function (xhr, status, error) {
var errorMessage = "Error saving rate limit";
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
alert(errorMessage);
},
complete: function () {
// Re-enable submit button
submitBtn.prop("disabled", false).text(originalText);
},
});
}
@ -98,3 +146,11 @@ $(document).ready(function ($) {
);
});
});
// Global functions are now defined inline in the template
// This file now only contains form validation and initialization
// Refresh rate limits list
function refreshRateLimits() {
window.location.reload();
}

View File

@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load humanize static %}
{% load i18n %}
{% load consumer_extras %}
{% block page_title %}{{ block.super }} / Consumer {{ consumer.app_name }}{% endblock page_title %}
@ -13,96 +14,169 @@
<div class="col-xs-12">
<h2>{% trans "Rate Limiting Configuration" %}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form.consumer_id }}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="row">
<div class="col-xs-12 col-sm-6">
{% if form.from_date.errors %}<div class="alert alert-danger">{{ form.from_date.errors }}</div>{% endif %}
<div class="form-group">
{{ form.from_date.label_tag }}
{{ form.from_date }}
</div>
</div>
<div class="col-xs-12 col-sm-6">
{% if form.to_date.errors %}<div class="alert alert-danger">{{ form.to_date.errors }}</div>{% endif %}
<div class="form-group">
{{ form.to_date.label_tag }}
{{ form.to_date }}
</div>
</div>
<!-- Add New Rate Limit Button -->
<div class="row" style="margin-bottom: 20px;">
<div class="col-xs-12">
<button type="button" class="btn btn-success" id="addRateLimitBtn" onclick="showAddRateLimitForm()">
<span class="glyphicon glyphicon-plus"></span> {% trans "Add New Rate Limit" %}
</button>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-4 col-md-2">
{% if form.per_second_call_limit.errors %}<div class="alert alert-danger">{{ form.per_second_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_second_call_limit.label_tag }}
{{ form.per_second_call_limit }}
</div>
<!-- Add/Edit Rate Limit Form (Initially Hidden) -->
<div id="rateLimitForm" style="display: none; margin-bottom: 30px;">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title" id="formTitle">{% trans "Add New Rate Limit" %}</h4>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
{% if form.per_minute_call_limit.errors %}<div class="alert alert-danger">{{ form.per_minute_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_minute_call_limit.label_tag }}
{{ form.per_minute_call_limit }}
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
{% if form.per_hour_call_limit.errors %}<div class="alert alert-danger">{{ form.per_hour_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_hour_call_limit.label_tag }}
{{ form.per_hour_call_limit }}
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
{% if form.per_day_call_limit.errors %}<div class="alert alert-danger">{{ form.per_day_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_day_call_limit.label_tag }}
{{ form.per_day_call_limit }}
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
{% if form.per_week_call_limit.errors %}<div class="alert alert-danger">{{ form.per_week_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_week_call_limit.label_tag }}
{{ form.per_week_call_limit }}
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
{% if form.per_month_call_limit.errors %}<div class="alert alert-danger">{{ form.per_month_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_month_call_limit.label_tag }}
{{ form.per_month_call_limit }}
</div>
</div>
</div>
<div class="panel-body">
<form id="rateLimitFormElement" method="post">
{% csrf_token %}
<input type="hidden" id="rateLimitId" name="rate_limit_id" value="">
<input type="hidden" name="action" id="formAction" value="create">
<button type="submit" class="btn btn-primary">{% trans "Update Rate Limits" %}</button>
</form>
<div class="row">
<div class="col-xs-12 col-sm-6">
<div class="form-group">
<label for="from_date">{% trans "From Date" %}</label>
<input type="datetime-local" class="form-control" id="from_date" name="from_date" required>
</div>
</div>
<div class="col-xs-12 col-sm-6">
<div class="form-group">
<label for="to_date">{% trans "To Date" %}</label>
<input type="datetime-local" class="form-control" id="to_date" name="to_date" required>
</div>
</div>
</div>
{% if consumer.created_at %}
<div class="row" style="margin-top: 20px;">
<div class="col-xs-6 col-sm-6">
<div class="form-group">
<label>{% trans "Rate Limits Created At" %}</label>
<input type="text" class="form-control" value="{{ consumer.created_at }}" readonly />
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div class="form-group">
<label>{% trans "Rate Limits Updated At" %}</label>
<input type="text" class="form-control" value="{{ consumer.updated_at }}" readonly />
<div class="row">
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_second_call_limit">{% trans "Per Second" %}</label>
<input type="number" class="form-control" id="per_second_call_limit" name="per_second_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_minute_call_limit">{% trans "Per Minute" %}</label>
<input type="number" class="form-control" id="per_minute_call_limit" name="per_minute_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_hour_call_limit">{% trans "Per Hour" %}</label>
<input type="number" class="form-control" id="per_hour_call_limit" name="per_hour_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_day_call_limit">{% trans "Per Day" %}</label>
<input type="number" class="form-control" id="per_day_call_limit" name="per_day_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_week_call_limit">{% trans "Per Week" %}</label>
<input type="number" class="form-control" id="per_week_call_limit" name="per_week_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_month_call_limit">{% trans "Per Month" %}</label>
<input type="number" class="form-control" id="per_month_call_limit" name="per_month_call_limit" value="-1" min="-1">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button type="submit" class="btn btn-primary" id="submitBtn">{% trans "Save Rate Limit" %}</button>
<button type="button" class="btn btn-default" onclick="hideRateLimitForm()">{% trans "Cancel" %}</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Rate Limits List -->
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">{% trans "Existing Rate Limits" %}</h4>
</div>
<div class="panel-body">
<div id="rateLimitsList">
{% if call_limits and call_limits.limits %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>{% trans "From Date" %}</th>
<th>{% trans "To Date" %}</th>
<th>{% trans "Per Second" %}</th>
<th>{% trans "Per Minute" %}</th>
<th>{% trans "Per Hour" %}</th>
<th>{% trans "Per Day" %}</th>
<th>{% trans "Per Week" %}</th>
<th>{% trans "Per Month" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Updated" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for limit in call_limits.limits %}
<tr>
<td>{{ limit.from_date|parse_iso_date:"Y-m-d H:i" }}</td>
<td>{{ limit.to_date|parse_iso_date:"Y-m-d H:i" }}</td>
<td>{{ limit.per_second_call_limit|default:"-1" }}</td>
<td>{{ limit.per_minute_call_limit|default:"-1" }}</td>
<td>{{ limit.per_hour_call_limit|default:"-1" }}</td>
<td>{{ limit.per_day_call_limit|default:"-1" }}</td>
<td>{{ limit.per_week_call_limit|default:"-1" }}</td>
<td>{{ limit.per_month_call_limit|default:"-1" }}</td>
<td>{{ limit.created_at|parse_iso_date:"Y-m-d H:i" }}</td>
<td>{{ limit.updated_at|parse_iso_date:"Y-m-d H:i" }}</td>
<td>
{% if limit.rate_limiting_id %}
<button type="button" class="btn btn-sm btn-primary"
onclick="editRateLimit('{{ limit.rate_limiting_id }}', '{{ limit.from_date|escapejs }}', '{{ limit.to_date|escapejs }}', '{{ limit.per_second_call_limit|default:"-1" }}', '{{ limit.per_minute_call_limit|default:"-1" }}', '{{ limit.per_hour_call_limit|default:"-1" }}', '{{ limit.per_day_call_limit|default:"-1" }}', '{{ limit.per_week_call_limit|default:"-1" }}', '{{ limit.per_month_call_limit|default:"-1" }}')">
<span class="glyphicon glyphicon-edit"></span> {% trans "Edit" %}
</button>
<button type="button" class="btn btn-sm btn-danger"
onclick="deleteRateLimit('{{ limit.rate_limiting_id|escapejs }}')">
<span class="glyphicon glyphicon-trash"></span> {% trans "Delete" %}
</button>
{% else %}
<span class="text-muted">{% trans "No ID" %}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<span class="glyphicon glyphicon-info-sign"></span>
{% trans "No rate limits configured for this consumer. Click 'Add New Rate Limit' to create one." %}
<!-- Debug: Show raw API response for troubleshooting -->
{% if call_limits %}
<br><small><strong>Debug - Raw call_limits data:</strong><br>
<pre style="font-size: 10px; background: #f5f5f5; padding: 10px; margin-top: 10px;">{{ call_limits }}</pre>
</small>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
@ -278,6 +352,118 @@
{% endif %}
});
// Global functions for CRUD operations - attached to window for global access
window.showAddRateLimitForm = function() {
// Reset form for new entry
$("#rateLimitForm").show();
$("#formTitle").text("Add New Rate Limit");
$("#formAction").val("create");
$("#rateLimitId").val("");
// Set default values
$("#from_date").val("2024-01-01T00:00");
$("#to_date").val("2026-01-01T00:00");
$("#per_second_call_limit").val("-1");
$("#per_minute_call_limit").val("-1");
$("#per_hour_call_limit").val("-1");
$("#per_day_call_limit").val("-1");
$("#per_week_call_limit").val("-1");
$("#per_month_call_limit").val("-1");
$("#submitBtn").text("Create Rate Limit");
// Scroll to form
$("html, body").animate({
scrollTop: $("#rateLimitForm").offset().top - 20
}, 500);
};
window.hideRateLimitForm = function() {
$("#rateLimitForm").hide();
};
window.editRateLimit = function(rateLimitingId, fromDate, toDate, perSecond, perMinute, perHour, perDay, perWeek, perMonth) {
// Show form for editing
$("#rateLimitForm").show();
$("#formTitle").text("Edit Rate Limit");
$("#formAction").val("update");
$("#rateLimitId").val(rateLimitingId);
// Convert ISO dates to datetime-local format
function convertISOToLocal(isoString) {
if (!isoString || isoString === "1099-12-31T23:00:00Z") return "2024-01-01T00:00";
try {
var date = new Date(isoString);
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, "0");
var day = String(date.getDate()).padStart(2, "0");
var hours = String(date.getHours()).padStart(2, "0");
var minutes = String(date.getMinutes()).padStart(2, "0");
return year + "-" + month + "-" + day + "T" + hours + ":" + minutes;
} catch (e) {
return "2024-01-01T00:00";
}
}
// Populate form fields
$("#from_date").val(convertISOToLocal(fromDate));
$("#to_date").val(convertISOToLocal(toDate));
$("#per_second_call_limit").val(perSecond);
$("#per_minute_call_limit").val(perMinute);
$("#per_hour_call_limit").val(perHour);
$("#per_day_call_limit").val(perDay);
$("#per_week_call_limit").val(perWeek);
$("#per_month_call_limit").val(perMonth);
$("#submitBtn").text("Update Rate Limit");
// Scroll to form
$("html, body").animate({
scrollTop: $("#rateLimitForm").offset().top - 20
}, 500);
};
window.deleteRateLimit = function(rateLimitingId) {
console.log("Deleting rate limit with ID:", rateLimitingId);
if (!confirm("Are you sure you want to delete this rate limit? This action cannot be undone.")) {
return;
}
// Create form data for delete request
var formData = new FormData();
formData.append("action", "delete");
formData.append("rate_limiting_id", rateLimitingId);
formData.append("csrfmiddlewaretoken", $("[name=csrfmiddlewaretoken]").val());
$.ajax({
url: window.location.pathname,
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
success: function(response) {
console.log("Delete response:", response);
if (response.success) {
// Reload page to show updated data
window.location.reload();
} else {
alert("Error: " + (response.error || "Unknown error occurred"));
}
},
error: function(xhr, status, error) {
console.error("Delete error:", xhr, status, error);
var errorMessage = "Error deleting rate limit";
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
alert(errorMessage);
}
});
};
// Global variables for refresh functionality
let refreshInterval = null;
let refreshCount = 0;

View File

@ -0,0 +1 @@
# Template tags module for consumers app

View File

@ -0,0 +1,96 @@
"""
Custom template filters for consumers app
"""
from django import template
from django.conf import settings
from datetime import datetime
import logging
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def parse_iso_date(date_str, format_str="Y-m-d H:i"):
"""
Parse ISO date string and format it for display
Usage: {{ date_string|parse_iso_date:"Y-m-d H:i" }}
"""
if not date_str or date_str in ["", "null", "None", None]:
return "N/A"
# Convert to string if it's not already
if not isinstance(date_str, str):
date_str = str(date_str)
# List of common date formats to try
formats_to_try = [
"%Y-%m-%dT%H:%M:%SZ", # 2024-01-01T12:00:00Z
"%Y-%m-%dT%H:%M:%S", # 2024-01-01T12:00:00
"%Y-%m-%dT%H:%M:%S.%fZ", # 2024-01-01T12:00:00.000Z
"%Y-%m-%dT%H:%M:%S.%f", # 2024-01-01T12:00:00.000
"%Y-%m-%d %H:%M:%S", # 2024-01-01 12:00:00
settings.API_DATE_FORMAT_WITH_SECONDS, # From settings
]
# Try to parse with different formats
for fmt in formats_to_try:
try:
parsed_date = datetime.strptime(date_str, fmt)
# Convert Django date format to Python strftime format
django_to_python = {
"Y": "%Y",
"m": "%m",
"d": "%d",
"H": "%H",
"i": "%M",
"s": "%S",
}
# Simple format conversion for common cases
python_format = format_str
for django_fmt, python_fmt in django_to_python.items():
python_format = python_format.replace(django_fmt, python_fmt)
return parsed_date.strftime(python_format)
except (ValueError, TypeError):
continue
# Try using fromisoformat for Python 3.7+
try:
# Handle timezone indicator
clean_date_str = date_str.replace("Z", "+00:00")
parsed_date = datetime.fromisoformat(clean_date_str.replace("Z", ""))
# Convert format and return
python_format = format_str
django_to_python = {
"Y": "%Y",
"m": "%m",
"d": "%d",
"H": "%H",
"i": "%M",
"s": "%S",
}
for django_fmt, python_fmt in django_to_python.items():
python_format = python_format.replace(django_fmt, python_fmt)
return parsed_date.strftime(python_format)
except (ValueError, AttributeError):
pass
# Last resort - return the original string or N/A
logger.warning(f"Could not parse date string: {date_str}")
return "Invalid Date"
@register.filter
def smart_default(value, default_value="N/A"):
"""
Smart default filter that handles various empty/null cases
Usage: {{ value|smart_default:"Default Value" }}
"""
if value is None or value == "" or value == "null" or value == "None":
return default_value
return value

View File

@ -12,7 +12,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views.generic import TemplateView, RedirectView, FormView
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponseRedirect
from obp.api import API, APIError
from base.filters import BaseFilter, FilterTime
@ -22,32 +22,36 @@ from .forms import ApiConsumersForm
class FilterAppType(BaseFilter):
"""Filter consumers by application type"""
filter_type = 'app_type'
filter_type = "app_type"
def _apply(self, data, filter_value):
filtered = [x for x in data if x['app_type'] == filter_value]
filtered = [x for x in data if x["app_type"] == filter_value]
return filtered
class FilterEnabled(BaseFilter):
"""Filter consumers by enabled state"""
filter_type = 'enabled'
filter_type = "enabled"
def _apply(self, data, filter_value):
enabled = filter_value in ['true']
filtered = [x for x in data if x['enabled'] == enabled]
enabled = filter_value in ["true"]
filtered = [x for x in data if x["enabled"] == enabled]
return filtered
class IndexView(LoginRequiredMixin, TemplateView):
"""Index view for consumers"""
template_name = "consumers/index.html"
def scrub(self, consumers):
"""Scrubs data in the given consumers to adher to certain formats"""
for consumer in consumers:
consumer['created'] = datetime.strptime(
consumer['created'], settings.API_DATE_FORMAT_WITH_SECONDS )
consumer["created"] = datetime.strptime(
consumer["created"], settings.API_DATE_FORMAT_WITH_SECONDS
)
return consumers
def compile_statistics(self, consumers):
@ -55,46 +59,50 @@ class IndexView(LoginRequiredMixin, TemplateView):
unique_developer_email = {}
unique_name = {}
for consumer in consumers:
unique_developer_email[consumer['developer_email']] = True
unique_name[consumer['app_name']] = True
unique_developer_email[consumer["developer_email"]] = True
unique_name[consumer["app_name"]] = True
unique_developer_email = unique_developer_email.keys()
unique_name = unique_name.keys()
statistics = {
'consumers_num': len(consumers),
'unique_developer_email_num': len(unique_developer_email),
'unique_name_num': len(unique_name),
"consumers_num": len(consumers),
"unique_developer_email_num": len(unique_developer_email),
"unique_name_num": len(unique_name),
}
return statistics
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
consumers = []
sorted_consumers=[]
api = API(self.request.session.get('obp'))
sorted_consumers = []
api = API(self.request.session.get("obp"))
try:
limit = self.request.GET.get('limit', 50)
offset = self.request.GET.get('offset', 0)
urlpath = '/management/consumers?limit={}&offset={}'.format(limit, offset)
limit = self.request.GET.get("limit", 50)
offset = self.request.GET.get("offset", 0)
urlpath = "/management/consumers?limit={}&offset={}".format(limit, offset)
consumers = api.get(urlpath)
if 'code' in consumers and consumers['code'] >= 400:
messages.error(self.request, consumers['message'])
if "code" in consumers and consumers["code"] >= 400:
messages.error(self.request, consumers["message"])
else:
consumers = FilterEnabled(context, self.request.GET)\
.apply(consumers['consumers'])
consumers = FilterAppType(context, self.request.GET)\
.apply(consumers)
consumers = FilterTime(context, self.request.GET, 'created')\
.apply(consumers)
consumers = FilterEnabled(context, self.request.GET).apply(
consumers["consumers"]
)
consumers = FilterAppType(context, self.request.GET).apply(consumers)
consumers = FilterTime(context, self.request.GET, "created").apply(
consumers
)
consumers = self.scrub(consumers)
sorted_consumers = sorted(
consumers, key=lambda consumer: consumer['created'], reverse=True)
consumers, key=lambda consumer: consumer["created"], reverse=True
)
context.update({
'consumers': sorted_consumers,
'limit': limit,
'offset': offset,
'statistics': self.compile_statistics(consumers),
})
context.update(
{
"consumers": sorted_consumers,
"limit": limit,
"offset": offset,
"statistics": self.compile_statistics(consumers),
}
)
except APIError as err:
messages.error(self.request, err)
@ -103,66 +111,256 @@ class IndexView(LoginRequiredMixin, TemplateView):
class DetailView(LoginRequiredMixin, FormView):
"""Detail view for a consumer"""
form_class = ApiConsumersForm
template_name = "consumers/detail.html"
def dispatch(self, request, *args, **kwargs):
self.api = API(request.session.get('obp'))
self.api = API(request.session.get("obp"))
return super(DetailView, self).dispatch(request, *args, **kwargs)
def get_form(self, *args, **kwargs):
form = super(DetailView, self).get_form(*args, **kwargs)
form.fields['consumer_id'].initial = self.kwargs['consumer_id']
form.fields["consumer_id"].initial = self.kwargs["consumer_id"]
# Get call limits data to populate form
api = API(self.request.session.get('obp'))
api = API(self.request.session.get("obp"))
try:
call_limits_urlpath = '/management/consumers/{}/consumer/call-limits'.format(self.kwargs['consumer_id'])
call_limits_urlpath = (
"/management/consumers/{}/consumer/call-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(call_limits_urlpath)
if not ('code' in call_limits and call_limits['code'] >= 400):
if not ("code" in call_limits and call_limits["code"] >= 400):
# Populate form with existing rate limiting data
if 'from_date' in call_limits and call_limits['from_date']:
if "from_date" in call_limits and call_limits["from_date"]:
try:
from_date_str = call_limits['from_date'].replace('Z', '')
from_date_str = call_limits["from_date"].replace("Z", "")
# Parse and ensure no timezone info for form field
dt = datetime.fromisoformat(from_date_str)
if dt.tzinfo:
dt = dt.replace(tzinfo=None)
form.fields['from_date'].initial = dt
form.fields["from_date"].initial = dt
except:
pass
if 'to_date' in call_limits and call_limits['to_date']:
if "to_date" in call_limits and call_limits["to_date"]:
try:
to_date_str = call_limits['to_date'].replace('Z', '')
to_date_str = call_limits["to_date"].replace("Z", "")
# Parse and ensure no timezone info for form field
dt = datetime.fromisoformat(to_date_str)
if dt.tzinfo:
dt = dt.replace(tzinfo=None)
form.fields['to_date'].initial = dt
form.fields["to_date"].initial = dt
except:
pass
form.fields['per_second_call_limit'].initial = call_limits.get('per_second_call_limit', '-1')
form.fields['per_minute_call_limit'].initial = call_limits.get('per_minute_call_limit', '-1')
form.fields['per_hour_call_limit'].initial = call_limits.get('per_hour_call_limit', '-1')
form.fields['per_day_call_limit'].initial = call_limits.get('per_day_call_limit', '-1')
form.fields['per_week_call_limit'].initial = call_limits.get('per_week_call_limit', '-1')
form.fields['per_month_call_limit'].initial = call_limits.get('per_month_call_limit', '-1')
form.fields["per_second_call_limit"].initial = call_limits.get(
"per_second_call_limit", "-1"
)
form.fields["per_minute_call_limit"].initial = call_limits.get(
"per_minute_call_limit", "-1"
)
form.fields["per_hour_call_limit"].initial = call_limits.get(
"per_hour_call_limit", "-1"
)
form.fields["per_day_call_limit"].initial = call_limits.get(
"per_day_call_limit", "-1"
)
form.fields["per_week_call_limit"].initial = call_limits.get(
"per_week_call_limit", "-1"
)
form.fields["per_month_call_limit"].initial = call_limits.get(
"per_month_call_limit", "-1"
)
except:
pass
return form
def form_valid(self, form):
def post(self, request, *args, **kwargs):
"""Handle POST requests for rate limit CRUD operations"""
action = request.POST.get("action")
"""Put limits data to API"""
# Check if this is an AJAX request
if request.headers.get("X-Requested-With") == "XMLHttpRequest" or action in [
"create",
"update",
"delete",
]:
if action == "create":
return self.create_rate_limit(request)
elif action == "update":
return self.update_rate_limit(request)
elif action == "delete":
return self.delete_rate_limit(request)
# Fallback to original form handling for compatibility
form = self.get_form()
if form.is_valid():
return self.form_valid_legacy(request, form)
else:
return self.form_invalid(form)
def create_rate_limit(self, request):
"""Create a new rate limit using v6.0.0 POST API"""
try:
data = ''
api_consumers_form = ApiConsumersForm(self.request.POST)
if api_consumers_form.is_valid():
data = api_consumers_form.cleaned_data
consumer_id = self.kwargs["consumer_id"]
urlpath = '/management/consumers/{}/consumer/call-limits'.format(data['consumer_id'])
# Helper function to format datetime to UTC
def format_datetime_utc(dt_str):
if not dt_str:
return "2024-01-01T00:00:00Z"
try:
dt = datetime.fromisoformat(dt_str)
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
except:
return "2024-01-01T00:00:00Z"
payload = {
"from_date": format_datetime_utc(request.POST.get("from_date")),
"to_date": format_datetime_utc(request.POST.get("to_date")),
"per_second_call_limit": str(
request.POST.get("per_second_call_limit", "-1")
),
"per_minute_call_limit": str(
request.POST.get("per_minute_call_limit", "-1")
),
"per_hour_call_limit": str(
request.POST.get("per_hour_call_limit", "-1")
),
"per_day_call_limit": str(request.POST.get("per_day_call_limit", "-1")),
"per_week_call_limit": str(
request.POST.get("per_week_call_limit", "-1")
),
"per_month_call_limit": str(
request.POST.get("per_month_call_limit", "-1")
),
}
# Use v6.0.0 API for creating rate limits
urlpath = "/management/consumers/{}/consumer/call-limits".format(
consumer_id
)
response = self.api.post(
urlpath, payload, version=settings.API_VERSION["v600"]
)
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
else:
messages.success(request, "Rate limit created successfully.")
except APIError as err:
messages.error(request, str(err))
except Exception as err:
messages.error(request, "Error creating rate limit: {}".format(err))
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def update_rate_limit(self, request):
"""Update existing rate limit using v5.1.0 PUT API"""
try:
consumer_id = self.kwargs["consumer_id"]
# Helper function to format datetime to UTC
def format_datetime_utc(dt_str):
if not dt_str:
return "2024-01-01T00:00:00Z"
try:
dt = datetime.fromisoformat(dt_str)
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
except:
return "2024-01-01T00:00:00Z"
payload = {
"from_date": format_datetime_utc(request.POST.get("from_date")),
"to_date": format_datetime_utc(request.POST.get("to_date")),
"per_second_call_limit": str(
request.POST.get("per_second_call_limit", "-1")
),
"per_minute_call_limit": str(
request.POST.get("per_minute_call_limit", "-1")
),
"per_hour_call_limit": str(
request.POST.get("per_hour_call_limit", "-1")
),
"per_day_call_limit": str(request.POST.get("per_day_call_limit", "-1")),
"per_week_call_limit": str(
request.POST.get("per_week_call_limit", "-1")
),
"per_month_call_limit": str(
request.POST.get("per_month_call_limit", "-1")
),
}
# Use v5.1.0 API for updating rate limits
urlpath = "/management/consumers/{}/consumer/call-limits".format(
consumer_id
)
response = self.api.put(
urlpath, payload, version=settings.API_VERSION["v510"]
)
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
else:
messages.success(request, "Rate limit updated successfully.")
except APIError as err:
messages.error(request, str(err))
except Exception as err:
messages.error(request, "Error updating rate limit: {}".format(err))
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def delete_rate_limit(self, request):
"""Delete a rate limit using v6.0.0 DELETE API"""
try:
consumer_id = self.kwargs["consumer_id"]
rate_limiting_id = request.POST.get("rate_limiting_id")
if not rate_limiting_id:
messages.error(request, "Rate limiting ID is required for deletion.")
return JsonResponse(
{"success": False, "error": "Missing rate limiting ID"}
)
# Use v6.0.0 API for deleting rate limits
urlpath = "/management/consumers/{}/consumer/call-limits/{}".format(
consumer_id, rate_limiting_id
)
response = self.api.delete(urlpath, version=settings.API_VERSION["v600"])
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
else:
messages.success(request, "Rate limit deleted successfully.")
except APIError as err:
messages.error(request, str(err))
except Exception as err:
messages.error(request, "Error deleting rate limit: {}".format(err))
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def form_valid_legacy(self, request, form):
"""Legacy form handling for backwards compatibility"""
try:
data = form.cleaned_data
urlpath = "/management/consumers/{}/consumer/call-limits".format(
data["consumer_id"]
)
# Helper function to format datetime to UTC
def format_datetime_utc(dt):
@ -171,107 +369,172 @@ class DetailView(LoginRequiredMixin, FormView):
# Convert to UTC and format as required by API
if dt.tzinfo:
dt = dt.astimezone(dt_module.timezone.utc).replace(tzinfo=None)
return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
payload = {
'from_date': format_datetime_utc(data['from_date']),
'to_date': format_datetime_utc(data['to_date']),
'per_second_call_limit': str(data['per_second_call_limit']) if data['per_second_call_limit'] is not None else "-1",
'per_minute_call_limit': str(data['per_minute_call_limit']) if data['per_minute_call_limit'] is not None else "-1",
'per_hour_call_limit': str(data['per_hour_call_limit']) if data['per_hour_call_limit'] is not None else "-1",
'per_day_call_limit': str(data['per_day_call_limit']) if data['per_day_call_limit'] is not None else "-1",
'per_week_call_limit': str(data['per_week_call_limit']) if data['per_week_call_limit'] is not None else "-1",
'per_month_call_limit': str(data['per_month_call_limit']) if data['per_month_call_limit'] is not None else "-1"
"from_date": format_datetime_utc(data["from_date"]),
"to_date": format_datetime_utc(data["to_date"]),
"per_second_call_limit": str(data["per_second_call_limit"])
if data["per_second_call_limit"] is not None
else "-1",
"per_minute_call_limit": str(data["per_minute_call_limit"])
if data["per_minute_call_limit"] is not None
else "-1",
"per_hour_call_limit": str(data["per_hour_call_limit"])
if data["per_hour_call_limit"] is not None
else "-1",
"per_day_call_limit": str(data["per_day_call_limit"])
if data["per_day_call_limit"] is not None
else "-1",
"per_week_call_limit": str(data["per_week_call_limit"])
if data["per_week_call_limit"] is not None
else "-1",
"per_month_call_limit": str(data["per_month_call_limit"])
if data["per_month_call_limit"] is not None
else "-1",
}
response = self.api.put(urlpath, payload)
if 'code' in response and response['code'] >= 400:
messages.error(self.request, response['message'])
return super(DetailView, self).form_invalid(api_consumers_form)
response = self.api.put(
urlpath, payload, version=settings.API_VERSION["v510"]
)
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
return super(DetailView, self).form_invalid(form)
except APIError as err:
messages.error(self.request, err)
return super(DetailView, self).form_invalid(api_consumers_form)
messages.error(request, err)
return super(DetailView, self).form_invalid(form)
except Exception as err:
messages.error(self.request, "{}".format(err))
return super(DetailView, self).form_invalid(api_consumers_form)
messages.error(request, "{}".format(err))
return super(DetailView, self).form_invalid(form)
msg = 'Rate limits for consumer {} have been updated successfully.'.format(
data['consumer_id'])
messages.success(self.request, msg)
self.success_url = self.request.path
return super(DetailView, self).form_valid(api_consumers_form)
msg = "Rate limits for consumer {} have been updated successfully.".format(
data["consumer_id"]
)
messages.success(request, msg)
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def get(self, request, *args, **kwargs):
# Check if this is an AJAX request for usage data
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return self.get_usage_data_ajax()
return super(DetailView, self).get(request, *args, **kwargs)
def get_usage_data_ajax(self):
"""Return usage data as JSON for AJAX refresh"""
api = API(self.request.session.get('obp'))
api = API(self.request.session.get("obp"))
try:
call_limits_urlpath = '/management/consumers/{}/consumer/call-limits'.format(self.kwargs['consumer_id'])
call_limits_urlpath = (
"/management/consumers/{}/consumer/call-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(call_limits_urlpath)
if 'code' in call_limits and call_limits['code'] >= 400:
return JsonResponse({'error': call_limits['message']}, status=400)
if "code" in call_limits and call_limits["code"] >= 400:
return JsonResponse({"error": call_limits["message"]}, status=400)
return JsonResponse(call_limits)
except APIError as err:
return JsonResponse({'error': str(err)}, status=500)
return JsonResponse({"error": str(err)}, status=500)
except Exception as err:
return JsonResponse({'error': str(err)}, status=500)
return JsonResponse({"error": str(err)}, status=500)
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
api = API(self.request.session.get('obp'))
api = API(self.request.session.get("obp"))
consumer = {}
call_limits = {}
try:
urlpath = '/management/consumers/{}'.format(self.kwargs['consumer_id'])
urlpath = "/management/consumers/{}".format(self.kwargs["consumer_id"])
consumer = api.get(urlpath)
if 'code' in consumer and consumer['code'] >= 400:
messages.error(self.request, consumer['message'])
if "code" in consumer and consumer["code"] >= 400:
messages.error(self.request, consumer["message"])
consumer = {}
else:
consumer['created'] = datetime.strptime(
consumer['created'], settings.API_DATE_FORMAT_WITH_SECONDS )
consumer["created"] = datetime.strptime(
consumer["created"], settings.API_DATE_FORMAT_WITH_SECONDS
)
# Get call limits using the correct API endpoint
call_limits_urlpath = '/management/consumers/{}/consumer/call-limits'.format(self.kwargs['consumer_id'])
call_limits = api.get(call_limits_urlpath)
call_limits_urlpath = (
"/management/consumers/{}/consumer/call-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(
call_limits_urlpath, version=settings.API_VERSION["v510"]
)
if 'code' in call_limits and call_limits['code'] >= 400:
messages.error(self.request, "{}".format(call_limits['message']))
call_limits = {}
if "code" in call_limits and call_limits["code"] >= 400:
messages.error(self.request, "{}".format(call_limits["message"]))
call_limits = {"limits": []}
else:
# Merge call limits data into consumer object
consumer.update({
'from_date': call_limits.get('from_date', ''),
'to_date': call_limits.get('to_date', ''),
'per_second_call_limit': call_limits.get('per_second_call_limit', '-1'),
'per_minute_call_limit': call_limits.get('per_minute_call_limit', '-1'),
'per_hour_call_limit': call_limits.get('per_hour_call_limit', '-1'),
'per_day_call_limit': call_limits.get('per_day_call_limit', '-1'),
'per_week_call_limit': call_limits.get('per_week_call_limit', '-1'),
'per_month_call_limit': call_limits.get('per_month_call_limit', '-1'),
'current_state': call_limits.get('current_state', {}),
'created_at': call_limits.get('created_at', ''),
'updated_at': call_limits.get('updated_at', ''),
})
# Handle different API response structures
import uuid
# Handle case where API returns data directly instead of in 'limits' array
if (
"limits" not in call_limits
and "per_second_call_limit" in call_limits
):
# API returned single limit object, wrap it in limits array
if "rate_limiting_id" not in call_limits:
call_limits["rate_limiting_id"] = str(uuid.uuid4())
call_limits = {"limits": [call_limits]}
elif "limits" not in call_limits:
# No limits data found
call_limits = {"limits": []}
else:
# Ensure each limit has a rate_limiting_id
for limit in call_limits.get("limits", []):
if (
"rate_limiting_id" not in limit
or not limit["rate_limiting_id"]
):
limit["rate_limiting_id"] = str(uuid.uuid4())
# For backwards compatibility, merge first limit into consumer if limits exist
if call_limits.get("limits") and len(call_limits["limits"]) > 0:
first_limit = call_limits["limits"][0]
consumer.update(
{
"from_date": first_limit.get("from_date", ""),
"to_date": first_limit.get("to_date", ""),
"per_second_call_limit": first_limit.get(
"per_second_call_limit", "-1"
),
"per_minute_call_limit": first_limit.get(
"per_minute_call_limit", "-1"
),
"per_hour_call_limit": first_limit.get(
"per_hour_call_limit", "-1"
),
"per_day_call_limit": first_limit.get(
"per_day_call_limit", "-1"
),
"per_week_call_limit": first_limit.get(
"per_week_call_limit", "-1"
),
"per_month_call_limit": first_limit.get(
"per_month_call_limit", "-1"
),
"current_state": call_limits.get("current_state", {}),
"created_at": first_limit.get("created_at", ""),
"updated_at": first_limit.get("updated_at", ""),
}
)
except APIError as err:
messages.error(self.request, err)
except Exception as err:
messages.error(self.request, "{}".format(err))
finally:
context.update({
'consumer': consumer,
'call_limits': call_limits
})
context.update({"consumer": consumer, "call_limits": call_limits})
return context
@ -279,52 +542,61 @@ class UsageDataAjaxView(LoginRequiredMixin, TemplateView):
"""AJAX view to return usage data for real-time updates"""
def get(self, request, *args, **kwargs):
api = API(self.request.session.get('obp'))
api = API(self.request.session.get("obp"))
try:
call_limits_urlpath = '/management/consumers/{}/consumer/call-limits'.format(self.kwargs['consumer_id'])
call_limits = api.get(call_limits_urlpath)
call_limits_urlpath = (
"/management/consumers/{}/consumer/call-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(
call_limits_urlpath, version=settings.API_VERSION["v510"]
)
if 'code' in call_limits and call_limits['code'] >= 400:
return JsonResponse({'error': call_limits['message']}, status=400)
if "code" in call_limits and call_limits["code"] >= 400:
return JsonResponse({"error": call_limits["message"]}, status=400)
return JsonResponse(call_limits)
except APIError as err:
return JsonResponse({'error': str(err)}, status=500)
return JsonResponse({"error": str(err)}, status=500)
except Exception as err:
return JsonResponse({'error': str(err)}, status=500)
return JsonResponse({"error": str(err)}, status=500)
class EnableDisableView(LoginRequiredMixin, RedirectView):
"""View to enable or disable a consumer"""
enabled = False
success = None
def get_redirect_url(self, *args, **kwargs):
api = API(self.request.session.get('obp'))
api = API(self.request.session.get("obp"))
try:
urlpath = '/management/consumers/{}'.format(kwargs['consumer_id'])
payload = {'enabled': self.enabled}
urlpath = "/management/consumers/{}".format(kwargs["consumer_id"])
payload = {"enabled": self.enabled}
response = api.put(urlpath, payload)
if 'code' in response and response['code'] >= 400:
messages.error(self.request, response['message'])
if "code" in response and response["code"] >= 400:
messages.error(self.request, response["message"])
else:
messages.success(self.request, self.success)
except APIError as err:
messages.error(self.request, err)
urlpath = self.request.POST.get('next', reverse('consumers-index'))
urlpath = self.request.POST.get("next", reverse("consumers-index"))
query = self.request.GET.urlencode()
redirect_url = '{}?{}'.format(urlpath, query)
redirect_url = "{}?{}".format(urlpath, query)
return redirect_url
class EnableView(EnableDisableView):
"""View to enable a consumer"""
enabled = True
success = "Consumer has been enabled."
class DisableView(EnableDisableView):
"""View to disable a consumer"""
enabled = False
success = "Consumer has been disabled."