From 30cd3b0764d7566f10af0ac2ce33f37f8a88bc51 Mon Sep 17 00:00:00 2001 From: Flavio Tarsetti <Flavio.Tarsetti@idiap.ch> Date: Wed, 1 Jun 2016 16:54:15 +0200 Subject: [PATCH] [plotters] added Plotters view in web-app --- beat/web/plotters/api.py | 272 ++++++++++++ beat/web/plotters/api_urls.py | 38 ++ beat/web/plotters/models.py | 18 +- beat/web/plotters/serializers.py | 136 +++++- .../web/plotters/templates/plotters/list.html | 35 ++ .../templates/plotters/panels/actions.html | 36 ++ .../templates/plotters/panels/editor.html | 259 +++++++++++ .../templates/plotters/panels/table.html | 117 +++++ .../web/plotters/templates/plotters/view.html | 149 +++++++ beat/web/plotters/templatetags/__init__.py | 0 .../web/plotters/templatetags/plotter_tags.py | 168 ++++++++ beat/web/plotters/tests.py | 404 ++++++++++++++++++ beat/web/plotters/urls.py | 30 ++ beat/web/plotters/views.py | 87 ++++ beat/web/reports/templatetags/report_tags.py | 2 + .../ui/contribution_breadcrumb_plotter.html | 47 ++ beat/web/ui/templatetags/ui_tags.py | 23 + 17 files changed, 1818 insertions(+), 3 deletions(-) create mode 100644 beat/web/plotters/templates/plotters/list.html create mode 100644 beat/web/plotters/templates/plotters/panels/actions.html create mode 100644 beat/web/plotters/templates/plotters/panels/editor.html create mode 100644 beat/web/plotters/templates/plotters/panels/table.html create mode 100644 beat/web/plotters/templates/plotters/view.html create mode 100644 beat/web/plotters/templatetags/__init__.py create mode 100644 beat/web/plotters/templatetags/plotter_tags.py create mode 100644 beat/web/plotters/tests.py create mode 100644 beat/web/ui/templates/ui/contribution_breadcrumb_plotter.html diff --git a/beat/web/plotters/api.py b/beat/web/plotters/api.py index 3358756c2..0838d4052 100644 --- a/beat/web/plotters/api.py +++ b/beat/web/plotters/api.py @@ -28,14 +28,27 @@ from ..common.api import ListContributionView from .models import Plotter, PlotterParameter, DefaultPlotter from .serializers import PlotterSerializer, PlotterParameterSerializer, DefaultPlotterSerializer +from .serializers import PlotterAllSerializer, PlotterCreationSerializer, FullPlotterSerializer +from .serializers import PlotterParameterAllSerializer, PlotterParameterCreationSerializer, FullPlotterParameterSerializer from django.db.models import Q +from ..code.api import ShareCodeView, RetrieveUpdateDestroyCodeView +from ..code.serializers import CodeDiffSerializer + +from ..common.api import (CheckContributionNameView, ShareView, DiffView, + ListContributionView, ListCreateContributionView, RetrieveUpdateDestroyContributionView) +from ..common.utils import validate_restructuredtext, ensure_html +from ..common.responses import BadRequestResponse + from rest_framework import generics from rest_framework import views from rest_framework import permissions from rest_framework.response import Response from rest_framework import status +from django.shortcuts import get_object_or_404 +from django.utils import six +from django.core.exceptions import ValidationError class ListPlotterView(ListContributionView): """ @@ -93,3 +106,262 @@ class ListDefaultPlotterView(generics.ListAPIView): def get_queryset(self): queryset = DefaultPlotter.objects.all() return queryset + + +#---------------------------------------------------------- + + +class CheckPlotterNameView(CheckContributionNameView): + """ + This view sanitizes a Plotter name and + checks whether it is already used. + """ + model = Plotter + + +#---------------------------------------------------------- + + +class SharePlotterView(ShareCodeView): + """ + This view allows to share a plotter with + other users and/or teams + """ + model = Plotter + + +#---------------------------------------------------------- + + +class ListCreatePlottersView(ListCreateContributionView): + """ + Read/Write end point that list the plotters available + from a given author and allows the creation of new plotters + """ + model = Plotter + serializer_class = PlotterAllSerializer + writing_serializer_class = PlotterCreationSerializer + namespace = 'api_plotters' + + +#---------------------------------------------------------- + + +class RetrieveUpdateDestroyPlottersView(RetrieveUpdateDestroyCodeView): + """ + Read/Write/Delete endpoint for a given plotter + """ + model = Plotter + serializer_class = FullPlotterSerializer + + def get_queryset(self): + plotter_author = self.kwargs.get('author_name') + plotter_name = self.kwargs.get('object_name') + plotter_version = self.kwargs.get('version') + + #plotter = get_object_or_404(Plotter, author__username=plotter_author, name=plotter_name, version=plotter_version) + plotter = Plotter.objects.filter(author__username=plotter_author, name=plotter_name, version=plotter_version) + self.check_object_permissions(self.request, plotter) + return plotter + + #def do_update(self, request, author_name, object_name, version=None): + # modified, algorithm = super(RetrieveUpdateDestroyPlottersView, self).do_update(request, author_name, object_name, version) + + # if modified: + # # Delete existing experiments using the algorithm (code changed) + # experiments = list(set(map(lambda x: x.experiment, + # algorithm.blocks.iterator()))) + # for experiment in experiments: experiment.delete() + + # return modified, algorithm + + +##---------------------------------------------------------- +# +# +#class DiffPlotterView(DiffView): +# """ +# This view shows the differences between two algorithms +# """ +# model = Algorithm +# serializer_class = CodeDiffSerializer + +#---------------------------------------------------------- + + +class CheckPlotterParameterNameView(CheckContributionNameView): + """ + This view sanitizes a PlotterParameter name and + checks whether it is already used. + """ + model = PlotterParameter + + +#---------------------------------------------------------- + + +class SharePlotterParameterView(ShareView): + """ + This view allows to share a PlotterParameter with + other users and/or teams + """ + model = PlotterParameter + + +#---------------------------------------------------------- + + +class ListPlotterParametersView(ListCreateContributionView): + """ + List all available PlotterParameters + """ + model = PlotterParameter + serializer_class = FullPlotterParameterSerializer + writing_serializer_class = PlotterParameterCreationSerializer + namespace = 'api_plotters' + + #def get_queryset(self): + # # get all plotterparameter accessible for user (private+public) + # queryset = PlotterParameter.objects.for_user(self.request.user, True) + + # return queryset + + +#---------------------------------------------------------- + + +#class ListCreatePlotterParametersView(ListCreateContributionView): +# """ +# Read/Write end point that list the PlotterParameters available +# from a given author and allows the creation of new PlotterParameters +# """ +# model = PlotterParameter +# serializer_class = FullPlotterParameterSerializer +# namespace = 'api_plotterparameters' + + +#---------------------------------------------------------- + + +class RetrieveUpdateDestroyPlotterParametersView(RetrieveUpdateDestroyContributionView): + """ + Read/Write/Delete endpoint for a given PlotterParameter + """ + model = PlotterParameter + serializer_class = FullPlotterParameterSerializer + + + def put(self, request, author_name, object_name, version=None): + if version is None: + return BadRequestResponse('A version number must be provided') + + try: + data = request.data + except ParseError as e: + raise serializers.ValidationError({'data': str(e)}) + else: + if not data: + raise serializers.ValidationError({'data': 'Empty'}) + + + if data.has_key('short_description'): + if not(isinstance(data['short_description'], six.string_types)): + raise serializers.ValidationError({'short_description', 'Invalid short_description data'}) + short_description = data['short_description'] + else: + short_description = None + + if data.has_key('description'): + if not(isinstance(data['description'], six.string_types)): + raise serializers.ValidationError({'description': 'Invalid description data'}) + description = data['description'] + try: + validate_restructuredtext(description) + except ValidationError as errors: + raise serializers.ValidationError({'description': [error for error in errors]}) + else: + description = None + + if data.has_key('strict'): + strict = data['strict'] + else: + strict = True + + plotter = None + if data.has_key('plotter'): + try: + if isinstance(data['plotter'], int): + plotter = Plotter.objects.get(id=data['plotter']) + else: + return BadRequestResponse('A valid plotter id number must be provided') + except: + return BadRequestResponse('A valid plotter id number must be provided') + + if (short_description is not None) and (len(short_description) > self.model._meta.get_field('short_description').max_length): + raise serializers.ValidationError({'short_description': 'Short description too long'}) + + + # Process the query string + if request.GET.has_key('fields'): + fields_to_return = request.GET['fields'].split(',') + else: + # Available fields (not returned by default): + # - html_description + fields_to_return = ['errors'] + + + # Retrieve the plotterparameter + dbplotterparameter = get_object_or_404(PlotterParameter, + author__username__iexact=author_name, + name__iexact=object_name, + version=version) + + # Check that the object can still be modified (if applicable, the + # documentation can always be modified) + if not dbplotterparameter.modifiable(): + raise PermissionDenied("The {} isn't modifiable anymore (either shared with someone else, or needed by an attestation)".format(dbplotterparameter.model_name())) + + errors = None + + + # Modification of the short_description + if (short_description is not None): + dbplotterparameter.short_description = short_description + + # Modification of the description + if description is not None: + dbplotterparameter.description = description + + # Modification of the plotter + if plotter is not None: + dbplotterparameter.plotter = plotter + + # Save the plotterparameter model + try: + dbplotterparameter.save() + except Exception as e: + return BadRequestResponse(str(e)) + + # Nothing to return? + if len(fields_to_return) == 0: + return Response(status=204) + + result = {} + + # Retrieve the errors (if necessary) + if 'errors' in fields_to_return: + if errors: + result['errors'] = errors + else: + result['errors'] = '' + + + # Retrieve the description in HTML format (if necessary) + if 'html_description' in fields_to_return: + description = dbplotterparameter.description + if len(description) > 0: + result['html_description'] = ensure_html(description) + else: + result['html_description'] = '' + + return Response(result) diff --git a/beat/web/plotters/api_urls.py b/beat/web/plotters/api_urls.py index 4b67b769e..34f18e85a 100644 --- a/beat/web/plotters/api_urls.py +++ b/beat/web/plotters/api_urls.py @@ -33,6 +33,44 @@ urlpatterns = [ url(r'^$', api.ListPlotterView.as_view(), name='all'), url(r'^format/(?P<author_name>\w+)/(?P<dataformat_name>[a-zA-Z0-9_\-]+)/(?P<version>\d+)/$', api.ListFormatPlotterView.as_view(), name='object'), url(r'^plotterparameter/$', api.ListPlotterParameterView.as_view(), name='all_plotterparameter'), + url(r'^plotterparameters/(?P<author_name>\w+)/(?P<object_name>[a-zA-Z0-9_\-]+)/(?P<version>\d+)/$', api.RetrieveUpdateDestroyPlotterParametersView.as_view(), name='view'), + url(r'^plotterparameters/(?P<author_name>\w+)/$', api.ListPlotterParametersView.as_view(), name='view'), url(r'^plotterparameter/(?P<author_name>\w+)/(?P<dataformat_name>[a-zA-Z0-9_\-]+)/(?P<version>\d+)/$', api.ListPlotterParameterView.as_view(), name='plotterparameter'), url(r'^defaultplotters/$', api.ListDefaultPlotterView.as_view(), name='all_defaultplotters'), + + #url(r'^$', + # api.ListPlottersView.as_view(), + # name='all_plotters', + # ), + + url(r'^check_name/$', + api.CheckPlotterNameView.as_view(), + name='check_name', + ), + + #url(r'^diff/(?P<author1>\w+)/(?P<name1>[-\w]+)/(?P<version1>\d+)/(?P<author2>\w+)/(?P<name2>[-\w]+)/(?P<version2>\d+)/$', + # api.DiffPlotterView.as_view(), + # name='diff', + # ), + + url(r'^(?P<author_name>\w+)/(?P<object_name>[-\w]+)/(?P<version>\d+)/share/$', + api.SharePlotterView.as_view(), + name='share', + ), + + url(r'^(?P<author_name>\w+)/$', + api.ListCreatePlottersView.as_view(), + name='list_create', + ), + + url(r'^(?P<author_name>\w+)/(?P<object_name>[-\w]+)/(?P<version>\d+)/$', + api.RetrieveUpdateDestroyPlottersView.as_view(), + name='object', + ), + + #url(r'^(?P<author_name>\w+)/(?P<object_name>[-\w]+)/$', + # api.RetrieveUpdateDestroyPlottersView.as_view(), + # name='object', + # ), + ] diff --git a/beat/web/plotters/models.py b/beat/web/plotters/models.py index 91f6e9e01..e8189d042 100755 --- a/beat/web/plotters/models.py +++ b/beat/web/plotters/models.py @@ -27,6 +27,7 @@ from django.db import models from django.conf import settings +from django.core.urlresolvers import reverse from django.contrib.auth.models import User @@ -234,11 +235,11 @@ class Plotter(Code): def modifiable(self): """Can be modified if nobody points at me""" - return super(Plotter, self).modifiable() and ((self.search_uses.count() + self.defaults.count()) == 0) + return super(Plotter, self).modifiable() and (self.defaults.count() == 0) def deletable(self): """Can be deleted if nobody points at me""" - return super(Plotter, self).deletable() and ((self.search_uses.count() + self.defaults.count()) == 0) + return super(Plotter, self).deletable() and (self.defaults.count() == 0) def core(self): return beat.core.plotter.Plotter(settings.PREFIX, self.fullname()) @@ -247,6 +248,19 @@ class Plotter(Code): def core_format(self): return beat.core.dataformat.DataFormat(settings.PREFIX, self.dataformat.fullname()) + def json_parameters(self): + return self.declaration['parameters'] if self.declaration['parameters'] else {} + + #_____ Utilities __________ + + def get_absolute_url(self): + + return reverse( + 'plotters:view', + args=(self.author.username, self.name, self.version,), + ) + + class PlotterParameter(Contribution): diff --git a/beat/web/plotters/serializers.py b/beat/web/plotters/serializers.py index cf1cf368a..11459820b 100644 --- a/beat/web/plotters/serializers.py +++ b/beat/web/plotters/serializers.py @@ -25,10 +25,18 @@ # # ############################################################################### -from ..common.serializers import DynamicFieldsSerializer, ContributionSerializer +from ..common.serializers import DynamicFieldsSerializer, ContributionSerializer, ContributionCreationSerializer from .models import Plotter, PlotterParameter, DefaultPlotter from rest_framework import serializers +from ..code.serializers import CodeSerializer, CodeCreationSerializer +from ..libraries.serializers import LibraryReferenceSerializer +from ..dataformats.serializers import ReferencedDataFormatSerializer + + +import beat.core.plotter +import simplejson as json + class PlotterSerializer(ContributionSerializer): dataformat = serializers.CharField(source="dataformat.fullname") @@ -59,3 +67,129 @@ class DefaultPlotterSerializer(DynamicFieldsSerializer): default_fields = [ 'dataformat', 'plotter', 'parameter', ] + + +#---------------------------------------------------------- + + +class PlotterCreationSerializer(CodeCreationSerializer): + class Meta(CodeCreationSerializer.Meta): + model = Plotter + beat_core_class = beat.core.plotter + + +#---------------------------------------------------------- + + +class PlotterAllSerializer(CodeSerializer): + dataformat = serializers.SerializerMethodField() + declaration_file = serializers.SerializerMethodField() + description_file = serializers.SerializerMethodField() + source_code_file = serializers.SerializerMethodField() + referenced_libraries = LibraryReferenceSerializer(many=True) + + class Meta(CodeSerializer.Meta): + model = Plotter + + +#---------------------------------------------------------- + + +class FullPlotterSerializer(PlotterAllSerializer): + + class Meta(PlotterAllSerializer.Meta): + default_fields = PlotterAllSerializer.Meta.default_fields + PlotterAllSerializer.Meta.extra_fields + + +#---------------------------------------------------------- + +class PlotterParameterCreationFailedException(Exception): + pass + +class PlotterParameterCreationSerializer(ContributionCreationSerializer): + class Meta(ContributionCreationSerializer.Meta): + model = PlotterParameter + #beat_core_class = beat.core.PlotterParameter + def create(self, validated_data): + plotterparameter = None + + if not validated_data.has_key("name"): + raise serializers.ValidationError('No name provided') + + try: + plotterparameter = PlotterParameter.objects.get(author=self.context['request'].user, name=validated_data['name']) + except: + pass + + if plotterparameter is not None: + raise serializers.ValidationError('A plotterparameter with this name already exists') + + validated_data['data'] = {} + plotterparameter = PlotterParameter.objects.create(**validated_data) + if plotterparameter is None: + raise PlotterParameterCreationFailedException() + return plotterparameter + + +#---------------------------------------------------------- + + +class PlotterParameterAllSerializer(ContributionSerializer): + data = serializers.SerializerMethodField() + description = serializers.SerializerMethodField() + plotter = serializers.SerializerMethodField() + + class Meta(ContributionSerializer.Meta): + model = PlotterParameter + + #def get_referencing_experiments(self, obj): + # user = self.context.get('user') + + # experiments = obj.experiments.for_user(user, True).order_by('-creation_date') + # serializer = ExperimentSerializer(experiments, many=True) + # referencing_experiments = serializer.data + + # # Put the pending experiments first + # ordered_result = filter(lambda x: x['creation_date'] is None, referencing_experiments) + # ordered_result += filter(lambda x: x['creation_date'] is not None, referencing_experiments) + + # return ordered_result + + #def get_new_experiment_url(self, obj): + # return obj.get_new_experiment_url() + +#---------------------------------------------------------- + + +class FullPlotterParameterSerializer(PlotterParameterAllSerializer): + + class Meta(PlotterParameterAllSerializer.Meta): + #exclude = ['declaration'] + exclude = [] + #default_fields = PlotterParameterAllSerializer.Meta.default_fields + PlotterParameterAllSerializer.Meta.extra_fields + default_fields = ['id', 'accessibility', 'modifiable', 'deletable', 'is_owner', 'name', 'fork_of', 'last_version', 'previous_version', 'short_description', 'description', 'version', 'creation_date', 'data', 'plotter'] + + def get_data(self, obj): + return json.loads(obj.data) + + def get_plotter(self, obj): + if obj.plotter is not None: + return obj.plotter.fullname() + else: + return "undefined plotter" + + #def get_plotter(self, obj): + # return obj.author.username + + #"accessibility": "public", + #"modifiable": true, + #"deletable": true, + #"is_owner": false, + #"name": "plot/bar/1", + #"fork_of": null, + #"last_version": true, + #"previous_version": null, + #"short_description": "Default parameters for bar plots", + #"description": "Raw content", + #"version": 1, + #"creation_date": "2015-09-03T16:55:47.620000", diff --git a/beat/web/plotters/templates/plotters/list.html b/beat/web/plotters/templates/plotters/list.html new file mode 100644 index 000000000..f067b5484 --- /dev/null +++ b/beat/web/plotters/templates/plotters/list.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +{% load plotter_tags %} +{% load ui_tags %} + +{% block title %}{{ block.super }} - {% if author.is_anonymous %}Public{% else %}{{ author.username }}'s{% endif %} Plotters{% endblock %} + +{% block content %} + +{% list_tabs author "plotters" %} + +{% plotter_table objects owner "plotter-list" %} + +{% endblock %} diff --git a/beat/web/plotters/templates/plotters/panels/actions.html b/beat/web/plotters/templates/plotters/panels/actions.html new file mode 100644 index 000000000..6dcaf415d --- /dev/null +++ b/beat/web/plotters/templates/plotters/panels/actions.html @@ -0,0 +1,36 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} +{% load plotter_tags %} + +<div class="btn-group btn-group-sm action-buttons pull-right"> + + <!-- Edit, as admin --> + {% if request.user.is_staff %} + <a class="btn btn-default btn-edit" href="{% url 'admin:plotters_plotter_change' object.id %}" data-toggle="tooltip" data-placement="bottom" title="Edit as admin"><i class="fa fa-cogs fa-lg"></i></a> + {% endif %} + + {% if open_source and not request.user.is_anonymous %} + <!-- Fork button, needs to be logged in --> + <a class="btn btn-default btn-fork" href="{% url 'plotters:fork' object.author.username object.name object.version %}" data-toggle="tooltip" data-placement="bottom" title="Fork"><i class="fa fa-code-fork fa-lg"></i></a> + {% endif %} + +</div> diff --git a/beat/web/plotters/templates/plotters/panels/editor.html b/beat/web/plotters/templates/plotters/panels/editor.html new file mode 100644 index 000000000..6d05895f2 --- /dev/null +++ b/beat/web/plotters/templates/plotters/panels/editor.html @@ -0,0 +1,259 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} +{% load ui_tags %} +<div class="row"> + <div class="col-sm-10"> + {% if object.splittable or object.analysis %} + <div class="alert alert-{% if object.splittable %}success{% else %}info{% endif %}"> + {% if object.splittable %}<i class="fa fa-random"></i> This plotter is <strong><span class="text-success">splittable</span></strong>{% endif %} + {% if object.analysis %}<i class="fa fa-line-chart"></i> This plotter is an <strong><span class="text-primary">analyzer</span></strong>. It can only be used on analysis blocks.{% endif %} + </div> + {% endif %} + + {% with parameters=object.json_parameters uses_and_groups=object.uses_and_groups %} + + {% with uses_and_groups.0 as uses %} + {% with uses_and_groups.1 as groups %} + {% if parameters or uses or groups %} + <div class="panel-group" role="tablist" aria-multiselectable="true"> + + {% if groups %} + <div id="groups-panel" class="panel panel-default"> + <div class="panel-heading" role="tab" id="heading-groups"> + <h4 class="panel-title"> + <a class="collapsed" role="button" data-toggle="collapse" data-parent="#groups-panel" href="#collapse-groups" aria-expanded="false" aria-controls="collapse-groups"> + Endpoint Groups + </a> <span id="counter" class="badge">{{ groups|length }}</span> + </h4> + </div> + <div id="collapse-groups" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-groups"> + <div class="panel-body"> + <div class="col-sm-10"> + <p class="help">Plotters have at least one + <strong>input</strong> and one <strong>output</strong>. All + plotter endpoints are organized in <strong>groups</strong>. + Groups are used by the platform to indicate which inputs and + outputs are synchronized together. The first group is + automatically synchronized with the channel defined by the + block in which the plotter is deployed.</p> + {% for group in groups %} + <div class="panel panel-default"> + {% if group.name %} + <div class="panel-heading"> + <h4 class="panel-title">Group: {{ group.name }}</h4> + </div> + {% else %} + <div class="panel-heading"> + <h4 class="panel-title">Unnamed group</h4> + </div> + {% endif %} + <table class="table table-responsive table-condensed table-hover"> + <thead> + <tr> + <th>Endpoint Name</th> + <th>Data Format</th> + <th>Nature</th> + </tr> + </thead> + <tbody> + {% for name, value in group.inputs.items %} + <tr> + <td>{{ name }}</td> + <td>{% with value.type|split_fullname as parts %}<a href="{% url "dataformats:view" parts.0 parts.1 parts.2 %}" target="_blank" title="Click to see dataformat in a new window">{{ value.type }}</a>{% endwith %}</td> + <td><span class="text-success"><i class="fa fa-arrow-right"></i> Input</span></td> + </tr> + {% endfor %} + {% for name, value in group.outputs.items %} + <tr class="active"> + <td>{{ name }}</td> + <td>{% with value.type|split_fullname as parts %}<a href="{% url "dataformats:view" parts.0 parts.1 parts.2 %}" target="_blank" title="Click to see dataformat in a new window">{{ value.type }}</a>{% endwith %}</td> + <td><span class="text-primary"><i class="fa fa-arrow-left"></i> Output</span></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endfor %} + </div> + </div> + </div> + </div> + {% endif %}{# groups #} + + {% if object.result_dataformat %} + {% with object.json_result as result %} + <div id="results-panel" class="panel panel-default"> + <div class="panel-heading" role="tab" id="heading-results"> + <h4 class="panel-title"> + <a class="collapsed" role="button" data-toggle="collapse" data-parent="#results-panel" href="#collapse-results" aria-expanded="false" aria-controls="collapse-results"> + Results + </a> <span id="counter" class="badge">{{ result|length }}</span> + </h4> + </div> + <div id="collapse-results" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-results"> + <div class="panel-body"> + <div class="col-sm-10"> + <p class="help">Analyzers may produce any number of + <strong>results</strong>. Once experiments using this analyzer + are done, you may display the results or filter experiments + using criteria based on them.</p> + <table class="table table-responsive table-condensed table-hover"> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + </tr> + </thead> + <tbody> + {% for name, value in result.items %} + <tr> + <td>{% if "+" in name %}{{ name|cut:"+" }} <i title="This result is displayed by default on search results" class="fa fa-eye"></i>{% else %}{{ name }}{% endif %}</td> + <td>{% if "/" in value %}{% with value|split_fullname as parts %}<a target="_blank" title="Click to view this dataformat in another window" href="{% url "dataformats:view" parts.0 parts.1 parts.2 %}">{{ value }}</a>{% endwith %}{% else %}{{ value }}{% endif %}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> + {% endwith %} + {% endif %}{# results #} + + {% if parameters %} + <div id="parameters-panel" class="panel panel-default"> + <div class="panel-heading" role="tab" id="heading-parameters"> + <h4 class="panel-title"> + <a class="collapsed" role="button" data-toggle="collapse" data-parent="#parameters-panel" href="#collapse-parameters" aria-expanded="false" aria-controls="collapse-parameters"> + Parameters + </a> <span id="counter" class="badge">{{ parameters|length }}</span> + </h4> + </div> + <div id="collapse-parameters" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-parameters"> + <div class="panel-body"> + <div class="col-sm-10"> + <p class="help">Parameters allow users to change the + configuration of an plotter when scheduling an experiment</p> + <table class="table table-responsive table-condensed table-hover"> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <th>Type</th> + <th>Default</th> + </tr> + </thead> + <tbody> + {% for key, value in parameters.items %} + <tr> + <td>{{ key }}</td> + <td class="help">{{ value.description }}</td> + <td>{{ value.type }}</td> + <td>{{ value.default }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> + {% endif %}{# parameters #} + + {% if uses %} + <div id="uses-panel" class="panel panel-default"> + <div class="panel-heading" role="tab" id="heading-uses"> + <h4 class="panel-title"> + <a class="collapsed" role="button" data-toggle="collapse" data-parent="#uses-panel" href="#collapse-uses" aria-expanded="false" aria-controls="collapse-uses"> + Libraries + </a> <span id="counter" class="badge">{{ uses|length }}</span> + </h4> + </div> + <div id="collapse-uses" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-uses"> + <div class="panel-body"> + <div class="col-sm-10"> + <p class="help">Plotters may use functions and classes + declared in libraries. Here you can see the libraries and + import names used by this library. You <strong>don't</strong> + need to import the library manually on your code, the platform + will do it for you. Just use the object as it has been imported + with the selected named. For example, if you choose to import a + library using the name <code>lib</code>, then access function + <code>f</code> within your code like <code>lib.f()</code>. + </p> + <table class="table table-responsive table-condensed table-hover"> + <thead> + <tr> + <th>Library</th> + <th>Import as</th> + </tr> + </thead> + <tbody> + {% for key, value in uses.items %} + <tr> + <td>{% with value|split_fullname as parts %}<a target="_blank" title="Click to view this library in a new window" data-toggle="tooltip" data-placement="top" href="{% url "libraries:view" parts.0 parts.1 parts.2 %}">{{ value }}</a>{% endwith %}</td> + <td>{{ key }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> + {% endif %}{# libraries "uses" #} + </div> + {% endif %}{# groups, uses or parameters #} + {% endwith %}{# groups, uses or parameters #} + {% endwith %}{# groups, uses or parameters #} + {% endwith %}{# groups, uses or parameters #} + + {% if open_source %} + <textarea class="form-control" id="code-display">{{ object.source_code_file.read }}</textarea> + <p class="help-block">{{ texts.code|safe }}</p> + {% else %} + <div class="alert alert-warning"> + <i class="fa fa-warning"></i> This plotter is only usable to you. Its code was <strong>not</strong> shared. + </div> + {% endif %} + </div> + + {% if owner and object.modifiable %} + <div class="col-sm-2 action-buttons"> + <a id="btn-edit-object" class="btn btn-primary btn-sm pull-right" href="{% url 'plotters:edit' object.author.username object.name object.version %}"><i class="fa fa-edit fa-lg"></i> Edit</a> + </div> + {% endif %} + +</div> + +{% if open_source %} +<script type="text/javascript"> +$(document).ready(function() { + var code_textarea = $('textarea#code-display'); + var code_editor = CodeMirror.fromTextArea(code_textarea[0], { + mode: '{{ object.get_language_display|lower }}', + readOnly: true, + }); + code_editor.refresh(); +}); +</script> +{% endif %} diff --git a/beat/web/plotters/templates/plotters/panels/table.html b/beat/web/plotters/templates/plotters/panels/table.html new file mode 100644 index 000000000..270291653 --- /dev/null +++ b/beat/web/plotters/templates/plotters/panels/table.html @@ -0,0 +1,117 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} +{% load ui_tags %} +{% load plotter_tags %} + +<div class="row filter"> + + <div class="col-sm-10 vertical-center"> + + {% if objects %} + <div id="filters" class="form-inline"> + + <div class="form-group form-group-sm"> + <div class="input-group input-group-sm"> + <span class="input-group-addon" id="basic-addon1"><i class="fa fa-search"></i></span> + <input type="text" tabindex="2" id="text-filter" class="form-control" placeholder="Filter rows..." aria-describedby="basic-addon1"> + </div> + </div> + + <div class="form-group form-group-sm"> + <label for="privacy-filter" class="control-label">Privacy:</label> + <select id="privacy-filter" class="form-control"> + <option selected>All</option> + <option>Public</option> + <option>Shared</option> + <option>Private</option> + </select> + </div> + + </div> + {% endif %} + + <!-- Notice there can be no div space if vertical-center is used --> + </div><div class="col-sm-2 vertical-center"> + + </div><!-- col --> + +</div><!-- row --> + +<div class="row"> + + <div class="col-sm-12"> + + {% if objects %} + <div class="scrollable table-responsive"> + <table id="{{ panel_id }}" class="table table-hover table-condensed object-list plotter-list"> + <thead> + <tr> + <th class="privacy"></th> + <th class="date">Updated</th> + <th>Name</th> + {% if request.user.is_staff %} + <th class="actions">Actions</th> + {% endif %} + </tr> + </thead> + <tbody> + {% for obj in objects %} + <tr> + <td class="privacy"> + <a title="{{ obj.get_sharing_display }}" data-toggle="tooltip" data-placement="top"> + {% if obj.get_sharing_display == 'Private' %} + <i class="fa fa-user fa-2x"></i> + {% elif obj.get_sharing_display == 'Shared' %} + <i class="fa fa-users fa-2x"></i> + {% else %} + <i class="fa fa-globe fa-2x"></i> + {% endif %} + </a> + </td> + <td class="date">{{ obj.creation_date|date }}</td> + <td class="name"><a href="{{ obj.get_absolute_url }}" data-toggle="tooltip" data-placement="top" title="Click to view">{{ obj.fullname }}{% if obj.short_description %} <span class='help'>({{ obj.short_description }})</span>{% endif %}</a></td> + {% if request.user.is_staff %} + <td class="actions"> + {% plotter_actions obj True %} + </td> + {% endif %} + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + + </div><!-- col --> + +</div><!-- row --> +{% if not objects %} +<div class="row"> + + <div class="col-sm-12 not-found"> + No plotter found + </div> + +</div><!-- row --> +{% endif %} <!-- if not objects --> + +{% filter_script panel_id "text-filter" "privacy-filter" %} diff --git a/beat/web/plotters/templates/plotters/view.html b/beat/web/plotters/templates/plotters/view.html new file mode 100644 index 000000000..d570e41f7 --- /dev/null +++ b/beat/web/plotters/templates/plotters/view.html @@ -0,0 +1,149 @@ +{% extends "base.html" %} +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +{% load plotter_tags %} +{% load experiment_tags %} +{% load report_tags %} +{% load ui_tags %} +{% load fingerprint %} + +{% block title %}{{ block.super }} - {{ plotter.fullname }}{% endblock %} + + +{% block stylesheets %} +{{ block.super }} +<link rel="stylesheet" href="{% fingerprint "experiments/css/style.css" %}" type="text/css" media="screen" /> +<link rel="stylesheet" href="{% fingerprint "chosen-bootstrap/chosen.bootstrap.min.css" %}" type="text/css" media="screen" /> +<link rel="stylesheet" href="{% fingerprint "ui/css/rst.css" %}" type="text/css" media="screen" /> +{% code_editor_css %} +{% endblock %} + + +{% block scripts %} +{{ block.super }} +<script src="{% fingerprint "chosen/chosen.jquery.min.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "experiments/js/utils.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "ui/js/history.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "raphael/raphael-min.js" %}" type="text/javascript" charset="utf-8"></script> +{% code_editor_scripts "python,rst" %} +{% endblock %} + + +{% block content %} + +<div class="row"> + + <div class="col-sm-9 vertical-center" onmouseover="expand_breadcrumb(this, 9, 3);" onmouseout="reset_breadcrumb(this, 9, 3);"> + {% contribution_breadcrumb_plotter plotter %} + <!-- Note: keep no space between divs here! --> + </div><div class="col-sm-3 vertical-center"> + {% plotter_actions plotter False %} + </div> + +</div> + + +{% if plotter.short_description %} +<div class="row"> + <div class="col-sm-12"> + <p class="help-block"><i class="fa fa-file-text"></i> {{ plotter.short_description }}</p> + </div> +</div> +{% endif %} + + +{% if plotter.fork_of %} +<div class="row"> + <div class="col-sm-12"> + <p class="help-block"><i class="fa fa-code-fork"></i> Forked from <a href="{{ plotter.fork_of.get_absolute_url }}">{{ plotter.fork_of.fullname }}</a></p> + </div> +</div> +{% endif %} + +{% visible_reports plotter as reports %} +<div class="row"> + <div class="col-sm-12"> + + {# Navigation Tabs #} + <ul id="object-tabs" class="nav nav-tabs" role="tablist"> + <li role="presentation" class="active"><a href="#viewer" role="tab" data-toggle="tab" aria-controls="viewer">Plotter</a></li> + <li role="presentation"><a {% if not plotter.description %}title="No documentation available" {% endif %}href="#doc" role="tab" data-toggle="tab" aria-controls="doc">Documentation{% if not plotter.description %} <i class="fa fa-warning"></i>{% endif %}</a></li> + {% if owner %} + <li role="presentation"><a href="#sharing" role="tab" data-toggle="tab" aria-controls="sharing">Sharing</a></li> + {% endif %} + <li role="presentation"><a href="#reports" role="tab" data-toggle="tab" aria-controls="reports">Reports<span class="badge">{{ reports.count }}</span></a></li> + <li role="presentation"><a href="#history" role="tab" data-toggle="tab" aria-controls="history">History</a></li> + </ul> + + {# Navigation Panes #} + <div class="tab-content"> + <div role="tabpanel" class="tab-pane active" id="viewer"> + {% plotter_editor plotter %} + </div> + <div role="tabpanel" class="tab-pane" id="doc"> + {% doc_editor plotter 'api_plotters:object' %} + </div> + {% if owner %} + <div role="tabpanel" class="tab-pane" id="sharing"> + {% plotter_sharing plotter %} + </div> + {% endif %} + <div role="tabpanel" class="tab-pane" id="reports"> + + {% if reports.count %} + <h4>Reports</h4> + {% report_table reports.all owner "report-list" %} + {% else %} + No reports are using this plotter. + {% endif %} + + </div> + <div role="tabpanel" class="tab-pane" id="history"> + {% history 'plotters' plotter 'history' 400 %} + </div> + <div role="tabpanel" class="tab-pane" id="compatibility"> + <div class="col-sm-5"> + {% with plotter.environments as execinfo %} + {% if not execinfo %} + This plotter was never executed. + {% else %} + <ul class="list-group"> + {% for key, value in execinfo %}<li class="list-group-item"><a title="Click to view" data-toggle="tooltip" data-placement="top" href="{% url 'backend:view-environment' key.name key.version %}">{{ key.fullname }}</a> <span class="badge">{{ value }}</span></li>{% endfor %} + </ul> + <p class="help">This table shows the number of times this plotter + has been <b>successfuly</b> run using the given environment. Note + this does not provide sufficient information to evaluate if the + plotter will run when submitted to different conditions.</p> + {% endif %} + {% endwith %} + </div> + </div> + </div> + + </div> +</div> + +<script type="text/javascript"> +$(document).ready(function(){manage_tabs('ul#object-tabs');}) +</script> +{% endblock %} diff --git a/beat/web/plotters/templatetags/__init__.py b/beat/web/plotters/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/beat/web/plotters/templatetags/plotter_tags.py b/beat/web/plotters/templatetags/plotter_tags.py new file mode 100644 index 000000000..1cad189f0 --- /dev/null +++ b/beat/web/plotters/templatetags/plotter_tags.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # +# or FITNESS FOR A PARTICULAR PURPOSE. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +import random + +from django import template +from django.conf import settings + +from ..models import Plotter + +register = template.Library() + + +@register.inclusion_tag('plotters/panels/table.html', takes_context=True) +def plotter_table(context, objects, owner, id): + '''Composes a plotter list table + + This panel primarily exists for user's plotter list page. + + Parameters: + + objects (iterable): An iterable containing plotter objects + owner (bool): A flag indicating if the list is being created for the + owner of his personal list page on the user-microsite or not. + id: The HTML id to set on the generated table. This is handy for the + filter functionality normally available on list pages. + + ''' + + return dict( + request=context['request'], + objects=objects, + owner=owner, + panel_id=id, + ) + + +@register.inclusion_tag('plotters/panels/actions.html', takes_context=True) +def plotter_actions(context, object, display_count): + '''Composes the action buttons for a particular plotter + + This panel primarily exists for showing action buttons for a given + plotter taking into consideration it is being displayed for a given user. + + Parameters: + + object (plotter): The plotter object concerned for which the + buttons will be drawn. + display_count (bool): If the set of buttons should include one with the + number of experiments using this plotter. + + ''' + return dict( + request=context['request'], + object=object, + display_count=display_count, + ) + + +@register.inclusion_tag('plotters/panels/sharing.html', takes_context=True) +def plotter_sharing(context, obj): + '''Composes the current sharing properties and a form to change them + + Parameters: + + obj (plotter): The plotter object concerned for which the + sharing panel will be drawn + + ''' + return { + 'request': context['request'], + 'object': obj, + 'owner': context['request'].user == obj.author, + 'users': context['users'], + 'teams': context['teams'], + } + + +@register.inclusion_tag('plotters/panels/viewer.html', takes_context=True) +def plotter_viewer(context, obj, xp, id): + '''Composes a canvas with the plotter (no further JS setup is + required) + + Parameters: + + obj (plotter): The plotter object concerned for which the + panel will be drawn. + xp (Experiment): The experiment to project on the top of the plotter + components. If not given, just draw the plotter. + id (str): The id of the canvas element that will be created + + ''' + return { + 'request': context['request'], + 'object': obj, + 'xp': xp, + 'panel_id': id, + 'URL_PREFIX': settings.URL_PREFIX, + } + + +@register.assignment_tag(takes_context=True) +def random_plotter(context): + '''Returns a random plotter that is visible to the current user''' + + candidates = Plotter.objects.for_user(context['request'].user, True) + return candidates[random.randint(0, candidates.count()-1)] + + + +@register.assignment_tag(takes_context=True) +def visible_plotters(context): + '''Calculates the visible plotters for a given user''' + + return Plotter.objects.for_user(context['request'].user, True) + + +@register.assignment_tag(takes_context=True) +def visible_reports(context, object): + '''Calculates the visible experiments for a given plotter and requestor''' + + return object.reports.for_user(context['request'].user, True) + + +#---------------------------------------------------------------- + + +@register.inclusion_tag('plotters/panels/editor.html', takes_context=True) +def plotter_editor(context, obj): + request = context['request'] + return { + 'owner': request.user == obj.author, + 'object': obj, + 'open_source': obj.open_source(request.user), + } + + + + +@register.inclusion_tag('plotters/dialogs/import_settings.html') +def plotter_import_settings(id): + return { 'dialog_id': id, + } diff --git a/beat/web/plotters/tests.py b/beat/web/plotters/tests.py new file mode 100644 index 000000000..a1ba9245f --- /dev/null +++ b/beat/web/plotters/tests.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # +# or FITNESS FOR A PARTICULAR PURPOSE. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +import os +import shutil +import simplejson as json + +from django.test import TestCase +from django.contrib.auth.models import User +from django.conf import settings +from django.core.urlresolvers import reverse + +from .models import Plotter, PlotterParameter + +from ..common.models import Shareable +from ..common.testutils import BaseTestCase + +from rest_framework import status +from rest_framework.test import APITestCase + + +#---------------------------------------------------------- + + +class PlotterParameterTestCase(APITestCase): + + def setUp(self): + self.tearDown() + + # Create the users + self.password = '1234' + + self.johndoe = User.objects.create_user('johndoe', 'johndoe@test.org', self.password) + self.jackdoe = User.objects.create_user('jackdoe', 'jackdoe@test.org', self.password) + self.plot = User.objects.create_user('plot', 'plotdoe@test.org', self.password) + + + + def tearDown(self): + for path in [settings.TOOLCHAINS_ROOT, settings.EXPERIMENTS_ROOT, + settings.DATAFORMATS_ROOT, settings.ALGORITHMS_ROOT, + settings.CACHE_ROOT]: + if os.path.exists(path): + shutil.rmtree(path) + + +#---------------------------------------------------------- + + +class PlotterParameterCreationTestCase(PlotterParameterTestCase): + + def setUp(self): + super(PlotterParameterCreationTestCase, self).setUp() + + self.url = reverse('api_plotters:view', kwargs={'author_name': self.johndoe.username}) + + self.data = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter1',\ + 'short_description':'some description',\ + 'description':'some longer description'\ + } + + def test_anonymous_user(self): + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content, '{"detail":"Authentication credentials were not provided."}') + + + def test_logged_in_user(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + def test_logged_in_user_existing_plotterparameter_name(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(json.loads(response.content)['non_field_errors'][0], 'This plotterparameter name already exists on this account') + + +#---------------------------------------------------------- + + +class PlotterParameterListTestCase(PlotterParameterTestCase): + + def setUp(self): + super(PlotterParameterListTestCase, self).setUp() + + self.url = reverse('api_plotters:view', kwargs={'author_name': self.johndoe.username}) + + self.data = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter1',\ + 'short_description':'some description',\ + 'description':'some longer description'\ + } + + self.data2 = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter2',\ + 'short_description':'some description2',\ + 'description':'some longer description2'\ + } + + def test_anonymous_user(self): + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_logged_in_user_no_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(json.loads(response.content)), 0) + + def test_logged_in_user_single_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(json.loads(response.content)), 1) + + def test_logged_in_user_multiple_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.post(self.url, self.data2, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data2['name']) + + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(json.loads(response.content)), 2) + + +#---------------------------------------------------------- + + +class PlotterParameterRetrievalTestCase(PlotterParameterTestCase): + + def setUp(self): + super(PlotterParameterRetrievalTestCase, self).setUp() + + self.url = reverse('api_plotters:view', kwargs={'author_name': self.johndoe.username}) + + self.url_single_plotterparameter = reverse('api_plotters:view', kwargs={ + 'author_name': self.johndoe.username, + 'object_name': 'plotterparameter1', + 'version': 1, + }) + + self.url_single_plotterparameter2 = reverse('api_plotters:view', kwargs={ + 'author_name': self.johndoe.username, + 'object_name': 'plotterparameter2', + 'version': 1, + }) + + self.data = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter1',\ + 'short_description':'some description',\ + 'description':'some longer description'\ + } + + self.data2 = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter2',\ + 'short_description':'some description2',\ + 'description':'some longer description2'\ + } + + def test_anonymous_user(self): + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_logged_in_user_no_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_logged_in_user_single_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data['name']+'/1') + + def test_logged_in_user_multiple_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.post(self.url, self.data2, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data2['name']) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data['name']+'/1') + + response = self.client.get(self.url_single_plotterparameter2, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data2['name']+'/1') + + +#---------------------------------------------------------- + + +class PlotterParameterUpdateTestCase(PlotterParameterTestCase): + + def setUp(self): + super(PlotterParameterUpdateTestCase, self).setUp() + + self.url = reverse('api_plotters:view', kwargs={'author_name': self.johndoe.username}) + + self.url_single_plotterparameter = reverse('api_plotters:view', kwargs={ + 'author_name': self.johndoe.username, + 'object_name': 'plotterparameter1', + 'version': 1, + }) + + self.url_single_plotterparameter2 = reverse('api_plotters:view', kwargs={ + 'author_name': self.johndoe.username, + 'object_name': 'plotterparameter2', + 'version': 1, + }) + + self.data = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter1',\ + 'short_description':'some description',\ + 'description':'some longer description'\ + } + + self.data2 = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter2',\ + 'short_description':'some description2',\ + 'description':'some longer description2'\ + } + + def test_anonymous_user(self): + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_logged_in_user_no_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_logged_in_user_single_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data['name']+'/1') + + def test_logged_in_user_multiple_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.post(self.url, self.data2, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data2['name']) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data['name']+'/1') + + response = self.client.get(self.url_single_plotterparameter2, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data2['name']+'/1') + + +#---------------------------------------------------------- + + +class PlotterParameterDeletionTestCase(PlotterParameterTestCase): + + def setUp(self): + super(PlotterParameterDeletionTestCase, self).setUp() + + self.url = reverse('api_plotters:view', kwargs={'author_name': self.johndoe.username}) + + self.url_single_plotterparameter = reverse('api_plotters:view', kwargs={ + 'author_name': self.johndoe.username, + 'object_name': 'plotterparameter1', + 'version': 1, + }) + + self.url_single_plotterparameter2 = reverse('api_plotters:view', kwargs={ + 'author_name': self.johndoe.username, + 'object_name': 'plotterparameter2', + 'version': 1, + }) + + self.data = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter1',\ + 'short_description':'some description',\ + 'description':'some longer description'\ + } + + self.data2 = {\ + #'author':self.johndoe,\ + 'name':'plotterparameter2',\ + 'short_description':'some description2',\ + 'description':'some longer description2'\ + } + + def test_anonymous_user(self): + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_logged_in_user_no_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_logged_in_user_single_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data['name']+'/1') + + def test_logged_in_user_multiple_plotterparameter(self): + self.client.login(username=self.johndoe.username, password=self.password) + + response = self.client.post(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data['name']) + + response = self.client.post(self.url, self.data2, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(json.loads(response.content)['name'], self.data2['name']) + + response = self.client.get(self.url_single_plotterparameter, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data['name']+'/1') + + response = self.client.get(self.url_single_plotterparameter2, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(json.loads(response.content)['name'], self.johndoe.username+'/'+self.data2['name']+'/1') diff --git a/beat/web/plotters/urls.py b/beat/web/plotters/urls.py index aaf3c4b9b..2c59ef458 100644 --- a/beat/web/plotters/urls.py +++ b/beat/web/plotters/urls.py @@ -42,4 +42,34 @@ urlpatterns = [ name='plot', ), + url( + r'^(?P<author>\w+)/(?P<name>[-\w]+)/(?P<version>\d+)/$', + views.view, + name='view', + ), + + url( + r'^(?P<author_name>\w+)/$', + views.list_plotters, + name='plotter-list', + ), + + url( + r'^$', + views.list_plotters_public, + name='plotter-public-list', + ), + + url( + r'^(?P<author>\w+)/(?P<name>[-\w]+)/$', + views.view, + name='plotter-view-latest', + ), + + url( + r'^(?P<author_name>\w+)/plotterparameter/$', + views.list_plotterparameters, + name='plotterparameter-list', + ), + ] diff --git a/beat/web/plotters/views.py b/beat/web/plotters/views.py index 178583093..91478b14e 100644 --- a/beat/web/plotters/views.py +++ b/beat/web/plotters/views.py @@ -37,13 +37,17 @@ import simplejson from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest from django.conf import settings +from django.shortcuts import render_to_response from django.shortcuts import get_object_or_404 +from django.template import RequestContext, Context +from django.contrib.auth.models import User from ..experiments.models import Experiment, Result from ..dataformats.models import DataFormat from ..reports.models import Report from .models import Plotter, PlotterParameter, DefaultPlotter +from ..team.models import Team import beat.core.experiment import beat.core.toolchain @@ -294,12 +298,95 @@ def plot(request): if do_b64_encode: fig = base64.b64encode(fig) return HttpResponse(fig, content_type=final_parameters['content_type']) +#---------------------------------------------------------- + + +def view(request, author, name, version=None): + """Shows the algorithm. The Web API is used to retrieve the details about + the algorithm and check the accessibility. + """ + + # Retrieves the algorithm + if version: + plotter = get_object_or_404( + Plotter, + author__username__iexact=author, + name__iexact=name, + version=int(version), + ) + else: + plotter = Plotter.objects.filter(author__username__iexact=author, + name__iexact=name).order_by('-version') + if not plotter: + raise Http404() + else: + plotter = plotter[0] + + (has_access, _, __) = plotter.accessibility_for(request.user) + + if not has_access: raise Http404() + + owner = (request.user == plotter.author) + + # Users the object can be shared with + users = User.objects.exclude(username__in=settings.ACCOUNTS_TO_EXCLUDE_FROM_TEAMS).order_by('username') + + # Render the page + return render_to_response('plotters/view.html', + { + 'plotter': plotter, + 'owner': owner, + 'users': users, + 'teams': Team.objects.for_user(request.user, True) + }, + context_instance=RequestContext(request)) + #---------------------------------------------------------- def list_plotters(request, author_name): '''List all accessible plotters to the request user''' + # check that the user exists on the system + author = get_object_or_404(User, username=author_name) + + objects=Plotter.objects.from_author_and_public(request.user,author_name) + + owner=(request.user==author) + + return render_to_response('plotters/list.html', + dict( + objects=objects, + author=author, + owner=owner, + ), + context_instance=RequestContext(request), + ) + +#---------------------------------------------------------- + + +def list_plotters_public(request): + '''List all accessible plotters to the request user''' + + # orders so that objects with latest information are displayed first + objects=Plotter.objects.public().order_by('-creation_date') + + return render_to_response('plotters/list.html', + dict( + objects=objects, + author=request.user, #anonymous + owner=False, + ), + context_instance=RequestContext(request), + ) + + +#---------------------------------------------------------- + + +def list_plotterparameters(request, author_name): + '''List all accessible plotters to the request user''' return render_to_response('plotters/list.html', dict(objects=Plotter.objects.from_author_and_public(request.user, diff --git a/beat/web/reports/templatetags/report_tags.py b/beat/web/reports/templatetags/report_tags.py index d6a3f1b81..5ae90d30f 100644 --- a/beat/web/reports/templatetags/report_tags.py +++ b/beat/web/reports/templatetags/report_tags.py @@ -30,6 +30,7 @@ from django import template from django.conf import settings from ..models import Report +from django.db.models.functions import Coalesce register = template.Library() @@ -53,6 +54,7 @@ def report_table(context, objects, owner, id): filter functionality normally available on list pages. ''' + objects = objects.annotate(updated=Coalesce('publication_date', 'creation_date',)).order_by('-updated') return dict( request=context['request'], diff --git a/beat/web/ui/templates/ui/contribution_breadcrumb_plotter.html b/beat/web/ui/templates/ui/contribution_breadcrumb_plotter.html new file mode 100644 index 000000000..0463a4795 --- /dev/null +++ b/beat/web/ui/templates/ui/contribution_breadcrumb_plotter.html @@ -0,0 +1,47 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} +<h3> +<ol class="breadcrumb"> + <li> + {% with p=object.get_sharing_display %} + {% if p == 'Private' %} + <i data-toggle="tooltip" data-placement="bottom" title="{{ p }}" class="fa fa-user fa-lg"></i> + {% elif p == 'Public' %} + <i data-toggle="tooltip" data-placement="bottom" title="{{ p }}" class="fa fa-globe fa-lg"></i> + {% elif p == 'Usable' %} + <i data-toggle="tooltip" data-placement="bottom" title="Usable, but no readable" class="fa fa-cog fa-lg"></i> + {% else %}<!-- shared specifically --> + <i data-toggle="tooltip" data-placement="bottom" title="Shared with some" class="fa fa-users fa-lg"></i> + {% endif %} + {% endwith %} + + {% if not request.user.is_anonymous %} + <a data-toggle="tooltip" data-placement="bottom" title="View all your {{ name_plural }}" href="{% url listurl request.user.username %}">{{ name_plural }}</a> + {% else %} + <a data-toggle="tooltip" data-placement="bottom" title="View all public {{ name_plural }}" href="{% url public_listurl %}">{{ name_plural }}</a> + {% endif %} + </li> + <li><a data-toggle="tooltip" data-placement="bottom" title="View all {{ object.author.username }}'s {{ name_plural }}" href="{% url listurl object.author.username %}">{{ object.author.username }}</a></li> + <li><a title="View the latest version" data-toggle="tooltip" data-placement="bottom" href="{% url viewurl object.author.username object.name %}">{{ object.name }}</a></li> + <li>{{ object.version }}</li> +</ol> +</h3> diff --git a/beat/web/ui/templatetags/ui_tags.py b/beat/web/ui/templatetags/ui_tags.py index 3b4b711ad..0570c26ab 100644 --- a/beat/web/ui/templatetags/ui_tags.py +++ b/beat/web/ui/templatetags/ui_tags.py @@ -58,6 +58,9 @@ def navbar(context): (False, ''), ('databases', 'databases:list'), ('environments', 'backend:list-environments'), + (False, ''), + ('plotters', 'plotters:plotter-public-list'), + #('plotterparameters', 'plotters:plotterparameter-public-list'), ], 'user_urls': [ ('experiments', 'experiments:list', True), @@ -74,6 +77,9 @@ def navbar(context): (False, '', False), ('databases', 'databases:list', False), ('environments', 'backend:list-environments', False), + (False, '', False), + ('plotters', 'plotters:plotter-list', True), + #('plotterparameters', 'plotters:plotterparameter-list', True), ], } @@ -97,6 +103,22 @@ def contribution_breadcrumb(context, obj): #-------------------------------------------------- +@register.inclusion_tag('ui/contribution_breadcrumb_plotter.html', takes_context=True) +def contribution_breadcrumb_plotter(context, obj): + name_plural = obj.get_verbose_name_plural() + return { + 'request': context['request'], + 'object': obj, + 'name_plural': name_plural, + 'listurl': name_plural + ':plotter-list', + 'public_listurl': name_plural + ':plotter-public-list', + 'viewurl': name_plural + ':plotter-view-latest', + } + + +#-------------------------------------------------- + + @register.inclusion_tag('ui/filter_script.html') def filter_script(panel_id, text_filter_id, privacy_filter_id): return dict( @@ -139,6 +161,7 @@ def list_tabs(context, user, tab): ('reports', 'reports:' + name), ('searches', 'search:' + name), ('teams', 'teams:' + name), + ('plotters', 'plotters:plotter-' + name), ]), system_tabs=OrderedDict([ ('databases', 'databases:list'), -- GitLab