feature/Add edit/show rate limiting feature

This commit is contained in:
Marko Milić 2025-09-05 07:46:57 +02:00
parent 09d96788f5
commit 4be22830bd
8 changed files with 777 additions and 95 deletions

View File

@ -13,7 +13,7 @@ USER_CURRENT = "/users/current"
def api_version_processor(request):
"""Returns the configured API_VERSION"""
return {'API_VERSION': settings.API_VERSION['v500']}
return {'API_VERSION': settings.API_VERSION['v510']}
def portal_page(request):
@ -82,7 +82,7 @@ def api_user_id(request):
"""Returns the API user id of the logged-in user"""
user_id = 'not authenticated'
get_current_user_api_url = USER_CURRENT
#Here we can not get the user from obp-api side, so we use the django auth user id here.
#Here we can not get the user from obp-api side, so we use the django auth user id here.
cache_key_django_user_id = request.session._session.get('_auth_user_id')
cache_key = '{},{},{}'.format('api_user_id',get_current_user_api_url, cache_key_django_user_id)
apicaches=None
@ -112,4 +112,3 @@ def api_tester_url(request):
"""Returns the URL to the API Tester for the API instance"""
url = getattr(settings, 'API_TESTER_URL', None)
return {'API_TESTER_URL': url}

View File

@ -12,8 +12,45 @@ class ApiConsumersForm(forms.Form):
required=True,
)
from_date = forms.DateTimeField(
label='From Date',
widget=forms.DateTimeInput(
attrs={
'class': 'form-control',
'type': 'datetime-local',
'value': '2024-01-01T00:00',
}
),
required=False,
initial='2024-01-01T00:00:00',
)
to_date = forms.DateTimeField(
label='To Date',
widget=forms.DateTimeInput(
attrs={
'class': 'form-control',
'type': 'datetime-local',
'value': '2026-01-01T00:00',
}
),
required=False,
initial='2026-01-01T00:00:00',
)
per_second_call_limit = forms.IntegerField(
label='Per Second Call Limit',
widget=forms.NumberInput(
attrs={
'class': 'form-control',
}
),
initial=-1,
required=False,
)
per_minute_call_limit = forms.IntegerField(
label='per_minute_call_limit',
label='Per Minute Call Limit',
widget=forms.NumberInput(
attrs={
'class': 'form-control',
@ -24,7 +61,7 @@ class ApiConsumersForm(forms.Form):
)
per_hour_call_limit = forms.IntegerField(
label='per_hour_call_limit',
label='Per Hour Call Limit',
widget=forms.NumberInput(
attrs={
'class': 'form-control',
@ -33,8 +70,9 @@ class ApiConsumersForm(forms.Form):
initial=-1,
required=False,
)
per_day_call_limit = forms.IntegerField(
label='per_day_call_limit',
label='Per Day Call Limit',
widget=forms.NumberInput(
attrs={
'class': 'form-control',
@ -43,8 +81,9 @@ class ApiConsumersForm(forms.Form):
initial=-1,
required=False,
)
per_week_call_limit = forms.IntegerField(
label='per_week_call_limit',
label='Per Week Call Limit',
widget=forms.NumberInput(
attrs={
'class': 'form-control',
@ -55,7 +94,7 @@ class ApiConsumersForm(forms.Form):
)
per_month_call_limit = forms.IntegerField(
label='per_month_call_limit',
label='Per Month Call Limit',
widget=forms.NumberInput(
attrs={
'class': 'form-control',

View File

@ -1,20 +1,184 @@
.consumers #consumer-list {
margin-top: 20px;
.consumers #consumer-list {
margin-top: 20px;
}
#consumers .btn-group-vertical.filter-enabled,
#consumers .btn-group-vertical.filter-apptype {
margin-top: 10px;
margin-top: 10px;
}
#consumers-detail div {
margin: 5px 0;
margin: 5px 0;
}
#consumers .filter a {
font-size: 12px;
font-size: 12px;
}
#consumers .actions .btn {
margin-bottom: 2px;
margin-bottom: 2px;
}
/* Rate Limiting Styles */
#consumers-detail h2 {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
}
#consumers-detail .panel-info {
border-color: #bee5eb;
background-color: #d1ecf1;
}
#consumers-detail .panel-info .panel-body {
background-color: #f8f9fa;
border-radius: 5px;
padding: 15px;
}
#consumers-detail .text-info {
color: #0c5460 !important;
font-size: 16px;
font-weight: bold;
}
#consumers-detail .text-muted {
color: #6c757d !important;
font-size: 12px;
}
#consumers-detail .form-group label {
font-weight: bold;
color: #495057;
}
#consumers-detail .btn-primary {
background-color: #007bff;
border-color: #007bff;
padding: 10px 20px;
font-weight: bold;
}
#consumers-detail .btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
/* Usage statistics 6-column layout */
#consumers-detail .panel-info .col-sm-2 {
min-height: 80px;
padding: 10px 5px;
transition: all 0.3s ease;
}
/* Readonly fields styling */
#consumers-detail input[readonly] {
background-color: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
border: 1px solid #dee2e6;
}
/* Refresh button styling */
#refreshUsageBtn {
transition: all 0.3s ease;
margin-left: 10px;
}
#refreshUsageBtn:hover {
background-color: #138496;
border-color: #117a8b;
}
#refreshUsageBtn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Progress bar styling */
#refreshProgress {
height: 10px;
background-color: #f5f5f5;
border-radius: 5px;
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
#refreshProgress .progress-bar {
transition: width 0.3s ease;
background-color: #17a2b8;
}
/* Usage update animation */
.usage-calls {
transition: background-color 0.5s ease;
}
.usage-calls.updating {
background-color: #d4edda !important;
padding: 2px 6px;
border-radius: 3px;
}
/* Spinning animation for refresh icon */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.glyphicon-spin {
animation: spin 1s infinite linear;
}
/* Panel pulse effect during refresh */
.panel-refreshing {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(23, 162, 184, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(23, 162, 184, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(23, 162, 184, 0);
}
}
/* Updated data highlight */
.data-updated {
background-color: #d4edda;
border-left: 3px solid #28a745;
padding-left: 10px;
transition: all 0.5s ease;
}
/* Responsive adjustments for usage stats */
@media (max-width: 768px) {
#consumers-detail .panel-info .col-xs-6 {
margin-bottom: 15px;
}
#consumers-detail .panel-info .col-sm-2 {
min-height: auto;
}
#refreshUsageBtn {
font-size: 12px;
padding: 4px 8px;
}
}
/* Timestamp fields in configuration section */
#consumers-detail .form-group input[readonly] {
font-size: 12px;
padding: 6px 12px;
}

View File

@ -1,2 +1,100 @@
$(document).ready(function($) {
$(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");
// If fields are empty, set default values
if (!fromDateField.val()) {
fromDateField.val("2024-01-01T00:00");
}
if (!toDateField.val()) {
toDateField.val("2026-01-01T00:00");
}
}
// Convert ISO datetime strings to datetime-local format for form inputs
function convertISOToLocalDateTime(isoString) {
if (!isoString) return "";
// Remove the 'Z' and convert to local datetime format
return isoString.replace("Z", "").substring(0, 16);
}
// Initialize datetime fields with existing values if they exist
function setExistingDateTimeValues() {
var fromDate = $("[data-from-date]").data("from-date");
var toDate = $("[data-to-date]").data("to-date");
if (fromDate && fromDate !== "1099-12-31T23:00:00Z") {
$("#id_from_date").val(convertISOToLocalDateTime(fromDate));
}
if (toDate && toDate !== "1099-12-31T23:00:00Z") {
$("#id_to_date").val(convertISOToLocalDateTime(toDate));
}
}
// Form validation
function validateRateLimitingForm() {
$("form").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;
}
});
// Check date range
var fromDate = new Date($("#id_from_date").val());
var toDate = new Date($("#id_to_date").val());
if (fromDate && toDate && fromDate > toDate) {
hasError = true;
errorMessage += "From Date must be before To Date.\n";
}
if (hasError) {
alert(errorMessage);
e.preventDefault();
return false;
}
});
}
// Add visual feedback for current usage status
function enhanceUsageDisplay() {
$(".text-info").each(function () {
var callsMade = parseInt($(this).text().match(/\d+/));
var parentDiv = $(this).closest(".col-xs-6, .col-sm-3");
var limitText = parentDiv.find("strong").text().toLowerCase();
// You could add logic here to highlight usage that's approaching limits
// For now, we'll just ensure consistent styling
$(this).addClass("usage-indicator");
});
}
// Initialize all functionality
initializeDateTimeFields();
setExistingDateTimeValues();
validateRateLimitingForm();
enhanceUsageDisplay();
// Add tooltips for better UX
$('[data-toggle="tooltip"]').tooltip();
// Add help text for rate limiting fields
$('input[name*="call_limit"]').each(function () {
$(this).attr(
"title",
"Use -1 for unlimited, or enter a positive number for the limit",
);
});
});

View File

@ -12,7 +12,7 @@
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Params" %}</h2>
<h2>{% trans "Rate Limiting Configuration" %}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form.consumer_id }}
@ -23,35 +23,59 @@
{% endif %}
<div class="row">
<div class="col-xs-2 col-sm-2">
<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>
</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>
</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-2 col-sm-2">
<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-2 col-sm-2">
<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-2 col-sm-2">
<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-2 col-sm-2">
<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 }}
@ -60,11 +84,84 @@
</div>
</div>
<button type="submit" class="btn btn-primary">{% trans "Update Consumer" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Update Rate Limits" %}</button>
</form>
{% 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>
</div>
</div>
{% endif %}
</div>
</div>
{% if consumer.current_state %}
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Current Usage" %}
<button type="button" id="refreshUsageBtn" class="btn btn-sm btn-info pull-right" onclick="refreshUsageStats()">
<span class="glyphicon glyphicon-refresh"></span> {% trans "Refresh (10s)" %}
</button>
</h2>
<div class="panel panel-info" id="usageStatsPanel">
<div class="panel-body" id="usageStatsContent">
{% if consumer.current_state.per_hour %}
<div class="row" id="usageStatsRow">
{% if consumer.current_state.per_second %}
<div class="col-xs-6 col-sm-2" data-period="per_second">
<strong>{% trans "Per Second" %}</strong><br>
<span class="text-info usage-calls">{{ consumer.current_state.per_second.calls_made }} calls made</span><br>
<small class="text-muted usage-reset">Resets in {{ consumer.current_state.per_second.reset_in_seconds }} seconds</small>
</div>
{% endif %}
{% if consumer.current_state.per_minute %}
<div class="col-xs-6 col-sm-2" data-period="per_minute">
<strong>{% trans "Per Minute" %}</strong><br>
<span class="text-info usage-calls">{{ consumer.current_state.per_minute.calls_made }} calls made</span><br>
<small class="text-muted usage-reset">Resets in {{ consumer.current_state.per_minute.reset_in_seconds }} seconds</small>
</div>
{% endif %}
<div class="col-xs-6 col-sm-2" data-period="per_hour">
<strong>{% trans "Per Hour" %}</strong><br>
<span class="text-info usage-calls">{{ consumer.current_state.per_hour.calls_made }} calls made</span><br>
<small class="text-muted usage-reset">Resets in {{ consumer.current_state.per_hour.reset_in_seconds }} seconds</small>
</div>
<div class="col-xs-6 col-sm-2" data-period="per_day">
<strong>{% trans "Per Day" %}</strong><br>
<span class="text-info usage-calls">{{ consumer.current_state.per_day.calls_made }} calls made</span><br>
<small class="text-muted usage-reset">Resets in {{ consumer.current_state.per_day.reset_in_seconds }} seconds</small>
</div>
<div class="col-xs-6 col-sm-2" data-period="per_week">
<strong>{% trans "Per Week" %}</strong><br>
<span class="text-info usage-calls">{{ consumer.current_state.per_week.calls_made }} calls made</span><br>
<small class="text-muted usage-reset">Resets in {{ consumer.current_state.per_week.reset_in_seconds }} seconds</small>
</div>
<div class="col-xs-6 col-sm-2" data-period="per_month">
<strong>{% trans "Per Month" %}</strong><br>
<span class="text-info usage-calls">{{ consumer.current_state.per_month.calls_made }} calls made</span><br>
<small class="text-muted usage-reset">Resets in {{ consumer.current_state.per_month.reset_in_seconds }} seconds</small>
</div>
</div>
{% endif %}
<div id="refreshProgress" class="progress" style="display: none; margin-top: 15px;">
<div class="progress-bar progress-bar-info progress-bar-striped active" id="progressBar" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-xs-12">
<div id="consumers-detail-consumer_id">
@ -122,51 +219,16 @@
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">
<div id="consumers-detail-redirect_url">
<strong>{% trans "Redirect URL" %}</strong><br />
<span>{{ consumer.redirect_url }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_minute_call_limit ">
<strong>{% trans "Per minute call limit" %}</strong><br />
<span>{{ consumer.per_minute_call_limit }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_hour_call_limit ">
<strong>{% trans "Per hour call limit" %} </strong><br />
<span>{{ consumer.per_hour_call_limit }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_day_call_limit">
<strong>{% trans "Per day call limit" %}</strong><br />
<span>{{ consumer.per_day_call_limit }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div id="consumers-detail-redirect_url">
<strong>{% trans "Redirect URL" %}</strong><br />
<span>{{ consumer.redirect_url }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_week_call_limit">
<strong>{% trans "Per week call limit" %}</strong><br />
<span>{{ consumer.per_week_call_limit }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_month_call_limit">
<strong>{% trans "Per month call limit" %}</strong><br />
<span>{{ consumer.per_month_call_limit }}</span>
</div>
</div>
</div>
<div class="row">
@ -204,11 +266,208 @@
{% endblock %}
{% block extrajs %}
{% comment %}
<script type="text/javascript" src="{% static 'consumers/js/consumers.js' %}"></script>
<script type="text/javascript">
// Add data attributes for existing datetime values
$(document).ready(function() {
{% if consumer.from_date %}
$('body').attr('data-from-date', '{{ consumer.from_date }}');
{% endif %}
{% if consumer.to_date %}
$('body').attr('data-to-date', '{{ consumer.to_date }}');
{% endif %}
});
// Global variables for refresh functionality
let refreshInterval = null;
let refreshCount = 0;
const MAX_REFRESH_COUNT = 10;
// Function to refresh usage statistics
function refreshUsageStats() {
const button = document.getElementById('refreshUsageBtn');
const progressDiv = document.getElementById('refreshProgress');
const progressBar = document.getElementById('progressBar');
// Disable button and show progress
button.disabled = true;
button.innerHTML = '<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> Refreshing...';
progressDiv.style.display = 'block';
// Reset counters
refreshCount = 0;
// Start refresh cycle
refreshInterval = setInterval(fetchUsageData, 1000);
fetchUsageData(); // Initial fetch
}
// Function to fetch usage data via AJAX
function fetchUsageData() {
const consumerId = '{{ consumer.consumer_id }}';
const panel = document.getElementById('usageStatsPanel');
// Add refreshing effect to panel
panel.classList.add('panel-refreshing');
$.ajax({
url: '{% url "consumers-usage-data" consumer.consumer_id %}',
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
timeout: 5000, // 5 second timeout
success: function(data) {
updateUsageDisplay(data);
refreshCount++;
// Update progress bar
const progress = (refreshCount / MAX_REFRESH_COUNT) * 100;
const progressBar = document.getElementById('progressBar');
progressBar.style.width = progress + '%';
progressBar.textContent = refreshCount + '/' + MAX_REFRESH_COUNT;
// Stop after 10 seconds
if (refreshCount >= MAX_REFRESH_COUNT) {
clearInterval(refreshInterval);
resetRefreshButton();
panel.classList.remove('panel-refreshing');
}
},
error: function(xhr, status, error) {
console.error('Error fetching usage data:', error);
showRefreshError(error);
refreshCount++;
const progress = (refreshCount / MAX_REFRESH_COUNT) * 100;
document.getElementById('progressBar').style.width = progress + '%';
if (refreshCount >= MAX_REFRESH_COUNT) {
clearInterval(refreshInterval);
resetRefreshButton();
panel.classList.remove('panel-refreshing');
}
}
});
}
// Function to show refresh error
function showRefreshError(error) {
const errorDiv = document.getElementById('refreshError');
if (!errorDiv) {
const newErrorDiv = document.createElement('div');
newErrorDiv.id = 'refreshError';
newErrorDiv.className = 'alert alert-warning alert-dismissible';
newErrorDiv.style.marginTop = '10px';
newErrorDiv.innerHTML = '<button type="button" class="close" data-dismiss="alert">&times;</button><strong>Warning:</strong> Failed to fetch latest data. Continuing...';
document.getElementById('usageStatsContent').appendChild(newErrorDiv);
// Auto-hide after 3 seconds
setTimeout(function() {
if (newErrorDiv.parentNode) {
newErrorDiv.parentNode.removeChild(newErrorDiv);
}
}, 3000);
}
}
// Function to update usage display with new data
function updateUsageDisplay(data) {
if (data && data.current_state) {
const currentState = data.current_state;
const periods = ['per_second', 'per_minute', 'per_hour', 'per_day', 'per_week', 'per_month'];
periods.forEach(function(period) {
const periodData = currentState[period];
if (periodData) {
const periodDiv = document.querySelector('[data-period="' + period + '"]');
if (periodDiv) {
const callsSpan = periodDiv.querySelector('.usage-calls');
const resetSpan = periodDiv.querySelector('.usage-reset');
if (callsSpan) {
const oldCalls = callsSpan.textContent.match(/\d+/);
const newCalls = periodData.calls_made;
// Check if calls increased
const callsIncreased = oldCalls && parseInt(oldCalls[0]) < newCalls;
callsSpan.textContent = newCalls + ' calls made';
// Add visual feedback
callsSpan.classList.add('updating');
if (callsIncreased) {
periodDiv.classList.add('data-updated');
// Flash effect for increased calls
callsSpan.style.backgroundColor = '#28a745';
callsSpan.style.color = 'white';
setTimeout(function() {
callsSpan.style.backgroundColor = '#d4edda';
callsSpan.style.color = '';
}, 200);
}
setTimeout(function() {
callsSpan.classList.remove('updating');
periodDiv.classList.remove('data-updated');
callsSpan.style.backgroundColor = '';
}, 1000);
}
if (resetSpan) {
resetSpan.textContent = 'Resets in ' + periodData.reset_in_seconds + ' seconds';
// Add subtle animation to reset timer
resetSpan.style.opacity = '0.7';
setTimeout(function() {
resetSpan.style.opacity = '1';
}, 300);
}
}
}
});
// Show last updated time
updateLastRefreshTime();
}
}
// Function to update last refresh time
function updateLastRefreshTime() {
let timeDiv = document.getElementById('lastRefreshTime');
if (!timeDiv) {
timeDiv = document.createElement('small');
timeDiv.id = 'lastRefreshTime';
timeDiv.className = 'text-muted';
timeDiv.style.display = 'block';
timeDiv.style.textAlign = 'center';
timeDiv.style.marginTop = '10px';
document.getElementById('usageStatsContent').appendChild(timeDiv);
}
const now = new Date();
timeDiv.textContent = 'Last updated: ' + now.toLocaleTimeString();
}
// Function to reset refresh button
function resetRefreshButton() {
const button = document.getElementById('refreshUsageBtn');
const progressDiv = document.getElementById('refreshProgress');
const panel = document.getElementById('usageStatsPanel');
button.disabled = false;
button.innerHTML = '<span class="glyphicon glyphicon-refresh"></span> {% trans "Refresh (10s)" %}';
progressDiv.style.display = 'none';
document.getElementById('progressBar').style.width = '0%';
document.getElementById('progressBar').textContent = '';
// Remove any error messages
const errorDiv = document.getElementById('refreshError');
if (errorDiv && errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
}
</script>
{% endcomment %}
{% endblock extrajs %}

View File

@ -5,7 +5,7 @@ URLs for consumers app
from django.urls import re_path
from .views import IndexView, DetailView, EnableView, DisableView
from .views import IndexView, DetailView, EnableView, DisableView, UsageDataAjaxView
urlpatterns = [
re_path(r'^$',
@ -20,4 +20,7 @@ urlpatterns = [
re_path(r'^(?P<consumer_id>[0-9a-z\-]+)/disable$',
DisableView.as_view(),
name='consumers-disable'),
re_path(r'^(?P<consumer_id>[0-9a-z\-]+)/usage-data$',
UsageDataAjaxView.as_view(),
name='consumers-usage-data'),
]

View File

@ -4,12 +4,15 @@ Views of consumers app
"""
from datetime import datetime
import datetime as dt_module
import json
from django.conf import settings
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 obp.api import API, APIError
from base.filters import BaseFilter, FilterTime
@ -110,6 +113,44 @@ class DetailView(LoginRequiredMixin, FormView):
def get_form(self, *args, **kwargs):
form = super(DetailView, self).get_form(*args, **kwargs)
form.fields['consumer_id'].initial = self.kwargs['consumer_id']
# Get call limits data to populate form
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)
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']:
try:
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
except:
pass
if 'to_date' in call_limits and call_limits['to_date']:
try:
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
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')
except:
pass
return form
def form_valid(self, form):
@ -121,15 +162,33 @@ class DetailView(LoginRequiredMixin, FormView):
if api_consumers_form.is_valid():
data = api_consumers_form.cleaned_data
urlpath = '/management/consumers/{}/consumer/calls_limit'.format(data['consumer_id'])
urlpath = '/management/consumers/{}/consumer/call-limits'.format(data['consumer_id'])
# Helper function to format datetime to UTC
def format_datetime_utc(dt):
if not dt:
return "2024-01-01T00:00:00Z"
# 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')
payload = {
'per_minute_call_limit': data['per_minute_call_limit'],
'per_hour_call_limit': data['per_hour_call_limit'],
'per_day_call_limit': data['per_day_call_limit'],
'per_week_call_limit': data['per_week_call_limit'],
'per_month_call_limit': data['per_month_call_limit']
'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)
except APIError as err:
messages.error(self.request, err)
return super(DetailView, self).form_invalid(api_consumers_form)
@ -137,31 +196,72 @@ class DetailView(LoginRequiredMixin, FormView):
messages.error(self.request, "{}".format(err))
return super(DetailView, self).form_invalid(api_consumers_form)
msg = 'calls limit of consumer {} has been updated successfully.'.format(
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)
def get(self, request, *args, **kwargs):
# Check if this is an AJAX request for usage data
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'))
try:
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)
return JsonResponse(call_limits)
except APIError as err:
return JsonResponse({'error': str(err)}, status=500)
except Exception as err:
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'))
consumer = {}
call_limits = {}
try:
urlpath = '/management/consumers/{}'.format(self.kwargs['consumer_id'])
consumer = api.get(urlpath)
consumer['created'] = datetime.strptime(
consumer['created'], settings.API_DATE_FORMAT_WITH_SECONDS )
call_limits_urlpath = '/management/consumers/{}/consumer/call-limits'.format(self.kwargs['consumer_id'])
consumer_call_limtis = api.get(call_limits_urlpath)
if 'code' in consumer_call_limtis and consumer_call_limtis['code'] >= 400:
messages.error(self.request, "{}".format(consumer_call_limtis['message']))
if 'code' in consumer and consumer['code'] >= 400:
messages.error(self.request, consumer['message'])
consumer = {}
else:
consumer['per_minute_call_limit'] = consumer_call_limtis['per_minute_call_limit']
consumer['per_hour_call_limit'] = consumer_call_limtis['per_hour_call_limit']
consumer['per_day_call_limit'] = consumer_call_limtis['per_day_call_limit']
consumer['per_week_call_limit'] = consumer_call_limtis['per_week_call_limit']
consumer['per_month_call_limit'] = consumer_call_limtis['per_month_call_limit']
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)
if 'code' in call_limits and call_limits['code'] >= 400:
messages.error(self.request, "{}".format(call_limits['message']))
call_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', ''),
})
except APIError as err:
messages.error(self.request, err)
@ -169,11 +269,31 @@ class DetailView(LoginRequiredMixin, FormView):
messages.error(self.request, "{}".format(err))
finally:
context.update({
'consumer': consumer
'consumer': consumer,
'call_limits': call_limits
})
return context
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'))
try:
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)
return JsonResponse(call_limits)
except APIError as err:
return JsonResponse({'error': str(err)}, status=500)
except Exception as err:
return JsonResponse({'error': str(err)}, status=500)
class EnableDisableView(LoginRequiredMixin, RedirectView):
"""View to enable or disable a consumer"""
enabled = False

View File

@ -43,7 +43,7 @@ class API(object):
self.start_session(session_data)
self.session_data = session_data
def call(self, method='GET', url='', payload=None, version=settings.API_VERSION['v500']):
def call(self, method='GET', url='', payload=None, version=settings.API_VERSION['v510']):
"""Workhorse which actually calls the API"""
log(logging.INFO, '{} {}'.format(method, url))
if payload:
@ -64,7 +64,7 @@ class API(object):
response.execution_time = elapsed
return response
def get(self, urlpath='', version=settings.API_VERSION['v500']):
def get(self, urlpath='', version=settings.API_VERSION['v510']):
"""
Gets data from the API
@ -77,7 +77,7 @@ class API(object):
else:
return response
def delete(self, urlpath, version=settings.API_VERSION['v500']):
def delete(self, urlpath, version=settings.API_VERSION['v510']):
"""
Deletes data from the API
@ -87,7 +87,7 @@ class API(object):
response = self.call('DELETE', url)
return self.handle_response(response)
def post(self, urlpath, payload, version=settings.API_VERSION['v500']):
def post(self, urlpath, payload, version=settings.API_VERSION['v510']):
"""
Posts data to given urlpath with given payload
@ -97,7 +97,7 @@ class API(object):
response = self.call('POST', url, payload)
return self.handle_response(response)
def put(self, urlpath, payload, version=settings.API_VERSION['v500']):
def put(self, urlpath, payload, version=settings.API_VERSION['v510']):
"""
Puts data on given urlpath with given payload
@ -175,4 +175,4 @@ class API(object):
result = self.get('/users')
for user in result['users']:
choices.append((user['user_id'], user['username']))
return choices
return choices