From 9f8c839a5979b849d660abed1adb1544ab5468d4 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 3 Jul 2017 14:19:03 +0200 Subject: [PATCH] Added connector metrics #26 And reworked API metrics to use a Django form --- apimanager/apimanager/settings.py | 1 + apimanager/base/static/css/base.css | 4 + apimanager/base/templates/base.html | 11 +- apimanager/metrics/forms.py | 204 ++++++++++++++++++ .../metrics/static/metrics/css/metrics.css | 6 +- apimanager/metrics/templates/metrics/api.html | 183 ++++++++++++++++ .../metrics/api_summary_partial_function.html | 21 ++ .../metrics/templates/metrics/connector.html | 113 ++++++++++ .../metrics/templates/metrics/index.html | 196 ----------------- .../metrics/summary_partial_function.html | 13 -- apimanager/metrics/urls.py | 22 +- apimanager/metrics/views.py | 107 ++++++--- 12 files changed, 630 insertions(+), 251 deletions(-) create mode 100644 apimanager/metrics/forms.py create mode 100644 apimanager/metrics/templates/metrics/api.html create mode 100644 apimanager/metrics/templates/metrics/api_summary_partial_function.html create mode 100644 apimanager/metrics/templates/metrics/connector.html delete mode 100644 apimanager/metrics/templates/metrics/index.html delete mode 100644 apimanager/metrics/templates/metrics/summary_partial_function.html diff --git a/apimanager/apimanager/settings.py b/apimanager/apimanager/settings.py index bc25ea8d..05fb9f90 100644 --- a/apimanager/apimanager/settings.py +++ b/apimanager/apimanager/settings.py @@ -184,6 +184,7 @@ API_DATETIMEFORMAT = '%Y-%m-%dT%H:%M:%SZ' +API_DATEFORMAT = '%Y-%m-%d' OAUTH_API = 'http://127.0.0.1:8080' diff --git a/apimanager/base/static/css/base.css b/apimanager/base/static/css/base.css index ca84054f..88c55fbb 100644 --- a/apimanager/base/static/css/base.css +++ b/apimanager/base/static/css/base.css @@ -72,6 +72,10 @@ footer a:hover, .footer a:focus { margin-left: 15px; } +.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:active { + background-color: #53c4ef; +} + .btn-primary { background-color: #53c4ef; border-color: #33a4cf; diff --git a/apimanager/base/templates/base.html b/apimanager/base/templates/base.html index 9f87ef71..f5f6d9d5 100644 --- a/apimanager/base/templates/base.html +++ b/apimanager/base/templates/base.html @@ -31,8 +31,15 @@ Consumers {% url "users-index" as users_index_url %} Users - {% url "metrics-index" as metrics_index_url %} - Metrics + {% url "api-metrics" as api_metrics_url %} + {% url "connector-metrics" as connector_metrics_url %} + {% url "customers-create" as customers_create_url %} Customers {% url "config-index" as config_index_url %} diff --git a/apimanager/metrics/forms.py b/apimanager/metrics/forms.py new file mode 100644 index 00000000..6e91f3e2 --- /dev/null +++ b/apimanager/metrics/forms.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" +Forms of metrics app +""" + +from django import forms +from django.conf import settings + + +class MetricsForm(forms.Form): + start_date = forms.DateTimeField( + label='Start Date', + input_formats=[settings.API_DATEFORMAT], + widget=forms.DateTimeInput( + attrs={ + 'placeholder': 'yyyy-mm-dd', + 'class': 'form-control', + } + ), + required=False, + ) + end_date = forms.DateTimeField( + label='End Date', + input_formats=[settings.API_DATEFORMAT], + widget=forms.DateTimeInput( + attrs={ + 'placeholder': 'yyyy-mm-dd', + 'class': 'form-control', + } + ), + required=False, + ) + limit = forms.IntegerField( + label='Limit', + widget=forms.NumberInput( + attrs={ + 'class': 'form-control', + } + ), + initial=100, + required=False, + ) + offset = forms.IntegerField( + label='Offset', + widget=forms.NumberInput( + attrs={ + 'class': 'form-control', + } + ), + initial=0, + required=False, + ) + + def __init__(self, *args, **kwargs): + kwargs.setdefault('label_suffix', '') + super(MetricsForm, self).__init__(*args, **kwargs) + + +class APIMetricsForm(MetricsForm): + ANONYMOUS = ( + ('', 'Anonymous and Non-Anonymous'), + ('true', 'Yes'), + ('false', 'No'), + ) + VERB = ( + ('', 'Any'), + ('DELETE', 'DELETE'), + ('GET', 'GET'), + ('POST', 'POST'), + ('PUT', 'PUT'), + ) + VERSION = ( + ('', 'Any'), + ('v1.2', '1.2'), + ('v1.2.1', '1.2.1'), + ('v1.3.0', '1.3.0'), + ('v1.4.0', '1.4.0'), + ('v2.0.0', '2.0.0'), + ('v2.1.0', '2.1.0'), + ('v2.2.0', '2.2.0'), + ('v3.0.0', '3.0.0'), + ) + + consumer_id = forms.CharField( + label='Consumer ID', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + user_id = forms.CharField( + label='User ID', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + anon = forms.ChoiceField( + label='Anonymous', + choices=ANONYMOUS, + widget=forms.Select( + attrs={ + 'class': 'form-control', + } + ), + initial='', + required=False, + ) + app_name = forms.CharField( + label='App Name', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + verb = forms.ChoiceField( + label='Verb', + choices=VERB, + widget=forms.Select( + attrs={ + 'class': 'form-control', + } + ), + initial='', + required=False, + ) + url = forms.CharField( + label='URL', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + implemented_by_partial_function = forms.CharField( + label='Implemented By Partial Function', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + implemented_in_version = forms.ChoiceField( + label='Implemented In Version', + choices=VERSION, + widget=forms.Select( + attrs={ + 'class': 'form-control', + } + ), + initial='', + required=False, + ) + + +class ConnectorMetricsForm(MetricsForm): + # override start_date until API returns values without given date + start_date = forms.DateTimeField( + label='Start Date', + input_formats=[settings.API_DATEFORMAT], + widget=forms.DateTimeInput( + attrs={ + 'placeholder': 'yyyy-mm-dd', + 'class': 'form-control', + } + ), + initial='1970-01-01', + required=True, + ) + connector_name = forms.CharField( + label='Connector Name', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + function_name = forms.CharField( + label='Function Name', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) + obp_api_request_id = forms.CharField( + label='OBP API Request ID', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + } + ), + required=False, + ) diff --git a/apimanager/metrics/static/metrics/css/metrics.css b/apimanager/metrics/static/metrics/css/metrics.css index e7bf15d9..0bb47704 100644 --- a/apimanager/metrics/static/metrics/css/metrics.css +++ b/apimanager/metrics/static/metrics/css/metrics.css @@ -2,10 +2,10 @@ margin-bottom: 10px; } -#metrics #metrics-list { - margin-top: 20px; +#metrics #metrics-data { + margin-top: 40px; } - + #metrics #metrics-list ul { margin-left: -25px; } diff --git a/apimanager/metrics/templates/metrics/api.html b/apimanager/metrics/templates/metrics/api.html new file mode 100644 index 00000000..2ee9abb9 --- /dev/null +++ b/apimanager/metrics/templates/metrics/api.html @@ -0,0 +1,183 @@ +{% extends 'base.html' %} +{% load static %} + +{% block page_title %}{{ block.super }} / API Metrics{% endblock page_title %} + +{% block content %} +
+

API Metrics

+ +
+

Filter

+
+ {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + +
+
+ {% if form.start_date.errors %}
{{ form.start_date.errors }}
{% endif %} +
+ {{ form.start_date.label_tag }} + {{ form.start_date }} +
+
+
+ {% if form.end_date.errors %}
{{ form.end_date.errors }}
{% endif %} +
+ {{ form.end_date.label_tag }} + {{ form.end_date }} +
+
+
+ {% if form.limit.errors %}
{{ form.limit.errors }}
{% endif %} +
+ {{ form.limit.label_tag }} + {{ form.limit }} +
+
+
+ {% if form.offset.errors %}
{{ form.offset.errors }}
{% endif %} +
+ {{ form.offset.label_tag }} + {{ form.offset }} +
+
+
+ +
+
+ {% if form.consumer_id.errors %}
{{ form.consumer_id.errors }}
{% endif %} +
+ {{ form.consumer_id.label_tag }} + {{ form.consumer_id }} +
+
+ +
+ {% if form.user_id.errors %}
{{ form.user_id.errors }}
{% endif %} +
+ {{ form.user_id.label_tag }} + {{ form.user_id }} +
+
+ +
+ {% if form.anon.errors %}
{{ form.anon.errors }}
{% endif %} +
+ {{ form.anon.label_tag }} + {{ form.anon }} +
+
+ +
+ {% if form.app_name.errors %}
{{ form.app_name.errors }}
{% endif %} +
+ {{ form.app_name.label_tag }} + {{ form.app_name }} +
+
+ +
+ +
+
+ {% if form.verb.errors %}
{{ form.verb.errors }}
{% endif %} +
+ {{ form.verb.label_tag }} + {{ form.verb }} +
+
+ +
+ {% if form.url.errors %}
{{ form.url.errors }}
{% endif %} +
+ {{ form.url.label_tag }} + {{ form.url }} +
+
+
+ +
+
+ {% if form.implemented_by_partial_function.errors %}
{{ form.implemented_by_partial_function.errors }}
{% endif %} +
+ {{ form.implemented_by_partial_function.label_tag }} + {{ form.implemented_by_partial_function }} +
+
+ +
+ {% if form.implemented_in_version.errors %}
{{ form.implemented_in_version.errors }}
{% endif %} +
+ {{ form.implemented_in_version.label_tag }} + {{ form.implemented_in_version }} +
+
+
+ + + +
+
+ +
+ + +
+ {% block tab_content %} +
+
+ + + + + + + + + + + + {% for metric in metrics %} + + + + + + + + {% endfor %} + +
#VerbURLDateDetails
{{ forloop.counter }}{{ metric.verb }} + {{ metric.url }} + {{ metric.date|date:"Y-m-d H:m:s" }} +
    +
  • User Name: {{ metric.user_name }}
  • +
  • User ID: {{ metric.user_id }}
  • +
  • Developer Email: {{ metric.developer_email }}
  • +
  • App Name: {{ metric.app_name }}
  • +
  • Consumer ID: {{ metric.consumer_id }}
  • +
  • Implemented by Partial Function: {{ metric.implemented_by_partial_function }}
  • +
  • Implemented In Version: {{ metric.implemented_in_version }}
  • +
+
+
+
+ {% endblock tab_content %} +
+
+ +
+{% endblock %} + +{% block extracss %} + +{% endblock extracss %} diff --git a/apimanager/metrics/templates/metrics/api_summary_partial_function.html b/apimanager/metrics/templates/metrics/api_summary_partial_function.html new file mode 100644 index 00000000..ab6c1dc7 --- /dev/null +++ b/apimanager/metrics/templates/metrics/api_summary_partial_function.html @@ -0,0 +1,21 @@ +{% extends 'metrics/api.html' %} +{% load static %} + +{% block nav_tabs %} +
  • List
  • +
  • Summary by Partial Function
  • +{% endblock nav_tabs %} + +{% block tab_content %} +
    + +
    +{% endblock tab_content %} + +{% block extrajs %} + + + +{% endblock extrajs %} diff --git a/apimanager/metrics/templates/metrics/connector.html b/apimanager/metrics/templates/metrics/connector.html new file mode 100644 index 00000000..51232c91 --- /dev/null +++ b/apimanager/metrics/templates/metrics/connector.html @@ -0,0 +1,113 @@ +{% extends 'base.html' %} +{% load static %} + +{% block page_title %}{{ block.super }} / Connector Metrics{% endblock page_title %} + +{% block content %} +
    +

    Connector Metrics

    + +
    +

    Filter

    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + +
    +
    + {% if form.start_date.errors %}
    {{ form.start_date.errors }}
    {% endif %} +
    + {{ form.start_date.label_tag }} + {{ form.start_date }} +
    +
    +
    + {% if form.end_date.errors %}
    {{ form.end_date.errors }}
    {% endif %} +
    + {{ form.end_date.label_tag }} + {{ form.end_date }} +
    +
    +
    + {% if form.limit.errors %}
    {{ form.limit.errors }}
    {% endif %} +
    + {{ form.limit.label_tag }} + {{ form.limit }} +
    +
    +
    + {% if form.offset.errors %}
    {{ form.offset.errors }}
    {% endif %} +
    + {{ form.offset.label_tag }} + {{ form.offset }} +
    +
    +
    + +
    +
    + {% if form.connector_name.errors %}
    {{ form.connector_name.errors }}
    {% endif %} +
    + {{ form.connector_name.label_tag }} + {{ form.connector_name }} +
    +
    +
    + {% if form.function_name.errors %}
    {{ form.function_name.errors }}
    {% endif %} +
    + {{ form.function_name.label_tag }} + {{ form.function_name }} +
    +
    +
    + {% if form.obp_api_request_id.errors %}
    {{ form.obp_api_request_id.errors }}
    {% endif %} +
    + {{ form.obp_api_request_id.label_tag }} + {{ form.obp_api_request_id }} +
    +
    +
    + + +
    +
    + + +
    +
    + + + + + + + + + + + + + {% for metric in metrics %} + + + + + + + + + {% endfor %} + +
    #DateConnector NameFunction NameOBP API Request IDDuration (ms)
    {{ forloop.counter }}{{ metric.date|date:"Y-m-d H:m:s" }}{{ metric.connector_name }}{{ metric.function_name }}{{ metric.obp_api_request_id }}{{ metric.duration }}
    +
    +
    + +
    +{% endblock %} + +{% block extracss %} + +{% endblock extracss %} diff --git a/apimanager/metrics/templates/metrics/index.html b/apimanager/metrics/templates/metrics/index.html deleted file mode 100644 index d76a51cd..00000000 --- a/apimanager/metrics/templates/metrics/index.html +++ /dev/null @@ -1,196 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block page_title %}{{ block.super }} / Metrics{% endblock page_title %} - -{% block content %} -
    -

    Metrics

    - -

    Filter

    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    -
    - -
    -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - {% with request.GET.anon as anon %} - - {% endwith %} -
    -
    - -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    - -
    -
    - - -
    -
    -
    - -
    -
    -
    - - -
    -
    - -
    -
    - - -
    -
    -
    - - - -
    - - - - -
    - {% block tab_content %} -
    -
    - - - - - - - - - - - - {% for metric in metrics %} - - - - - - - - {% endfor %} - -
    #VerbURLDateDetails
    {{ forloop.counter }}{{ metric.verb }} - {{ metric.url }} - {{ metric.date|date:"Y-m-d H:m:s" }} -
      -
    • User Name: {{ metric.user_name }}
    • -
    • User ID: {{ metric.user_id }}
    • -
    • Developer Email: {{ metric.developer_email }}
    • -
    • App Name: {{ metric.app_name }}
    • -
    • Consumer ID: {{ metric.consumer_id }}
    • -
    • Implemented by Partial Function: {{ metric.implemented_by_partial_function }}
    • -
    • Implemented In Version: {{ metric.implemented_in_version }}
    • -
    -
    -
    -
    - {% endblock tab_content %} -
    - -
    -{% endblock %} - -{% block extrajs %} - - - -{% endblock extrajs %} - -{% block extracss %} - -{% endblock extracss %} diff --git a/apimanager/metrics/templates/metrics/summary_partial_function.html b/apimanager/metrics/templates/metrics/summary_partial_function.html deleted file mode 100644 index 476310f1..00000000 --- a/apimanager/metrics/templates/metrics/summary_partial_function.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'metrics/index.html' %} -{% load static %} - -{% block nav_tabs %} -
  • List
  • -
  • Summary by Partial Function
  • -{% endblock nav_tabs %} - -{% block tab_content %} -
    - -
    -{% endblock tab_content %} diff --git a/apimanager/metrics/urls.py b/apimanager/metrics/urls.py index 57bd7142..ad682792 100644 --- a/apimanager/metrics/urls.py +++ b/apimanager/metrics/urls.py @@ -5,14 +5,20 @@ from django.conf.urls import url -from .views import IndexView, SummaryPartialFunctionView +from .views import ( + APIMetricsView, + APISummaryPartialFunctionView, + ConnectorMetricsView +) urlpatterns = [ - url(r'^$', - IndexView.as_view(), - name='metrics-index'), - url(r'^summary-partial-function$', - SummaryPartialFunctionView.as_view(), - name='metrics-summary-partial-function'), - + url(r'^api/$', + APIMetricsView.as_view(), + name='api-metrics'), + url(r'^api/summary-partial-function$', + APISummaryPartialFunctionView.as_view(), + name='api-metrics-summary-partial-function'), + url(r'^connector/$', + ConnectorMetricsView.as_view(), + name='connector-metrics'), ] diff --git a/apimanager/metrics/views.py b/apimanager/metrics/views.py index d98dffdb..56060e8c 100644 --- a/apimanager/metrics/views.py +++ b/apimanager/metrics/views.py @@ -12,10 +12,12 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import TemplateView +from django.views.generic import FormView, TemplateView +from django.utils.http import urlquote from base.api import api, APIError +from .forms import APIMetricsForm, ConnectorMetricsForm def get_random_color(to_hash): @@ -26,7 +28,6 @@ def get_random_color(to_hash): return 'rgba({}, {}, {}, 0.3)'.format(r, g, b) - def get_barchart_data(metrics, fieldname): """ Gets bar chart data compatible with Chart.js from the field with given @@ -57,56 +58,97 @@ def get_barchart_data(metrics, fieldname): return data +class MetricsView(LoginRequiredMixin, TemplateView): + """View for metrics (sort of abstract base class)""" + form_class = None + template_name = None + api_urlpath = None -class IndexView(LoginRequiredMixin, TemplateView): - """Index view for metrics""" - template_name = "metrics/index.html" - - def scrub(self, metrics): - """Scrubs data in the given consumers to adher to certain formats""" + def get_form(self): + """ + Get bound form either from request.GET or initials + We need a bound form because we already send a request to the API + without user intervention on initial request + """ + if self.request.GET: + data = self.request.GET + else: + fields = self.form_class.declared_fields + data = {} + for name, field in fields.items(): + if field.initial: + data[name] = field.initial + form = self.form_class(data) + return form + + def to_django(self, metrics): + """ + Convert metrics data from API to format understood by Django + - Make datetime out of string in field 'date' + """ for metric in metrics: metric['date'] = datetime.strptime( metric['date'], settings.API_DATETIMEFORMAT) return metrics - - def get_params(self, request_get): + def to_api(self, cleaned_data): """ - API treats empty parameters as actual values, so we have to filter - them out + Convert form data from Django to format understood by API + - API treats empty parameters as actual values, so we have to remove + them + - Need to convert datetimes into required format + """ + params = [] + for name, value in cleaned_data.items(): + # Maybe we should define the API format as Django format to not + # have to convert in places like this? + if value.__class__.__name__ == 'datetime': + value = value.strftime(settings.API_DATEFORMAT) + if value: + # API does not like quoted data + params.append('{}={}'.format(name, value)) + params = '&'.join(params) + return params + + def get_metrics(self, cleaned_data): + """ + Gets the metrics from the API, using given cleaned form data. """ - querydict = request_get.copy() - keys = list(querydict.keys()) - for key in keys: - if not querydict[key]: - querydict.pop(key) - return querydict.urlencode() - - - def get_context_data(self, **kwargs): - context = super(IndexView, self).get_context_data(**kwargs) metrics = [] - params = self.get_params(self.request.GET) + params = self.to_api(cleaned_data) + urlpath = '{}?{}'.format(self.api_urlpath, params) try: - urlpath = '/management/metrics?{}'.format(params) metrics = api.get(self.request, urlpath) - metrics = self.scrub(metrics['metrics']) + metrics = self.to_django(metrics['metrics']) except APIError as err: messages.error(self.request, err) + return metrics + def get_context_data(self, **kwargs): + context = super(MetricsView, self).get_context_data(**kwargs) + metrics = [] + form = self.get_form() + if form.is_valid(): + metrics = self.get_metrics(form.cleaned_data) context.update({ 'metrics': metrics, - 'barchart_data': json.dumps({}) + 'form': form, }) return context +class APIMetricsView(MetricsView): + """View for API metrics""" + form_class = APIMetricsForm + template_name = 'metrics/api.html' + api_urlpath = '/management/metrics' + -class SummaryPartialFunctionView(IndexView): - template_name = "metrics/summary_partial_function.html" +class APISummaryPartialFunctionView(APIMetricsView): + template_name = 'metrics/api_summary_partial_function.html' def get_context_data(self, **kwargs): - context = super(SummaryPartialFunctionView, self).get_context_data( + context = super(APISummaryPartialFunctionView, self).get_context_data( **kwargs) barchart_data = json.dumps(get_barchart_data( context['metrics'], 'implemented_by_partial_function')) @@ -115,3 +157,10 @@ def get_context_data(self, **kwargs): 'barchart_data': barchart_data, }) return context + + +class ConnectorMetricsView(MetricsView): + """View for connector metrics""" + form_class = ConnectorMetricsForm + template_name = 'metrics/connector.html' + api_urlpath = '/management/connector/metrics'