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