# docs/source/conf.py

# -*- coding: utf-8 -*-
#
# chris ultron backend documentation build configuration file, created by
# sphinx-quickstart on Thu Jun 16 09:24:54 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.doctest',
    'sphinx.ext.intersphinx',
    'sphinx.ext.todo',
    'sphinx.ext.coverage',
    'sphinx.ext.mathjax',
    'sphinx.ext.ifconfig',
    'sphinx.ext.viewcode',
    'sphinx.ext.githubpages',
    'sphinxcontrib.httpdomain'
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The encoding of source files.
#
# source_encoding = 'utf-8-sig'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = u'chris ultron backend'
copyright = u'2016, FNNDSC Devs'
author = u'FNNDSC Devs'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.0.1'
# The full version, including alpha/beta/rc tags.
release = u'0.0.1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None

# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []

# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None

# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True

# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True

# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []

# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True


# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []

# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'chris ultron backend v0.0.1'

# A shorter title for the navigation bar.  Default is the same as html_title.
#
# html_short_title = None

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None

# The name of an image file (relative to this directory) to use as a favicon of
# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []

# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None

# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True

# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}

# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}

# If false, no module index is generated.
#
# html_domain_indices = True

# If false, no index is generated.
#
# html_use_index = True

# If true, the index is split into individual pages for each letter.
#
# html_split_index = False

# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True

# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True

# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it.  The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''

# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None

# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'

# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}

# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'

# Output file base name for HTML help builder.
htmlhelp_basename = 'chrisultronbackenddoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',

     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',

     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',

     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'chrisultronbackend.tex', u'chris ultron backend Documentation',
     u'FNNDSC Devs', 'manual'),
]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None

# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False

# If true, show page references after internal links.
#
# latex_show_pagerefs = False

# If true, show URL addresses after external links.
#
# latex_show_urls = False

# Documents to append as an appendix to all manuals.
#
# latex_appendices = []

# If false, no module index is generated.
#
# latex_domain_indices = True


# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'chrisultronbackend', u'chris ultron backend Documentation',
     [author], 1)
]

# If true, show URL addresses after external links.
#
# man_show_urls = False


# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'chrisultronbackend', u'chris ultron backend Documentation',
     author, 'chrisultronbackend', 'One line description of project.',
     'Miscellaneous'),
]

# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []

# If false, no module index is generated.
#
# texinfo_domain_indices = True

# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'

# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False


# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# The basename for the epub file. It defaults to the project name.
# epub_basename = project

# The HTML theme for the epub output. Since the default themes are not
# optimized for small screen space, using the same theme for HTML and epub
# output is usually not wise. This defaults to 'epub', a theme designed to save
# visual space.
#
# epub_theme = 'epub'

# The language of the text. It defaults to the language option
# or 'en' if the language is not set.
#
# epub_language = ''

# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A tuple containing the cover image and cover page html template filenames.
#
# epub_cover = ()

# A sequence of (type, uri, title) tuples for the guide element of content.opf.
#
# epub_guide = ()

# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#
# epub_pre_files = []

# HTML files that should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#
# epub_post_files = []

# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']

# The depth of the table of contents in toc.ncx.
#
# epub_tocdepth = 3

# Allow duplicate toc entries.
#
# epub_tocdup = True

# Choose between 'default' and 'includehidden'.
#
# epub_tocscope = 'default'

# Fix unsupported image types using the Pillow.
#
# epub_fix_images = False

# Scale large images.
#
# epub_max_image_width = 0

# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# epub_show_urls = 'inline'

# If false, no index is generated.
#
# epub_use_index = True


# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}


# utils/scripts/plugin_search.py

#!/usr/bin/env python3

import  logging
logging.disable(logging.CRITICAL)

import  os
import  sys
import  json
import  socket
import  requests

import  pudb

import  pfmisc

# pfstorage local dependencies
from    pfmisc._colors      import  Colors
from    pfmisc.debug        import  debug
from    pfmisc.C_snode      import  *
from    pfstate             import  S

from    argparse            import RawTextHelpFormatter
from    argparse            import ArgumentParser


str_desc        = """

    NAME

        plugin_search.py

    PREREQUISITES

        The python `pfmisc` and `pfstate` modules:

            pip install -U pfmisc pfstate

    SYNOPSIS

        plugin_search.py        --for <someField(s)>                    \\
                                --using <someTemplateDesc>              \\
                                [--version]                             \\
                                [--man]                                 \\
                                [--jsonReturn]                          \\
                                [--syslogPrepend]                       \\
                                [--verbosity <level>]

    ARGS

        --for <someField(s)>
        A comma separated list of fields to return from the plugin search.
        Results are typically printed in a "table", with each row containing
        the value for each of the `for` fields.

        Typical examples:

            --for name
            --for name,id

        --using <someTemplateDesc>
        The search condition/pattern to apply. This is typically some
        value relevant to the plugin description as referenced with the
        CUBE API.

        The <someTemplateDesc> can be a compound string that is comma
        separated. For instance 'type=ds,name=pfdo' will search for all
        plugins that have 'pfdo' in their name and 'ds' as their type.

        Typical examples:

            --using name=pl-fshack
            --using name=dicom,type=ds

        [--version]
        Print the version and exit.

        [--jsonReturn]
        If specified, print the full JSON return from the API call and
        various class methods that apply processing to the API call.

        [--syslogPrepend]
        If specified, prepend pseudo (colorized) syslog info to output
        print calls.

        [--verbosity <level>]
        Apply a verbosity to the output.


    DESCRIPTION

        `plugin_search.py` provides for a simple CLI mechanism to search
        a CUBE instance plugin space for various detail. Given, for example,
        a plugin name, return the plugin ID, or given a plugin type, return
        all names.

        The primary purpose of this script is a helper component in creating
        autonomous feeds to a ChRIS instance from some stand-alone script,
        although it is quite well suited as a CLI mechanism to query for
        information on various plugins in a CUBE instantiation.

    EXAMPLES

    * List by name all the DS plugins in a CUBE instance:

        $ python plugin_search.py --for name --using type=ds
        (type=ds)       name    pl-pfdo_med2img
        (type=ds)       name    pl-pfdo_mgz2img
        (type=ds)       name    pl-mgz2lut_report
        (type=ds)       name    pl-z2labelmap
        (type=ds)       name    pl-freesurfer_pp
        (type=ds)       name    pl-fastsurfer_inference
        (type=ds)       name    pl-fshack
        (type=ds)       name    pl-mpcs
        (type=ds)       name    pl-pfdicom_tagsub
        (type=ds)       name    pl-pfdicom_tagextract

    * List by name all DS plugins that also have 'dicom' in their name:

        $ python plugin_search.py --for name --using type=ds,name=dicom
        (type=ds,name=dicom)    name    pl-pfdicom_tagsub
        (type=ds,name=dicom)    name    pl-pfdicom_tagextract

    * Find the ID of the `pl-fshack` plugin:

        $ python plugin_search.py --for id --using name=pl-fshack
        (name=pl-fshack)        id      10

    * List all the plugins (assuming they all start with `pl`) by
      name, id, and plugin type:

        $ python plugin_search.py --for name,id,type --using name=pl
        (name=pl)      name pl-pfdo_med2img                  id 17     type ds
        (name=pl)      name pl-pfdo_mgz2img                  id 16     type ds
        (name=pl)      name pl-mgz2lut_report                id 15     type ds
        (name=pl)      name pl-z2labelmap                    id 13     type ds
        (name=pl)      name pl-freesurfer_pp                 id 12     type ds
        (name=pl)      name pl-fastsurfer_inference          id 11     type ds
        (name=pl)      name pl-fshack                        id 10     type ds
        (name=pl)      name pl-mpcs                          id 9      type ds
        (name=pl)      name pl-pfdicom_tagsub                id 8      type ds
        (name=pl)      name pl-pfdicom_tagextract            id 7      type ds
        (name=pl)      name pl-s3push                        id 6      type ds
        (name=pl)      name pl-dsdircopy                     id 5      type ds
        (name=pl)      name pl-s3retrieve                    id 3      type ds
        (name=pl)      name pl-simpledsapp                   id 2      type ds
        (name=pl)      name pl-mri10yr06mo01da_normal        id 14     type fs
        (name=pl)      name pl-dircopy                       id 4      type fs
        (name=pl)      name pl-simplefsapp                   id 1      type fs

"""

# Determine the hostIP
str_defIP   = [l for l in (
                [ip for ip in socket.gethostbyname_ex(socket.gethostname())[2]
                if not ip.startswith("127.")][:1],
                    [[(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close())
                for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) if l][0][0]


str_version     = "1.0.0"
str_name        = "plugin_search.py"
parser          = ArgumentParser(
                    description     = str_desc,
                    formatter_class = RawTextHelpFormatter
)

parser.add_argument(
    '--version',
    help    = 'if specified, print verion',
    action  = 'store_true',
    dest    = 'b_version',
    default = False,
)
parser.add_argument(
    '--man', '-x',
    help    = 'if specified, show help and exit',
    action  = 'store_true',
    dest    = 'b_man',
    default = False,
)
parser.add_argument(
    '--syslogPrepend',
    help    = 'if specified, prepend syslog info to output',
    action  = 'store_true',
    dest    = 'b_syslog',
    default = False,
)
parser.add_argument(
    '--jsonReturn',
    help    = 'if specified, return results as JSON',
    action  = 'store_true',
    dest    = 'b_json',
    default = False,
)
parser.add_argument(
    '--for',
    help    = 'property for which to search',
    action  = 'store',
    dest    = 'str_for',
    default = '',
)
parser.add_argument(
    '--using',
    help    = 'search template in <key>=<value>[,..] form',
    action  = 'store',
    dest    = 'str_using',
    default = '',
)
parser.add_argument(
    '--verbosity',
    help    = 'the system verbosity',
    action  = 'store',
    dest    = 'verbosity',
    default = 1,
)

args        = parser.parse_args()

class D(S):
    """
    A derived 'pfstate' class that keeps system state.

    See https://github.com/FNNDSC/pfstate for more information.
    """

    def __init__(self, *args, **kwargs):
        """
        An object to hold some generic/global-ish system state, in C_snode
        trees.
        """
        self.state_create(
        {
            "CUBE":
            {
                "protocol": "http",
                "port":     "8000",
                "address":  "%HOSTIP",
                "user":     "chris",
                "password": "chris1234",
            },
            "self":
            {
                'httpProxy':
                {
                    'use':          False,
                    'httpSpec':     ''
                }
            }
        },
        *args, **kwargs)

class PluginSearch(object):
    """
    A class that interacts with CUBE via the collection+json API
    and is specialized to perform searches on the plugin space.
    """

    def S(self, *args):
        """
        set/get components of the state object
        """
        if len(args) == 1:
            return self.state.T.cat(args[0])
        else:
            self.state.T.touch(args[0], args[1])

    def __init__(self, *args, **kwargs):
        """
        Class constructor.
        """
        self.d_args     = vars(*args)
        self.state      = D(
            version     = str_version,
            name        = str_name,
            desc        = str_desc,
            args        = vars(*args)
        )
        self.dp         = pfmisc.debug(
            verbosity   = int(self.d_args['verbosity']),
            within      = str_name,
            syslog      = self.d_args['b_syslog']
        )
        # Check the IP in the state structure and optionally update
        IP = self.S('/CUBE/address')
        if IP == "%HOSTIP":
            self.S('/CUBE/address', str_defIP)

    def search_templatize(self):
        """
        Parse the CLI '--using <template>' and return a dictionary
        of parameters to search.

        The <template> value is of form:

            name_exact=<name>,version=<version>,[<key>=<value>,...]

        and is returned as

            {
                'name_exact':   <name>,
                'version':      <version>[,
                <key>:          <value>]
            }

        """
        b_status    : bool      = False
        d_params    : dict      = {}
        str_message : str       = ''
        paramCount  : int       = 0

        if 'str_using' in self.d_args.keys():
            if len(self.d_args['str_using']):
                l_using     = self.d_args['str_using'].split(',')
                for param in l_using:
                    l_keyVal    = param.split('=')
                    d_params[l_keyVal[0]]   = l_keyVal[1]
                    paramCount += 1
                    b_status    = True
                str_message = '%d parameters templatized' % paramCount
            else:
                str_message = "'--using' value is zero length"
        else:
            str_message     = "'--using' not in CLI args"

        return {
            'status':   b_status,
            'message':  str_message,
            'params':   d_params
        }

    def search_CUBEAPIcall(self):
        """

        This method implements the actual search logic.

        """
        d_resp              : dict  = {}
        d_templatize        : dict  = self.search_templatize()
        b_status            : bool  = False

        if d_templatize['status']:
            d_params    = d_templatize['params']
            d_headers           : dict = {
                'Accept':   'application/vnd.collection+json'
            }
            d_resp              : dict = {}
            str_dataServiceAddr : str  = "%s://%s:%s" % (
                                    self.S('/CUBE/protocol'),
                                    self.S('/CUBE/address'),
                                    self.S('/CUBE/port')
                                )
            str_dataServiceURL  : str  = 'api/v1/plugins/search/?limit=100'
            str_user            : str  = self.S('/CUBE/user')
            str_passwd          : str  = self.S('/CUBE/password')
            str_URL             : str  = '%s/%s' % (
                                    str_dataServiceAddr,
                                    str_dataServiceURL
                                )
            try:
                resp = requests.get(
                                    str_URL,
                                    params  = d_params,
                                    auth    = (str_user, str_passwd),
                                    timeout = 30,
                                    headers = d_headers
                        )
                b_status    = True
            except (requests.exceptions.Timeout,
                    requests.exceptions.RequestException) as e:
                logging.error(str(e))
                raise
            d_resp = resp.json()
        return {
            'status':   b_status,
            'response': d_resp
        }

    def search_desiredReturnFind(self, d_search):
        """
        For a given search response from CUBE, return the
        specific value sought.
        """
        b_status        :   bool    = False
        l_thistarget    :   list    = []
        l_target        :   list    = []

        if d_search['status']:
            if d_search['response']['collection']['total']:
                for d_hit in d_search['response']['collection']['items']:
                    l_data  :   list = d_hit['data']
                    l_thistarget     = []
                    for str_desired in self.d_args['str_for'].split(','):
                        l_hit   :   list = list(
                                    filter(
                                        lambda info: info['name'] == str_desired, l_data
                                        )
                                    )
                        if len(l_hit):
                            l_thistarget.append(l_hit[0])
                            b_status    = True
                    l_target.append(l_thistarget)

        return {
            'status':   b_status,
            'search':   d_search,
            'target':   l_target
        }

    def do(self):
        """
        Main entry point to this class.
        """
        d_result    = self.search_desiredReturnFind(self.search_CUBEAPIcall())
        return d_result

def main(*args):
    """
    The main method of the script, when called directly from the CLI
    """
    retCode     : int   = 1
    search      = PluginSearch(args[0])

    if search.d_args['b_man']:
        print(str_desc)
        sys.exit(0)

    if search.d_args['b_version']:
        print(str_version)
        sys.exit(0)

    d_result    = search.do()

    if search.d_args['b_json']:
        search.dp.qprint(json.dumps(d_result, indent = 4))
        if len(d_result['target']):
            retCode = 0
    else:
        for target in d_result['target']:
            search.dp.qprint('(%s)' % search.d_args['str_using'], end='')
            for hit in target:
                search.dp.qprint('%10s %-30s' % (hit['name'], hit['value']),
                                 end='', syslog=False)
            search.dp.qprint('')
            retCode = 0

    sys.exit(retCode)

if __name__ == "__main__":
    main(args)


# chris_backend/collectionjson/migrations/0001_initial.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-06-10 19:48
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Dummy',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100)),
            ],
        ),
        migrations.CreateModel(
            name='Idiot',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100)),
            ],
        ),
        migrations.CreateModel(
            name='Moron',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100)),
            ],
        ),
        migrations.CreateModel(
            name='Simple',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100)),
            ],
        ),
        migrations.AddField(
            model_name='dummy',
            name='idiots',
            field=models.ManyToManyField(to='collectionjson.Idiot'),
        ),
        migrations.AddField(
            model_name='dummy',
            name='moron',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collectionjson.Moron'),
        ),
    ]


# chris_backend/collectionjson/tests/models.py


from django.db import models
from django_filters.rest_framework import FilterSet

# Create your models here.
class Moron(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        app_label = "collectionjson"
        

class MoronFilter(FilterSet):
    
    class Meta:
        model = Moron
        fields = ['name']

        
class Idiot(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        app_label = "collectionjson"


class Dummy(models.Model):
    name = models.CharField(max_length=100)
    moron = models.ForeignKey('Moron', on_delete=models.CASCADE)
    idiots = models.ManyToManyField('Idiot')

    class Meta:
        app_label = "collectionjson"
        

class Simple(models.Model):
    name = models.CharField(max_length=100)
    
    class Meta:
        app_label = "collectionjson"





# chris_backend/collectionjson/tests/serializers.py


from rest_framework.serializers import HyperlinkedIdentityField
from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer

from collectionjson.fields import ItemLinkField
from .models import Dummy, Idiot, Moron, Simple

        
class MoronHyperlinkedModelSerializer(HyperlinkedModelSerializer):
    class Meta(object):
        model = Moron
        fields = ('url', 'name')
        

class IdiotHyperlinkedModelSerializer(HyperlinkedModelSerializer):
    class Meta(object):
        model = Idiot
        fields = ('url', 'name')


class DummyHyperlinkedModelSerializer(HyperlinkedModelSerializer):
    other_stuff = ItemLinkField('get_other_link')
    empty = ItemLinkField('get_empty_link')
    some_link = HyperlinkedIdentityField(view_name='moron-detail')

    class Meta(object):
        model = Dummy
        fields = ('url', 'name', 'moron', 'idiots', 'other_stuff', 'some_link', 'empty')

    def get_other_link(self, obj):
        return 'http://other-stuff.com/'

    def get_empty_link(self, obj):
        return None
    

class SimpleModelSerializer(ModelSerializer):

    class Meta(object):
        model = Simple
        fields = ('name', )


# chris_backend/collectionjson/tests/test_parsers.py


import json
import logging

from django.conf.urls import url, include
from django.test.utils import override_settings
from django.test import TestCase

from rest_framework import status
from rest_framework.routers import DefaultRouter

from . import views


@override_settings(ROOT_URLCONF='collectionjson.tests.test_parsers')
class SimplePOSTTest(TestCase):
    endpoint = ''

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)
        self.content_type='application/vnd.collection+json'

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class TestCollectionJsonParser(SimplePOSTTest):
    endpoint = '/rest-api/moron/'

    def setUp(self):
        super(TestCollectionJsonParser, self).setUp()

    def test_create_success(self):
        post = json.dumps({"template": {"data": [{"name": "name", "value": "Bob"}]}})
        response = self.client.post(self.endpoint, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data["name"], "Bob")

    def test_create_failure_invalid_template_not_dict(self):
        post = json.dumps([{"template": {"data": [{"name": "name", "value": "Bob"}]}}])
        response = self.client.post(self.endpoint, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_create_failure_invalid_template_missing_field(self):
        post = json.dumps({"templlte": {"data": [{"name": "name", "value": "Bob"}]}})
        response = self.client.post(self.endpoint, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)


router = DefaultRouter()
router.register('moron', views.MoronModelViewSet)
urlpatterns = [
    url(r'^rest-api/', include(router.urls)),
]



# chris_backend/collectionjson/tests/test_renderers.py


import logging
import json

from django.conf.urls import url, include
from django.test.utils import override_settings
from django.test import TestCase

from collection_json import Collection
from rest_framework import status
from rest_framework.routers import DefaultRouter

from .models import Dummy, Idiot, Moron, Simple
from . import views


def create_models():
    #import pdb; pdb.set_trace()
    bob = Moron.objects.create(name='Bob LawLaw')
    dummy = Dummy.objects.create(name='Yolo McSwaggerson', moron=bob)
    dummy.idiots.add(Idiot.objects.create(name='frick'))
    dummy.idiots.add(Idiot.objects.create(name='frack'))   

@override_settings(ROOT_URLCONF='collectionjson.tests.test_renderers')
class SimpleGetTest(TestCase):
    endpoint = ''

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)
        self.response = self.client.get(self.endpoint)
        content = json.loads(self.response.content.decode('utf8'))
        self.total = content['collection'].pop('total', None)  # remove the non-standard 'total' property
        self.collection = Collection.from_json(json.dumps(content))

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class TestCollectionJsonRenderer(SimpleGetTest):
    endpoint = '/rest-api/dummy/'

    def setUp(self):
        create_models()
        super(TestCollectionJsonRenderer, self).setUp()

    def test_it_has_the_right_response_code(self):
        self.assertEqual(self.response.status_code, status.HTTP_200_OK)

    def test_it_has_the_right_content_type(self):
        content_type = self.response['Content-Type']
        self.assertEqual(content_type, 'application/vnd.collection+json')

    def test_it_has_the_version_number(self):
        self.assertEqual(self.collection.version, '1.0')

    def test_it_has_an_href(self):
        href = self.collection.href
        self.assertEqual(href, 'http://testserver/rest-api/dummy/')

    def test_it_has_the_correct_total(self):
        self.assertEqual(self.total, 1)

    def get_dummy(self):
        return self.collection.items[0]

    def test_the_dummy_item_has_an_href(self):
        href = self.get_dummy().href
        self.assertTrue(href.startswith('http://testserver/rest-api/dummy/'))

    def test_the_dummy_item_contains_name(self):
        name = self.get_dummy().data.find('name')[0].value
        self.assertEqual(name, 'Yolo McSwaggerson')

    def get_dummy_link(self, rel):
        links = self.get_dummy()['links']
        return next(x for x in links if x['rel'] == rel)

    def test_the_dummy_item_links_to_child_elements(self):
        href = self.get_dummy().links.find(rel='moron')[0].href
        self.assertTrue(href.startswith('http://testserver/rest-api/moron/'))

    def test_link_fields_are_rendered_as_links(self):
        href = self.get_dummy().links.find(rel='other_stuff')[0].href
        self.assertEqual(href, 'http://other-stuff.com/')

    def test_empty_link_fields_are_not_rendered_as_links(self):
        links = self.get_dummy().links.find(rel='empty_link')
        self.assertEqual(len(links), 0)

    def test_attribute_links_are_rendered_as_links(self):
        href = self.get_dummy().links.find(rel='some_link')[0].href
        self.assertEqual(href, 'http://testserver/rest-api/moron/1/')

    def test_many_to_many_relationships_are_rendered_as_links(self):
        idiots = self.get_dummy().links.find(rel='idiots')
        self.assertTrue(idiots[0].href.startswith('http://testserver/rest-api/idiot/'))
        self.assertTrue(idiots[1].href.startswith('http://testserver/rest-api/idiot/'))


class TestNoSerializerViews(SimpleGetTest):
    endpoint = '/rest-api/no-serializer/'

    def setUp(self):
        create_models()
        super(TestNoSerializerViews, self).setUp()

    def test_views_without_a_serializer_work(self):
        value = self.collection.items[0].data.find('foo')[0].value
        self.assertEqual(value, '1')


class TestNormalModels(SimpleGetTest):
    endpoint = '/rest-api/normal-model/'

    def setUp(self):
        Simple.objects.create(name='Foobar Baz')
        super(TestNormalModels, self).setUp()

    def test_items_dont_have_a_href(self):
        href_count = len(self.collection.items[0].find(name='href'))
        self.assertEqual(href_count, 0)


class TestCollectionJsonRendererPagination(SimpleGetTest):
    endpoint = '/rest-api/paginated/'

    def test_paginated_views_display_data(self):
        foo = self.collection.items[0].find(name='foo')[0]
        self.assertEqual(foo.value, 1)

    def test_paginated_views_display_next(self):
        next_link = self.collection.links.find(rel='next')[0]
        self.assertEqual(next_link.href, 'http://test.com/colleciton/next')

    def test_paginated_views_display_previous(self):
        next_link = self.collection.links.find(rel='previous')[0]
        self.assertEqual(next_link.href, 'http://test.com/colleciton/previous')


class TestCollectionJsonRendererPaginationWithNone(SimpleGetTest):
    endpoint = '/rest-api/none-paginated/'

    def test_paginated_view_does_not_display_next(self):
        self.assertEqual(len(self.collection.links.find(rel='next')), 0)

    def test_paginated_view_does_not_display_previous(self):
        self.assertEqual(len(self.collection.links.find(rel='previous')), 0)


class TestErrorHandling(SimpleGetTest):
    endpoint = '/rest-api/parse-error/'

    def test_errors_are_reported(self):
        self.assertEqual(self.collection.error.message, 'lol nice one')


class TestUrlRewrite(SimpleGetTest):
    endpoint = '/rest-api/url-rewrite/'

    def test_the_href_url_can_be_rewritten(self):
        rewritten_url = "http://rewritten.com/rest-api/url-rewrite/"
        self.assertEqual(self.collection.href, rewritten_url)

@override_settings(ROOT_URLCONF='collectionjson.tests.test_renderers')
class TestEmpty(TestCase):

    def test_empty_content_works(self):
        response = self.client.get('/rest-api/empty/')
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(response.content.decode('utf8'), '')


router = DefaultRouter()
router.register('dummy', views.DummyReadOnlyModelViewSet)
router.register('moron', views.MoronReadOnlyModelViewSet)
router.register('idiot', views.IdiotReadOnlyModelViewSet)
router.register('normal-model', views.SimpleViewSet)
urlpatterns = [
    url(r'^rest-api/', include(router.urls)),
    url(r'^rest-api/no-serializer/', views.NoSerializerView.as_view()),
    url(r'^rest-api/paginated/', views.PaginatedDataView.as_view()),
    url(r'^rest-api/none-paginated/', views.NonePaginatedDataView.as_view()),
    url(r'^rest-api/parse-error/', views.ParseErrorView.as_view()),
    url(r'^rest-api/url-rewrite/', views.UrlRewriteView.as_view()),
    url(r'^rest-api/empty/', views.EmptyView.as_view()),
]



# chris_backend/collectionjson/tests/test_services.py


import logging

from django.conf.urls import url, include
from django.test.utils import override_settings
from django.test import TestCase

from rest_framework.routers import DefaultRouter

from collectionjson import services

from .models import Moron
from . import views


@override_settings(ROOT_URLCONF='collectionjson.tests.test_parsers')

class SimpleGetTest(TestCase):
    endpoint = ''

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)
        self.response = self.client.get(self.endpoint)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class FunctionTests(SimpleGetTest):
    """
    Test top-level functions in the services module
    """

    endpoint = '/rest-api/moron/'
    
    def setUp(self):

       # create two objects
       Moron.objects.get_or_create(name='bob')
       Moron.objects.get_or_create(name='paul')

       super(FunctionTests, self).setUp()

    def test_get_list_response(self):
        """
        Test whether services.get_list_response() returns a response with two objects
        """
        # get response
        response = self.response
        # get the view and queryset
        view = response.renderer_context['view']
        queryset = view.get_queryset()
        list_response = services.get_list_response(view, queryset)
        # set required response attributes
        list_response.accepted_renderer = response.accepted_renderer
        list_response.accepted_media_type = response.accepted_media_type
        list_response.renderer_context = response.renderer_context
        self.assertContains(list_response, "bob")
        self.assertContains(list_response, "paul")

    def test_append_collection_links(self):
        """
        Test whether services.append_collection_links() appends collection links 
        to its response argument
        """
        response = self.response
        links = {"morons": self.endpoint}
        response = services.append_collection_links(response, links)
        self.assertEqual(response.data['collection_links'],
                         {'morons': self.endpoint})

    def test_append_collection_template(self):
        """
        Test whether services.append_collection_template() appends a collection+json 
        template to its response argument
        """
        response = self.response
        template_data = {"name": ""} 
        response = services.append_collection_template(response, template_data)
        self.assertEqual(response.data['template'],
                         {'data': [{'name': 'name', 'value': ''}]})

    def test_append_collection_querylist(self):
        """
        Test whether services.append_collection_querylist() appends a collection+json
        queries template list to it response argument
        """
        response = self.response
        query_urls = [self.endpoint]
        response = services.append_collection_querylist(response, query_urls)
        self.assertEqual(response.data['queries'],
                         [{'href': query_urls[0], 'rel': 'search',
                           "data": [{"name": "name", "value": ""}]}])


router = DefaultRouter()
router.register('moron', views.MoronModelViewSet)
urlpatterns = [
    url(r'^rest-api/', include(router.urls)),
]



# chris_backend/collectionjson/tests/views.py

from six.moves.urllib.parse import urljoin

from django_filters.rest_framework import DjangoFilterBackend

from rest_framework.views import APIView
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from rest_framework.response import Response
from rest_framework import status
from rest_framework.exceptions import ParseError

from collectionjson.renderers import CollectionJsonRenderer
from collectionjson.parsers import CollectionJsonParser

from .models import Dummy, Idiot, Moron, MoronFilter, Simple
from .serializers import MoronHyperlinkedModelSerializer, IdiotHyperlinkedModelSerializer
from .serializers import DummyHyperlinkedModelSerializer, SimpleModelSerializer


class MoronModelViewSet(ModelViewSet):
    renderer_classes = (CollectionJsonRenderer, )
    parser_classes = (CollectionJsonParser, )
    queryset = Moron.objects.all()
    serializer_class = MoronHyperlinkedModelSerializer
    filter_backends = (DjangoFilterBackend,)
    filterset_class = MoronFilter
    

class MoronReadOnlyModelViewSet(ReadOnlyModelViewSet):
    renderer_classes = (CollectionJsonRenderer, )
    queryset = Moron.objects.all()
    serializer_class = MoronHyperlinkedModelSerializer


class IdiotReadOnlyModelViewSet(ReadOnlyModelViewSet):
    renderer_classes = (CollectionJsonRenderer, )
    queryset = Idiot.objects.all()
    serializer_class = IdiotHyperlinkedModelSerializer


class DummyReadOnlyModelViewSet(ReadOnlyModelViewSet):
    renderer_classes = (CollectionJsonRenderer, )
    queryset = Dummy.objects.all()
    serializer_class = DummyHyperlinkedModelSerializer
    

class SimpleViewSet(ReadOnlyModelViewSet):
    renderer_classes = (CollectionJsonRenderer, )
    queryset = Simple.objects.all()
    serializer_class = SimpleModelSerializer


class NoSerializerView(APIView):
    renderer_classes = (CollectionJsonRenderer, )

    def get(self, request):
        return Response({'foo': '1'})


class PaginatedDataView(APIView):
    renderer_classes = (CollectionJsonRenderer, )

    def get(self, request):
        return Response({
            'next': 'http://test.com/colleciton/next',
            'previous': 'http://test.com/colleciton/previous',
            'results': [{'foo': 1}],
        })
    

class NonePaginatedDataView(APIView):
    renderer_classes = (CollectionJsonRenderer, )

    def get(self, request):
        return Response({
            'next': None,
            'previous': None,
            'results': [{'foo': 1}],
        })    


class ParseErrorView(APIView):
    renderer_classes = (CollectionJsonRenderer, )

    def get(self, request):
        raise ParseError('lol nice one')


class UrlRewriteRenderer(CollectionJsonRenderer):
    def get_href(self, request):
        return urljoin('http://rewritten.com', request.path)
    

class UrlRewriteView(APIView):
    renderer_classes = (UrlRewriteRenderer, )

    def get(self, request):
        return Response({'foo': 'bar'})
    

class EmptyView(APIView):
    renderer_classes = (CollectionJsonRenderer, )

    def get(self, request):
        return Response(status=status.HTTP_204_NO_CONTENT)


# chris_backend/config/settings/common.py

# -*- coding: utf-8 -*-
"""
Django settings for chris_backend project.

Generated by 'django-admin startproject' using Django 2.2.12.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_filters',
    'django_celery_beat',
    'mod_wsgi.server',
    'rest_framework',
    'rest_framework.authtoken',
    'corsheaders',
    'collectionjson',
    'core',
    'feeds',
    'plugins',
    'plugininstances',
    'pipelines',
    'pipelineinstances',
    'uploadedfiles',
    'pacsfiles',
    'servicefiles',
    'filebrowser',
    'users'
]

# Pagination
REST_FRAMEWORK = {
    'PAGE_SIZE': 10,
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'DEFAULT_RENDERER_CLASSES': (
        'collectionjson.renderers.CollectionJsonRenderer',
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ),
    'DEFAULT_PARSER_CLASSES': (
        'collectionjson.parsers.CollectionJsonParser',
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_FILTER_BACKENDS': (
        'django_filters.rest_framework.DjangoFilterBackend',
    )
}

MIDDLEWARE = [
    'core.middleware.ResponseMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'America/New_York'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'


# chris_backend/config/settings/local.py

# -*- coding: utf-8 -*-
"""
Local settings

- Run in Debug mode
- Use console backend for emails
- Add Django Debug Toolbar
- Add django-extensions as app
"""

from .common import *  # noqa
from core.swiftmanager import SwiftManager

# Normally you should not import ANYTHING from Django directly
# into your settings, but ImproperlyConfigured is an exception.
from django.core.exceptions import ImproperlyConfigured

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'w1kxu^l=@pnsf!5piqz6!!5kdcdpo79y6jebbp+2244yjm*#+k'

# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/2.2/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ['*']

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

# LOGGING CONFIGURATION
# See http://docs.djangoproject.com/en/2.2/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '[%(asctime)s] [%(levelname)s]'
                      '[%(name)s][%(filename)s:%(lineno)d %(funcName)s] %(message)s'
        },
        'simple': {
            'format': '[%(asctime)s] [%(levelname)s]'
                      '[%(module)s %(process)d %(thread)d] %(message)s'
        },
    },
    'handlers': {
        'console_verbose': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'verbose',
        },
        'console_simple': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/tmp/debug.log',
            'formatter': 'simple'
        }
    },
    'loggers': {
        '': {  # root logger
            'level': 'INFO',
            'handlers': ['console_simple'],
        },
    }
}

for app in ['collectionjson', 'core', 'feeds', 'plugins', 'plugininstances', 'pipelines',
            'pipelineinstances', 'uploadedfiles', 'pacsfiles', 'servicefiles', 'users',
            'filebrowser']:
    LOGGING['loggers'][app] = {
            'level': 'DEBUG',
            'handlers': ['console_verbose', 'file'],
            'propagate': False  # required to avoid double logging with root logger
        }

# Swift service settings
DEFAULT_FILE_STORAGE = 'swift.storage.SwiftStorage'
SWIFT_AUTH_URL = 'http://swift_service:8080/auth/v1.0'
SWIFT_USERNAME = 'chris:chris1234'
SWIFT_KEY = 'testing'
SWIFT_CONTAINER_NAME = 'users'
SWIFT_CONNECTION_PARAMS = {'user': SWIFT_USERNAME,
                           'key': SWIFT_KEY,
                           'authurl': SWIFT_AUTH_URL}
try:
    SwiftManager(SWIFT_CONTAINER_NAME, SWIFT_CONNECTION_PARAMS).create_container()
except Exception as e:
    raise ImproperlyConfigured(str(e))

# ChRIS store settings
CHRIS_STORE_URL = 'http://chris-store.local:8010/api/v1/'

# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES['default']['NAME'] = 'chris_dev'
DATABASES['default']['USER'] = 'chris'
DATABASES['default']['PASSWORD'] = 'Chris1234'
DATABASES['default']['TEST'] = {'NAME': 'test_chris_dev'}
DATABASES['default']['HOST'] = 'chris_dev_db'
DATABASES['default']['PORT'] = '5432'

# Mail settings
# ------------------------------------------------------------------------------
EMAIL_HOST = 'localhost'
EMAIL_PORT = 1025
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# django-debug-toolbar
# ------------------------------------------------------------------------------
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INSTALLED_APPS += ['debug_toolbar']

INTERNAL_IPS = ['127.0.0.1',]

DEBUG_TOOLBAR_CONFIG = {
    'DISABLE_PANELS': [
        'debug_toolbar.panels.redirects.RedirectsPanel',
    ],
    'SHOW_TEMPLATE_CONTEXT': True,
}

# django-extensions
# ------------------------------------------------------------------------------
INSTALLED_APPS += ['django_extensions']

# TESTING
# ------------------------------------------------------------------------------
TEST_RUNNER = 'django.test.runner.DiscoverRunner'

COMPUTE_RESOURCE_URL = 'http://pfcon.remote:30005/api/v1/'

# corsheaders
# ------------------------------------------------------------------------------
CORS_ORIGIN_ALLOW_ALL = True
CORS_EXPOSE_HEADERS = ['Allow', 'Content-Type', 'Content-Length']


# Celery settings

#CELERY_BROKER_URL = 'amqp://guest:guest@localhost'
CELERY_BROKER_URL = 'amqp://queue:5672'

#: Only add pickle to this list if your broker is secured
#: from unwanted access (see userguide/security.html)
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'

# Worker settings
# messages to prefetch at a time multiplied by the number of concurrent processes
# default is 4 (four messages for each process)
CELERYD_PREFETCH_MULTIPLIER = 2


# chris_backend/config/settings/production.py

# -*- coding: utf-8 -*-
"""
Production Configurations

"""

from .common import *  # noqa
from environs import Env, EnvValidationError
from core.swiftmanager import SwiftManager

# Normally you should not import ANYTHING from Django directly
# into your settings, but ImproperlyConfigured is an exception.
from django.core.exceptions import ImproperlyConfigured


# Environment variables-based secrets
env = Env()
env.read_env()  # also read .env file, if it exists


def get_secret(setting, secret_type=env):
    """Get the secret variable or return explicit exception."""
    try:
        return secret_type(setting)
    except EnvValidationError as e:
        raise ImproperlyConfigured(str(e))


# SECRET CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/2.2/ref/settings/#secret-key
# Raises ImproperlyConfigured exception if DJANGO_SECRET_KEY not set
SECRET_KEY = get_secret('DJANGO_SECRET_KEY')


# SITE CONFIGURATION
# ------------------------------------------------------------------------------
# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/2.2/ref/settings/#allowed-hosts
ALLOWED_HOSTS = get_secret('DJANGO_ALLOWED_HOSTS', env.list)
# END SITE CONFIGURATION


# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
# Raises ImproperlyConfigured exception if DATABASE_URL not set
DATABASES['default']['NAME'] = get_secret('POSTGRES_DB')
DATABASES['default']['USER'] = get_secret('POSTGRES_USER')
DATABASES['default']['PASSWORD'] = get_secret('POSTGRES_PASSWORD')
DATABASES['default']['HOST'] = get_secret('DATABASE_HOST')
DATABASES['default']['PORT'] = get_secret('DATABASE_PORT')


# SWIFT SERVICE CONFIGURATION
# ------------------------------------------------------------------------------
DEFAULT_FILE_STORAGE = 'swift.storage.SwiftStorage'
SWIFT_AUTH_URL = get_secret('SWIFT_AUTH_URL')
SWIFT_USERNAME = get_secret('SWIFT_USERNAME')
SWIFT_KEY = get_secret('SWIFT_KEY')
SWIFT_CONTAINER_NAME = get_secret('SWIFT_CONTAINER_NAME')
SWIFT_CONNECTION_PARAMS = {'user': SWIFT_USERNAME,
                           'key': SWIFT_KEY,
                           'authurl': SWIFT_AUTH_URL}
try:
    SwiftManager(SWIFT_CONTAINER_NAME, SWIFT_CONNECTION_PARAMS).create_container()
except Exception as e:
    raise ImproperlyConfigured(str(e))


# CHRIS STORE SERVICE CONFIGURATION
CHRIS_STORE_URL = get_secret('CHRIS_STORE_URL')


# LOGGING CONFIGURATION
# See http://docs.djangoproject.com/en/2.2/topics/logging for
# more details on how to customize your logging configuration.
ADMINS = [('FNNDSC Developers', 'dev@babymri.org')]
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'console': {
            'format': '[%(levelname)s][%(module)s %(process)d %(thread)d] %(message)s'
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'console',
        },
    },
    'loggers': {
        '': {  # root logger
            'level': 'INFO',
            'handlers': ['console'],
        }
    }
}


# STATIC FILES (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_ROOT = get_secret('STATIC_ROOT')


# CORSHEADERS
# ------------------------------------------------------------------------------
CORS_ORIGIN_ALLOW_ALL = get_secret('DJANGO_CORS_ORIGIN_ALLOW_ALL', env.bool)
CORS_ORIGIN_WHITELIST = get_secret('DJANGO_CORS_ORIGIN_WHITELIST', env.list)


# Celery settings

CELERY_BROKER_URL = get_secret('CELERY_BROKER_URL')

#: Only add pickle to this list if your broker is secured
#: from unwanted access (see userguide/security.html)
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'

# Worker settings
# messages to prefetch at a time multiplied by the number of concurrent processes
# default is 4 (four messages for each process)
CELERYD_PREFETCH_MULTIPLIER = 2


# REVERSE PROXY
# ------------------------------------------------------------------------------
SECURE_PROXY_SSL_HEADER = get_secret('DJANGO_SECURE_PROXY_SSL_HEADER', env.list)
SECURE_PROXY_SSL_HEADER = tuple(SECURE_PROXY_SSL_HEADER) if SECURE_PROXY_SSL_HEADER else None
USE_X_FORWARDED_HOST = get_secret('DJANGO_USE_X_FORWARDED_HOST', env.bool)


# chris_backend/core/migrations/0001_initial.py

# Generated by Django 2.2.12 on 2020-09-22 21:53

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='ChrisInstance',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(default='ChRIS instance', max_length=100)),
                ('uuid', models.UUIDField(default=uuid.uuid4)),
                ('description', models.CharField(blank=True, max_length=600)),
            ],
            options={
                'verbose_name': 'ChRIS instance',
                'verbose_name_plural': 'ChRIS instance',
            },
        ),
    ]


# chris_backend/core/migrations/0002_chrisinstance_job_id_prefix.py

# Generated by Django 2.2.12 on 2021-01-20 23:46

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('core', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='chrisinstance',
            name='job_id_prefix',
            field=models.CharField(blank=True, default='chris-jid-', max_length=100),
        ),
    ]


# chris_backend/feeds/migrations/0001_initial.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-06 18:20
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('plugins', '0002_auto_20160706_1417'),
    ]

    operations = [
        migrations.CreateModel(
            name='Comment',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('title', models.CharField(blank=True, default='', max_length=100)),
                ('content', models.TextField(blank=True, default='')),
            ],
            options={
                'ordering': ('creation_date',),
            },
        ),
        migrations.CreateModel(
            name='Feed',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('modification_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(blank=True, default='', max_length=100)),
                ('owner', models.ManyToManyField(related_name='feed', to=settings.AUTH_USER_MODEL)),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feed', to='plugins.PluginInstance')),
            ],
            options={
                'ordering': ('creation_date',),
            },
        ),
        migrations.CreateModel(
            name='FeedFile',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('fname', models.FileField(max_length=200, upload_to='')),
                ('feed', models.ManyToManyField(related_name='files', to='feeds.Feed')),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='file', to='plugins.PluginInstance')),
            ],
        ),
        migrations.CreateModel(
            name='Note',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('modification_date', models.DateTimeField(auto_now_add=True)),
                ('title', models.CharField(blank=True, default='', max_length=100)),
                ('content', models.TextField(blank=True, default='')),
                ('feed', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='note', to='feeds.Feed')),
            ],
            options={
                'ordering': ('creation_date',),
            },
        ),
        migrations.CreateModel(
            name='Tag',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(blank=True, default='', max_length=100)),
                ('color', models.CharField(max_length=20)),
                ('feed', models.ManyToManyField(related_name='tags', to='feeds.Feed')),
                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
            ],
        ),
        migrations.AddField(
            model_name='comment',
            name='feed',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='feeds.Feed'),
        ),
        migrations.AddField(
            model_name='comment',
            name='owner',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
        ),
    ]


# chris_backend/feeds/migrations/0002_auto_20160712_1438.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-12 18:38
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='feed',
            name='plugin_inst',
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feed', to='plugins.PluginInstance'),
        ),
    ]


# chris_backend/feeds/migrations/0003_auto_20161031_1124.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-31 15:24
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0002_auto_20160712_1438'),
    ]

    operations = [
        migrations.AlterField(
            model_name='feedfile',
            name='fname',
            field=models.FileField(max_length=2048, upload_to=''),
        ),
    ]


# chris_backend/feeds/migrations/0004_auto_20161201_1121.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-12-01 16:21
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0003_auto_20161031_1124'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='feedfile',
            name='feed',
        ),
        migrations.AddField(
            model_name='feedfile',
            name='feed',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='feeds.Feed'),
            preserve_default=False,
        ),
    ]


# chris_backend/feeds/migrations/0005_auto_20170301_1312.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2017-03-01 18:12
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0004_auto_20161201_1121'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='feed',
            options={'ordering': ('-creation_date',)},
        ),
    ]


# chris_backend/feeds/migrations/0006_auto_20181005_1702.py

# Generated by Django 2.0.7 on 2018-10-05 17:02

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0005_auto_20170301_1312'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='comment',
            options={'ordering': ('-creation_date',)},
        ),
        migrations.AlterModelOptions(
            name='feedfile',
            options={'ordering': ('fname',)},
        ),
        migrations.AlterModelOptions(
            name='note',
            options={},
        ),
        migrations.AlterField(
            model_name='feedfile',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='plugins.PluginInstance'),
        ),
    ]


# chris_backend/feeds/migrations/0007_auto_20181101_2034.py

# Generated by Django 2.0.7 on 2018-11-01 20:34

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0006_auto_20181005_1702'),
    ]

    operations = [
        migrations.CreateModel(
            name='Tagging',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.Feed')),
            ],
        ),
        migrations.RemoveField(
            model_name='tag',
            name='feed',
        ),
        migrations.AddField(
            model_name='tagging',
            name='tag',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.Tag'),
        ),
        migrations.AddField(
            model_name='tag',
            name='feeds',
            field=models.ManyToManyField(related_name='tags', through='feeds.Tagging', to='feeds.Feed'),
        ),
        migrations.AlterUniqueTogether(
            name='tagging',
            unique_together={('feed', 'tag')},
        ),
    ]


# chris_backend/feeds/migrations/0008_auto_20190114_1437.py

# Generated by Django 2.1.4 on 2019-01-14 14:37

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0007_auto_20181101_2034'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='feedfile',
            name='feed',
        ),
        migrations.RemoveField(
            model_name='feedfile',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='feed',
            name='plugin_inst',
        ),
        migrations.DeleteModel(
            name='FeedFile',
        ),
    ]


# chris_backend/feeds/migrations/0009_auto_20210804_1530.py

# Generated by Django 2.2.24 on 2021-08-04 19:30

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0008_auto_20190114_1437'),
    ]

    operations = [
        migrations.AlterField(
            model_name='comment',
            name='content',
            field=models.TextField(blank=True),
        ),
        migrations.AlterField(
            model_name='comment',
            name='title',
            field=models.CharField(blank=True, max_length=100),
        ),
        migrations.AlterField(
            model_name='feed',
            name='name',
            field=models.CharField(blank=True, db_index=True, max_length=200),
        ),
        migrations.AlterField(
            model_name='note',
            name='content',
            field=models.TextField(blank=True),
        ),
        migrations.AlterField(
            model_name='note',
            name='title',
            field=models.CharField(blank=True, max_length=100),
        ),
        migrations.AlterField(
            model_name='tag',
            name='name',
            field=models.CharField(blank=True, max_length=100),
        ),
    ]


# chris_backend/feeds/tests/test_models.py


import logging

from django.test import TestCase
from django.contrib.auth.models import User
from django.conf import settings

from plugins.models import PluginMeta, Plugin, PluginParameter, ComputeResource
from plugininstances.models import PluginInstance
from feeds.models import Note, Feed


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class FeedModelTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.feed_name = "Feed1"
        self.plugin_name = "pacspull"
        self.plugin_type = "fs"
        self.plugin_parameters = {'mrn': {'type': 'string', 'optional': False},
                                  'img_type': {'type': 'string', 'optional': True}}
        self.username = 'foo'
        self.password = 'bar'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create a "fs" plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                         type=self.plugin_type)
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        # add plugin parameter
        PluginParameter.objects.get_or_create(
            plugin=plugin,
            name='mrn',
            type=self.plugin_parameters['mrn']['type'],
            optional=self.plugin_parameters['mrn']['optional'])

        # create user
        user = User.objects.create_user(username=self.username,
                                        password=self.password)

        # create a plugin instance that in turn creates a new feed
        (plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        plg_inst.feed.name = self.feed_name
        plg_inst.feed.save()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_save_creates_new_note_just_after_feed_is_created(self):
        """
        Test whether overriden save method creates a note just after a feed is created.
        """
        self.assertEqual(Note.objects.count(), 1)

    def test_get_creator(self):
        """
        Test whether custom get_creator method properly returns the user that created the
        feed.
        """
        user = User.objects.get(username=self.username)
        feed = Feed.objects.get(name=self.feed_name)
        feed_creator = feed.get_creator()
        self.assertEqual(feed_creator, user)

    def test_get_plugin_instances_status_count(self):
        """
        Test whether custom get_plugin_instances_status_count method properly returns
        the number of plugin instances with a specific status.
        """
        feed = Feed.objects.get(name=self.feed_name)
        count = feed.get_plugin_instances_status_count('created')
        self.assertEqual(count, 1)
        count = feed.get_plugin_instances_status_count('waiting')
        self.assertEqual(count, 0)
        count = feed.get_plugin_instances_status_count('scheduled')
        self.assertEqual(count, 0)
        count = feed.get_plugin_instances_status_count('started')
        self.assertEqual(count, 0)
        count = feed.get_plugin_instances_status_count('registeringFiles')
        self.assertEqual(count, 0)
        count = feed.get_plugin_instances_status_count('finishedSuccessfully')
        self.assertEqual(count, 0)
        count = feed.get_plugin_instances_status_count('finishedWithError')
        self.assertEqual(count, 0)
        count = feed.get_plugin_instances_status_count('cancelled')
        self.assertEqual(count, 0)


# chris_backend/feeds/tests/test_serializers.py


import logging
from unittest import mock

from django.test import TestCase, tag
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import serializers

from plugins.models import PluginMeta, Plugin, ComputeResource
from plugininstances.models import PluginInstance
from feeds.models import Feed, Tag, Tagging
from feeds.serializers import TaggingSerializer, FeedSerializer


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class SerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.username = 'foo'
        self.password = 'bar'
        self.feedname = "Feed1"
        self.other_username = 'boo'
        self.other_password = 'far'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create users
        User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        user = User.objects.create_user(username=self.username,
                                        password=self.password)

        # create a "fs" plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        # create a feed by creating a "fs" plugin instance
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = self.feedname
        pl_inst.feed.save()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class TaggingSerializerTests(SerializerTests):

    def setUp(self):
        super(TaggingSerializerTests, self).setUp()

        # create a tag
        self.user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag1", color="blue", owner=self.user)

        # tag self.feedname with Tag1
        feed = Feed.objects.get(name=self.feedname)
        Tagging.objects.get_or_create(tag=tag, feed=feed)

    def test_create(self):
        """
        Test whether overriden 'create' method raises a ValidationError when a new
        tagging already exists in the DB.
        """
        feed = Feed.objects.get(name=self.feedname)
        tag = Tag.objects.get(name="Tag1")
        data = {'tag': tag, 'feed': feed}
        tagging_serializer = TaggingSerializer(data=data)
        with self.assertRaises(serializers.ValidationError):
            tagging_serializer.create(data)

    def test_validate_tag(self):
        """
        Test whether custom validate_tag method returns a tag instance or
        raises a serializers.ValidationError.
        """
        feed = Feed.objects.get(name=self.feedname)
        tag = Tag.objects.get(name="Tag1")
        data = {'tag': tag, 'feed': feed}
        tagging_serializer = TaggingSerializer(data=data)
        tagging_serializer.context['request'] = mock.Mock()
        tagging_serializer.context['request'].user = self.user

        tag_inst = tagging_serializer.validate_tag(tag.id)
        self.assertEqual(tag, tag_inst)

        with self.assertRaises(serializers.ValidationError):
            tagging_serializer.validate_tag('') # error if no id is passed

        with self.assertRaises(serializers.ValidationError):
            tagging_serializer.validate_tag(tag.id + 1) # error if tag not found in DB

        with self.assertRaises(serializers.ValidationError):
            other_user = User.objects.get(username=self.other_username)
            tagging_serializer.context['request'].user = other_user
            tagging_serializer.validate_tag(tag.id) # error if users doesn't own tag

    def test_validate_feed(self):
        """
        Test whether custom validate_feed method returns a feed instance or
        raises a serializers.ValidationError.
        """
        feed = Feed.objects.get(name=self.feedname)
        tag = Tag.objects.get(name="Tag1")
        data = {'tag': tag, 'feed': feed}
        tagging_serializer = TaggingSerializer(data=data)
        tagging_serializer.context['request'] = mock.Mock()
        tagging_serializer.context['request'].user = self.user

        feed_inst = tagging_serializer.validate_feed(feed.id)
        self.assertEqual(feed, feed_inst)

        with self.assertRaises(serializers.ValidationError):
            tagging_serializer.validate_feed('')  # error if no id is passed

        with self.assertRaises(serializers.ValidationError):
            tagging_serializer.validate_feed(feed.id + 1)  # error if feed not found in DB

        with self.assertRaises(serializers.ValidationError):
            other_user = User.objects.get(username=self.other_username)
            tagging_serializer.context['request'].user = other_user
            tagging_serializer.validate_feed(feed.id)  # error if users doesn't own feed


class FeedSerializerTests(SerializerTests):

    def setUp(self):
        super(FeedSerializerTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.feed_serializer = FeedSerializer(feed)

    def test_validate_name(self):
        """
        Test whether overriden validate_name method raises a serializers.ValidationError
        when the feed name contains forward slashes or it is the special 'uploads'
        identifier.
        """
        with self.assertRaises(serializers.ValidationError):
            self.feed_serializer.validate_name('myfeed/')
        with self.assertRaises(serializers.ValidationError):
            self.feed_serializer.validate_name('uploads')
        name = self.feed_serializer.validate_name('myfeed')
        self.assertEqual(name, 'myfeed')

    def test_validate_new_owner(self):
        """
        Test whether custom validate_new_owner method returns a user instance
        or raises a serializers.ValidationError when the proposed new owner is
        not a system-registered user.
        """
        new_owner = User.objects.get(username=self.other_username)
        user_inst = self.feed_serializer.validate_new_owner(new_owner.username)
        self.assertEqual(user_inst, new_owner)

        with self.assertRaises(serializers.ValidationError):
            self.feed_serializer.validate_new_owner('not a registered user')

    def test_get_creator_username(self):
        """
        Test whether overriden get_creator_username method returns the username of the
        user that created the feed.
        """
        creator_username = self.feed_serializer.get_creator_username(self.feed_serializer.instance)
        self.assertEqual(creator_username, self.username)

    def test_get_started_jobs(self):
        """
        Test whether overriden get_created_jobs method returns the correct number of
        associated plugin instances in 'created' status.
        """
        count = self.feed_serializer.get_created_jobs(self.feed_serializer.instance)
        self.assertEqual(count, 1)

    def test_get_finished_jobs(self):
        """
        Test whether overriden get_finished_jobs method returns the correct number of
        associated plugin instances in 'finishedSuccessfully' status.
        """
        count = self.feed_serializer.get_finished_jobs(self.feed_serializer.instance)
        self.assertEqual(count, 0)

    def test_get_errored_jobs(self):
        """
        Test whether overriden get_errored_jobs method returns the correct number of
        associated plugin instances in 'finishedWithError' status.
        """
        started_jobs = self.feed_serializer.get_errored_jobs(self.feed_serializer.instance)
        self.assertEqual(started_jobs, 0)

    def test_get_cancelled_jobs(self):
        """
        Test whether overriden get_cancelled_jobs method returns the correct number of
        associated plugin instances in 'cancelled' status.
        """
        started_jobs = self.feed_serializer.get_cancelled_jobs(self.feed_serializer.instance)
        self.assertEqual(started_jobs, 0)


# chris_backend/feeds/tests/test_views.py


import logging
import json
import io

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import status

from plugins.models import PluginMeta, Plugin, ComputeResource
from plugininstances.models import PluginInstance, PluginInstanceFile
from feeds.models import Note, Tag, Tagging, Feed, Comment
from core.swiftmanager import SwiftManager


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ViewTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type='application/vnd.collection+json'
        self.chris_username = 'chris'
        self.chris_password = 'chris12'
        self.username = 'foo'
        self.password = 'bar'
        self.other_username = 'boo'
        self.other_password = 'far'
             
        self.plugin_name = "pacspull"
        self.plugin_type = "fs"
        self.plugin_parameters = {'mrn': {'type': 'string', 'optional': False},
                                  'img_type': {'type': 'string', 'optional': True}}
        self.feedname = "Feed1"

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create basic models
        
        # create the chris user and two other users
        User.objects.create_user(username=self.chris_username,
                                 password=self.chris_password)
        User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        user = User.objects.create_user(username=self.username,
                                        password=self.password)
        
        # create two plugins of different types

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()
        
        # create a feed by creating a "fs" plugin instance
        pl_inst = PluginInstance.objects.create(plugin=plugin, owner=user, title='test',
            compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = self.feedname
        pl_inst.feed.save()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)
        

class NoteDetailViewTests(ViewTests):
    """
    Test the note-detail view.
    """

    def setUp(self):
        super(NoteDetailViewTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.read_update_url = reverse("note-detail", kwargs={"pk": feed.id})
        self.put = json.dumps({"template": {"data": [{"name": "title", "value": "Note1"},
                                          {"name": "content", "value": "My first note"}]}})

    def test_note_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, "title")
        self.assertContains(response, "content")

    def test_note_detail_success_user_chris(self):
        self.client.login(username=self.chris_username, password=self.chris_password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, "title")
        self.assertContains(response, "content")

    def test_note_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_note_detail_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_note_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "Note1")
        self.assertContains(response, "My first note")

    def test_note_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_note_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
        

class FeedListViewTests(ViewTests):
    """
    Test the feed-list view.
    """

    def setUp(self):
        super(FeedListViewTests, self).setUp()     
              
        self.list_url = reverse("feed-list")

        # create an additional feed using a "fs" plugin instance
        plugin = Plugin.objects.get(meta__name="pacspull")
        user = User.objects.get(username=self.username)
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = "Feed2"
        pl_inst.feed.save()
    
    def test_feed_create_failure_post_not_allowed(self):
        self.client.login(username=self.username, password=self.password)
        # try to create a new feed with a POST request to the list of feeds
        post = json.dumps({"template": {"data": [{"name": "name", "value": "Feed2"}]}})
        response = self.client.post(self.list_url, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

    def test_feed_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "Feed1")
        self.assertContains(response, "Feed2")

    def test_feed_list_success_chris_user_lists_all_users_feeds(self):
        self.client.login(username=self.chris_username, password=self.chris_password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "Feed1")
        self.assertContains(response, "Feed2")

    def test_feed_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_list_from_other_users_not_listed(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertNotContains(response, "Feed1")
        self.assertNotContains(response, "Feed2")


class FeedListQuerySearchViewTests(ViewTests):
    """
    Test the feed-list-query-search view.
    """

    def setUp(self):
        super(FeedListQuerySearchViewTests, self).setUp()     
              
        self.list_url = reverse("feed-list-query-search") + '?name=' + self.feedname

        # create an additional feed using a "fs" plugin instance
        plugin = Plugin.objects.get(meta__name="pacspull")
        user = User.objects.get(username=self.username)
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = "Feed2"
        pl_inst.feed.save()

    def test_feed_list_query_search_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, self.feedname)
        self.assertNotContains(response, "Feed2")

    def test_feed_list_query_search_success_chris_user_lists_all_matching_feeds(self):
        self.client.login(username=self.chris_username, password=self.chris_password)
        response = self.client.get(self.list_url)
        self.assertContains(response, self.feedname)

    def test_feed_list_query_search_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_list_query_search_from_other_users_not_listed(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertNotContains(response, "Feed2")
        

class FeedDetailViewTests(ViewTests):
    """
    Test the feed-detail view.
    """

    def setUp(self):
        super(FeedDetailViewTests, self).setUp()     
        feed = Feed.objects.get(name=self.feedname)
        
        self.read_update_delete_url = reverse("feed-detail", kwargs={"pk": feed.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "name", "value": "Updated"},
                                  {"name": "owner", "value": self.other_username}]}})
          
    def test_feed_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, self.feedname)

    def test_feed_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_detail_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_feed_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "Updated")
        new_owner = User.objects.get(username=self.other_username)
        feed = Feed.objects.get(name="Updated")
        self.assertIn(new_owner, feed.owner.all())

    def test_feed_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_feed_update_failure_new_unregistered_owner(self):
        put = json.dumps({"template": {"data": [{"name": "owner", "value": "foouser"}]}})
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        feed = Feed.objects.get(name=self.feedname)
        self.assertFalse(len(feed.owner.all()) > 1)

    def test_feed_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Feed.objects.count(), 0)

    def test_feed_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class CommentListViewTests(ViewTests):
    """
    Test the comment-list view.
    """

    def setUp(self):
        super(CommentListViewTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.corresponding_feed_url = reverse("feed-detail", kwargs={"pk": feed.id})
        self.create_read_url = reverse("comment-list", kwargs={"pk": feed.id})
        self.post = json.dumps(
            {"template": {"data": [{"name": "title", "value": "Comment1"}]}})

        # create two comments
        user = User.objects.get(username=self.username)
        Comment.objects.get_or_create(title="Comment2",feed=feed, owner=user)
        Comment.objects.get_or_create(title="Comment3",feed=feed, owner=user)

    def test_comment_create_success_related_feed_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data["title"], "Comment1")
        self.assertTrue(response.data["feed"].endswith(self.corresponding_feed_url))

    def test_comment_create_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_comment_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_comment_list_success_related_feed_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "Comment2")
        self.assertContains(response, "Comment3")

    def test_comment_list_success_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "Comment2")
        self.assertContains(response, "Comment3")

    def test_comment_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
        

class CommentDetailViewTests(ViewTests):
    """
    Test the comment-detail view.
    """

    def setUp(self):
        super(CommentDetailViewTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.corresponding_feed_url = reverse("feed-detail", kwargs={"pk": feed.id})

        # create a comment
        user = User.objects.get(username=self.username)
        (comment, tf) = Comment.objects.get_or_create(title="Comment1",
                                                      feed=feed, owner=user)

        self.read_update_delete_url = reverse("comment-detail", kwargs={"pk": comment.id})       
        self.put = json.dumps({
            "template": {"data": [{"name": "title", "value": "Updated"}]}})
          
    def test_comment_detail_success_related_feed_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "Comment1")
        self.assertTrue(response.data["feed"].endswith(self.corresponding_feed_url))

    def test_comment_detail_success_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "Comment1")
        self.assertTrue(response.data["feed"].endswith(self.corresponding_feed_url))

    def test_comment_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_comment_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "Updated")

    def test_comment_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_comment_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_comment_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Comment.objects.count(), 0)

    def test_comment_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_comment_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
        

class TagListViewTests(ViewTests):
    """
    Test the tag-list view.
    """

    def setUp(self):
        super(TagListViewTests, self).setUp()
        self.create_read_url = reverse("tag-list")
        self.post = json.dumps(
            {"template": {"data": [{"name": "name", "value": "Tag1"},
                                   {"name": "color", "value": "Black"}]}})

        # create two tags
        user = User.objects.get(username=self.username)
        Tag.objects.get_or_create(name="Tag2", color="blue", owner=user)
        Tag.objects.get_or_create(name="Tag3", color="red", owner=user)
        
    def test_tag_create_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data["name"], "Tag1")

    def test_tag_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "Tag2")
        self.assertContains(response, "Tag3")

    def test_tag_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
        
    def test_tag_list_for_other_users_not_listed(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.create_read_url)
        self.assertNotContains(response, "Tag2")
        self.assertNotContains(response, "Tag3")


class TagDetailViewTests(ViewTests):
    """
    Test the tag-detail view.
    """

    def setUp(self):
        super(TagDetailViewTests, self).setUp()

        # create a tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag1", color="blue", owner=user)

        self.read_update_delete_url = reverse("tag-detail", kwargs={"pk": tag.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "name", "value": "Updated"},
                                  {"name": "color", "value": "black"}]}})

    def test_tag_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "Tag1")

    def test_tag_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_detail_failure_not_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_tag_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "Updated")

    def test_tag_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_tag_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Tag.objects.count(), 0)

    def test_tag_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class FeedTagListViewTests(ViewTests):
    """
    Test the feed-tag-list view.
    """

    def setUp(self):
        super(FeedTagListViewTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.list_url = reverse("feed-tag-list", kwargs={"pk": feed.id})

        # create Tag2 tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag2", color="blue", owner=user)

        # tag self.feedname with Tag2
        Tagging.objects.get_or_create(tag=tag, feed=feed)

        # create a new feed by creating a "fs" plugin instance

        plugin = Plugin.objects.get(meta__name="pacspull")
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = "new"
        pl_inst.feed.save()

        # create Tag3 tag
        (tag, tf) = Tag.objects.get_or_create(name="Tag3", color="red", owner=user)

        # tag the new feed with Tag3
        feed = Feed.objects.get(name="new")
        Tagging.objects.get_or_create(tag=tag, feed=feed)

    def test_feed_tag_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "Tag2")
        self.assertNotContains(response, "Tag3") # tag list is feed-specific

    def test_feed_tag_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_tag_list_failure_not_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
        
    def test_feed_tag_list_from_other_feed_owners_not_listed(self):
        self.client.login(username=self.other_username, password=self.other_password)
        feed = Feed.objects.get(name=self.feedname)
        owner = User.objects.get(username=self.username)
        new_owner = User.objects.get(username=self.other_username)
        # make new_owner an owner of the feed together with the feed's current owner
        feed.owner.set([owner, new_owner])
        feed.save()
        response = self.client.get(self.list_url)
        self.assertNotContains(response, "Tag2") # a feed owner can not see another feed owner's tags


class TagFeedListViewTests(ViewTests):
    """
    Test the tag-feed-list view.
    """

    def setUp(self):
        super(TagFeedListViewTests, self).setUp()

        # create Tag2 tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag2", color="blue", owner=user)

        self.list_url = reverse("tag-feed-list", kwargs={"pk": tag.id})

        # tag self.feedname with Tag2
        feed = Feed.objects.get(name=self.feedname)
        Tagging.objects.get_or_create(tag=tag, feed=feed)

        # create a new feed by creating a "fs" plugin instance
        plugin = Plugin.objects.get(meta__name="pacspull")
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = "new"
        pl_inst.feed.save()

        # create Tag3 tag
        (tag, tf) = Tag.objects.get_or_create(name="Tag3", color="red", owner=user)

        # tag the new feed with Tag3
        feed = Feed.objects.get(name="new")
        Tagging.objects.get_or_create(tag=tag, feed=feed)

    def test_tag_feed_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, self.feedname)
        self.assertNotContains(response, "new")  # feed list is tag-specific

    def test_tag_feed_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_feed_list_failure_not_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class FeedTaggingListViewTests(ViewTests):
    """
    Test the feed-tagging-list view.
    """

    def setUp(self):
        super(FeedTaggingListViewTests, self).setUp()

        feed = Feed.objects.get(name=self.feedname)
        self.create_read_url = reverse("feed-tagging-list", kwargs={"pk": feed.id})

        # create Tag1 tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag1", color="green", owner=user)

        self.post = json.dumps(
            {"template": {"data": [{"name": "tag_id", "value": tag.id}]}})

        # create Tag2 tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag2", color="blue", owner=user)

        # tag self.feedname with Tag2
        Tagging.objects.get_or_create(tag=tag, feed=feed)

    def test_feed_tagging_create_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        tag = Tag.objects.get(name="Tag1")
        self.assertEqual(response.data["tag_id"], tag.id)
        feed = Feed.objects.get(name=self.feedname)
        self.assertEqual(response.data["feed_id"], feed.id)

    def test_feed_tagging_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_tagging_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        tag = Tag.objects.get(name="Tag2")
        self.assertContains(response, tag.id)

    def test_feed_tagging_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_feed_tagging_list_failure_not_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_feed_tagging_list_from_other_feed_owners_not_listed(self):
        self.client.login(username=self.other_username, password=self.other_password)
        feed = Feed.objects.get(name=self.feedname)
        owner = User.objects.get(username=self.username)
        new_owner = User.objects.get(username=self.other_username)
        # make new_owner an owner of the feed together with the feed's current owner
        feed.owner.set([owner, new_owner])
        feed.save()
        response = self.client.get(self.create_read_url)
        tag = Tag.objects.get(name="Tag2")
        self.assertNotContains(response, tag.id) # a feed owner can not see another feed owner's taggings


class TagTaggingListViewTests(ViewTests):
    """
    Test the tag-tagging-list view.
    """

    def setUp(self):
        super(TagTaggingListViewTests, self).setUp()

        feed = Feed.objects.get(name=self.feedname)

        # create Tag2 tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag2", color="blue", owner=user)

        self.create_read_url = reverse("tag-tagging-list", kwargs={"pk": tag.id})

        self.post = json.dumps(
            {"template": {"data": [{"name": "feed_id", "value": feed.id}]}})

        # create a new feed by creating a "fs" plugin instance
        plugin = Plugin.objects.get(meta__name="pacspull")
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.name = "new"
        pl_inst.feed.save()

        # tag new feed with Tag2
        feed = Feed.objects.get(name="new")
        Tagging.objects.get_or_create(tag=tag, feed=feed)

        # create Tag3 tag
        (tag, tf) = Tag.objects.get_or_create(name="Tag3", color="red", owner=user)

        # tag self.feedname with Tag3
        Tagging.objects.get_or_create(tag=tag, feed=feed)

    def test_tag_tagging_create_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        tag = Tag.objects.get(name="Tag2")
        self.assertEqual(response.data["tag_id"], tag.id)
        feed = Feed.objects.get(name=self.feedname)
        self.assertEqual(response.data["feed_id"], feed.id)

    def test_tag_tagging_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_tagging_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        feed = Feed.objects.get(name="new")
        self.assertContains(response, feed.id)
        feed = Feed.objects.get(name=self.feedname)
        self.assertNotContains(response, feed.id)

    def test_tag_tagging_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tag_tagging_list_failure_not_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class TaggingDetailViewTests(ViewTests):
    """
    Test the tagging-detail view.
    """

    def setUp(self):
        super(TaggingDetailViewTests, self).setUp()

        # create a tag
        user = User.objects.get(username=self.username)
        (tag, tf) = Tag.objects.get_or_create(name="Tag1", color="blue", owner=user)

        # tag self.feedname with Tag1
        feed = Feed.objects.get(name=self.feedname)
        (tagging, tf) = Tagging.objects.get_or_create(tag=tag, feed=feed)

        self.read_delete_url = reverse("tagging-detail", kwargs={"pk": tagging.id})

    def test_tagging_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_delete_url)
        tag = Tag.objects.get(name="Tag1")
        self.assertEqual(response.data["tag_id"], tag.id)
        feed = Feed.objects.get(name=self.feedname)
        self.assertContains(response, feed.id)

    def test_tagging_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tagging_detail_failure_not_related_tag_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_tagging_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Tagging.objects.count(), 0)

    def test_tagging_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_tagging_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class FeedFileListViewTests(ViewTests):
    """
    Test the feedfile-list view.
    """

    def setUp(self):
        super(FeedFileListViewTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.corresponding_feed_url = reverse("feed-detail", kwargs={"pk": feed.id})
        self.list_url = reverse("feedfile-list", kwargs={"pk": feed.id})

        # create a test file
        # test_file_path = self.test_dir
        # self.test_file = test_file_path + '/file1.txt'
        # file = open(self.test_file, "w")
        # file.write("test file1")
        # file.close()
        # file = open(test_file_path + '/file2.txt', "w")
        # file.write("test file2")
        # file.close()

        # create two files in the DB "already uploaded" to the server from two different
        # plugin instances that write to the same feed
        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        plg_inst = PluginInstance.objects.all()[0]
        self.path1 = 'tests/file1.txt'
        with io.StringIO("test file1") as file1:
            self.swift_manager.upload_obj(self.path1, file1.read(),
                                          content_type='text/plain')
        (plg_inst_file, tf) = PluginInstanceFile.objects.get_or_create(plugin_inst=plg_inst)
        plg_inst_file.fname.name = self.path1
        plg_inst_file.save()

        # create a second 'ds' plugin instance in the same feed tree
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name="mri_convert")
        (plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, previous_id=plg_inst.id,
            compute_resource=plugin.compute_resources.all()[0])
        self.path2 = 'tests/file2.txt'
        with io.StringIO("test file2") as file2:
            self.swift_manager.upload_obj(self.path2, file2.read(),
                                          content_type='text/plain')
        (plg_inst_file, tf) = PluginInstanceFile.objects.get_or_create(plugin_inst=plg_inst)
        plg_inst_file.fname.name = self.path2
        plg_inst_file.save()

    def tearDown(self):
        # delete file from Swift storage
        self.swift_manager.delete_obj(self.path1)
        self.swift_manager.delete_obj(self.path2)
        super().tearDown()

    def test_feedfile_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        # it shows all the files for all the plugin instances that write to the feed
        self.assertContains(response, "file1.txt")
        self.assertContains(response, "file2.txt")

    def test_feedfile_list_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_feedfile_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class FeedPluginInstanceListViewTests(ViewTests):
    """
    Test the feed-plugininstance-list view.
    """

    def setUp(self):
        super(FeedPluginInstanceListViewTests, self).setUp()
        feed = Feed.objects.get(name=self.feedname)
        self.list_url = reverse("feed-plugininstance-list", kwargs={"pk": feed.id})

        # create two files in the DB "already uploaded" to the server from two different
        # plugin instances that write to the same feed
        plg_inst = PluginInstance.objects.get(title='test')

        # create a second 'ds' plugin instance in the same feed tree
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name="mri_convert")
        PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, previous_id=plg_inst.id, title='test1',
            compute_resource=plugin.compute_resources.all()[0])

    def test_feed_plugin_instance_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        # it shows all the plugin instances associated to the feed
        self.assertContains(response, "test")
        self.assertContains(response, "test1")

    def test_feed_plugin_instance_list_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_feed_plugin_instance_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/filebrowser/tests/test_services.py


import logging
from unittest import mock, skip

from django.test import TestCase

from servicefiles.models import ServiceFile, Service
from pacsfiles.models import PACSFile
from uploadedfiles.models import UploadedFile
from plugininstances.models import PluginInstanceFile
from filebrowser import services


class ServiceTests(TestCase):
    """
    Test top-level functions in the services module
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.username = 'foo'

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_get_path_file_model_class(self):
        """
        Test whether services.get_path_file_model_class function gets the correct file
        model class associated to a path.
        """
        username = self.username

        path = 'SERVICES/PACS'
        model_class = services.get_path_file_model_class(path, username)
        self.assertEqual(model_class, PACSFile)

        path = 'SERVICES'
        model_class = services.get_path_file_model_class(path, username)
        self.assertEqual(model_class, ServiceFile)

        path = f'{username}/uploads'
        model_class = services.get_path_file_model_class(path, username)
        self.assertEqual(model_class, UploadedFile)

        path = f'{username}'
        model_class = services.get_path_file_model_class(path, username)
        self.assertEqual(model_class, PluginInstanceFile)

        path = 'SOR'
        model_class = services.get_path_file_model_class(path, username)
        self.assertIs(model_class, None)

    def test_get_path_file_queryset(self):
        """
        Test whether services.get_path_file_queryset function correctly returns the
        queryset associated to a path or Raises ValueError if the path is not found.
        """
        username = self.username
        path = 'SOR'
        with self.assertRaises(ValueError):
            services.get_path_file_queryset(path, username)

        path = f'{username}/crazypath1234567890'
        with self.assertRaises(ValueError):
            services.get_path_file_queryset(path, username)

        path = f'{username}/uploads'
        qs = services.get_path_file_queryset(path, username)
        self.assertTrue(qs.count() >= 0)

    def test_get_path_folders(self):
        """
        Test whether services.get_path_folders function appends a collection+json
        template to its response argument
        """
        username = self.username

        path = f'{username}'
        folders = services.get_path_folders(path, username)
        self.assertIn('uploads', folders)

        path = 'SERVICES'
        # create a service's files in the DB
        service = Service(identifier='lolo')
        service.save()
        f1 = ServiceFile(service=service)
        f1.fname.name = 'SERVICES/lolo/lele/a.txt'
        f1.save()
        f2 = ServiceFile(service=service)
        f2.fname.name = 'SERVICES/lolo/b.txt'
        f2.save()
        f3 = ServiceFile(service=service)
        f3.fname.name = 'SERVICES/lili/c.txt'
        f3.save()
        folders = services.get_path_folders(path, username)
        self.assertEqual(sorted(folders), ['PACS', 'lili', 'lolo'])


# chris_backend/filebrowser/tests/test_views.py


import logging
import io
from unittest import mock

from django.test import TestCase, tag
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse

from rest_framework import status

from core.swiftmanager import SwiftManager
from uploadedfiles.models import UploadedFile


class FileBrowserViewTests(TestCase):
    """
    Generic filebrowser view tests' setup and tearDown.
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'
        self.username = 'test'
        self.password = 'testpass'

        # create a user
        User.objects.create_user(username=self.username, password=self.password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class FileBrowserPathListViewTests(FileBrowserViewTests):
    """
    Test the 'filebrowserpath-list' view.
    """

    def setUp(self):
        super(FileBrowserPathListViewTests, self).setUp()
        self.read_url = reverse('filebrowserpath-list')

    def test_filebrowserpath_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, 'path')
        self.assertEqual(response.data['results'][0]['subfolders'], f'SERVICES,{self.username}')

    def test_filebrowserpath_list_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class FileBrowserPathListQuerySearchViewTests(FileBrowserViewTests):
    """
    Test the 'filebrowserpath-list-query-search' view.
    """

    def setUp(self):
        super(FileBrowserPathListQuerySearchViewTests, self).setUp()

        # create a file in the DB "already uploaded" to the server)
        user = User.objects.get(username=self.username)
        upload_path = f'{self.username}/uploads/myfolder/file1.txt'
        uploadedfile = UploadedFile(owner=user)
        uploadedfile.fname.name = upload_path
        uploadedfile.save()
        self.read_url = reverse('filebrowserpath-list-query-search') + f'?path={self.username}/uploads/'

    def test_filebrowserpath_list_query_search_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, f'{self.username}/uploads')
        self.assertIn('myfolder', response.data['results'][0]['subfolders'].split(','))

    def test_filebrowserpath_list_query_search_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class FileBrowserPathViewTests(FileBrowserViewTests):
    """
    Test the filebrowserpath view.
    """

    def setUp(self):
        super(FileBrowserPathViewTests, self).setUp()

        # create a file in the DB "already uploaded" to the server)
        user = User.objects.get(username=self.username)
        upload_path = f'{self.username}/uploads/myfolder/file1.txt'
        uploadedfile = UploadedFile(owner=user)
        uploadedfile.fname.name = upload_path
        uploadedfile.save()

        self.read_url = reverse("filebrowserpath", kwargs={"path": f'{self.username}/uploads'})

    def test_filebrowserpath_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, f'{self.username}/uploads')
        self.assertIn('myfolder', response.data['subfolders'].split(','))

    def test_filebrowserpath_failure_not_found(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url + 'unknownpath/')
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_filebrowserpath_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class FileBrowserPathFileListViewTests(FileBrowserViewTests):
    """
    Test the 'filebrowserpath-list' view.
    """

    def setUp(self):
        super(FileBrowserPathFileListViewTests, self).setUp()

        # create a file in the DB "already uploaded" to the server)
        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                          settings.SWIFT_CONNECTION_PARAMS)

        # upload file to Swift storage
        self.upload_path = f'{self.username}/uploads/file2.txt'
        with io.StringIO("test file") as file1:
            self.swift_manager.upload_obj(self.upload_path, file1.read(),
                                         content_type='text/plain')
        user = User.objects.get(username=self.username)
        uploadedfile = UploadedFile(owner=user)
        uploadedfile.fname.name = self.upload_path
        uploadedfile.save()

        self.read_url = reverse("filebrowserpathfile-list",
                                kwargs={"path": f'{self.username}/uploads'})

    def tearDown(self):
        # delete file from Swift storage
        self.swift_manager.delete_obj(self.upload_path)

        super(FileBrowserPathFileListViewTests, self).tearDown()

    def test_filebrowserpathfile_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, 'file_resource')
        self.assertContains(response, self.upload_path)

    def test_filebrowserpathfile_list_failure_not_found(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url + 'unknownpath/')
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_filebrowserpathfile_list_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/pacsfiles/migrations/0001_initial.py

# Generated by Django 2.1.4 on 2020-04-23 23:11

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='PACS',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('identifier', models.CharField(max_length=20, unique=True)),
            ],
        ),
        migrations.CreateModel(
            name='PACSFile',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('fname', models.FileField(max_length=512, unique=True, upload_to='')),
                ('PatientID', models.CharField(db_index=True, max_length=15)),
                ('PatientName', models.CharField(max_length=30)),
                ('StudyInstanceUID', models.CharField(db_index=True, max_length=50)),
                ('StudyDescription', models.CharField(blank=True, max_length=200)),
                ('SeriesInstanceUID', models.CharField(db_index=True, max_length=50)),
                ('SeriesDescription', models.CharField(blank=True, max_length=200)),
                ('pacs', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pacsfiles.PACS')),
            ],
            options={
                'ordering': ('-fname',),
            },
        ),
    ]


# chris_backend/pacsfiles/migrations/0002_auto_20200616_1433.py

# Generated by Django 2.2.12 on 2020-06-16 18:33

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('pacsfiles', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='pacsfile',
            name='PatientAge',
            field=models.IntegerField(blank=True, null=True),
        ),
        migrations.AddField(
            model_name='pacsfile',
            name='PatientBirthDate',
            field=models.DateField(blank=True, null=True),
        ),
        migrations.AddField(
            model_name='pacsfile',
            name='PatientSex',
            field=models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='PatientID',
            field=models.CharField(db_index=True, max_length=150),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='PatientName',
            field=models.CharField(blank=True, max_length=150),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='SeriesDescription',
            field=models.CharField(blank=True, max_length=500),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='SeriesInstanceUID',
            field=models.CharField(db_index=True, max_length=150),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='StudyDescription',
            field=models.CharField(blank=True, max_length=500),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='StudyInstanceUID',
            field=models.CharField(db_index=True, max_length=150),
        ),
    ]


# chris_backend/pacsfiles/migrations/0003_pacsfile_modality.py

# Generated by Django 2.2.12 on 2020-07-06 18:11

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('pacsfiles', '0002_auto_20200616_1433'),
    ]

    operations = [
        migrations.AddField(
            model_name='pacsfile',
            name='Modality',
            field=models.CharField(blank=True, max_length=15),
        ),
    ]


# chris_backend/pacsfiles/migrations/0004_auto_20210409_1532.py

# Generated by Django 2.2.12 on 2021-04-09 19:32

import datetime
from django.db import migrations, models
from django.utils.timezone import utc


class Migration(migrations.Migration):

    dependencies = [
        ('pacsfiles', '0003_pacsfile_modality'),
    ]

    operations = [
        migrations.AddField(
            model_name='pacsfile',
            name='ProtocolName',
            field=models.CharField(blank=True, max_length=64),
        ),
        migrations.AddField(
            model_name='pacsfile',
            name='StudyDate',
            field=models.DateField(db_index=True, default=datetime.datetime(2021, 4, 9, 19, 32, 14, 90971, tzinfo=utc)),
            preserve_default=False,
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='SeriesInstanceUID',
            field=models.CharField(max_length=150),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='StudyInstanceUID',
            field=models.CharField(max_length=150),
        ),
    ]


# chris_backend/pacsfiles/migrations/0005_auto_20210831_1024.py

# Generated by Django 2.2.24 on 2021-08-31 14:24

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('pacsfiles', '0004_auto_20210409_1532'),
    ]

    operations = [
        migrations.AddField(
            model_name='pacsfile',
            name='AccessionNumber',
            field=models.CharField(blank=True, db_index=True, max_length=100),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='PatientID',
            field=models.CharField(db_index=True, max_length=100),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='SeriesDescription',
            field=models.CharField(blank=True, max_length=400),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='SeriesInstanceUID',
            field=models.CharField(max_length=100),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='StudyDescription',
            field=models.CharField(blank=True, max_length=400),
        ),
        migrations.AlterField(
            model_name='pacsfile',
            name='StudyInstanceUID',
            field=models.CharField(max_length=100),
        ),
    ]


# chris_backend/pacsfiles/tests/test_serializers.py


import logging
import time
import io
from unittest import mock

from django.test import TestCase, tag
from django.conf import settings
from rest_framework import serializers

from pacsfiles.models import PACS, PACSFile
from pacsfiles.serializers import PACSFileSerializer
from pacsfiles.serializers import SwiftManager


class PACSFileSerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_validate_path_failure_does_not_start_with_SERVICES_PACS(self):
        """
        Test whether overriden validate_path method validates submitted path must start
        with the 'SERVICES/PACS/' string.
        """
        pacsfiles_serializer = PACSFileSerializer()
        path = 'cube/123456-Jorge/brain/brain_mri/file1.dcm'
        with self.assertRaises(serializers.ValidationError):
            pacsfiles_serializer.validate_path(path)
        path = 'SERVICES/123456-Jorge/brain/brain_mri/file1.dcm'
        with self.assertRaises(serializers.ValidationError):
            pacsfiles_serializer.validate_path(path)

    def test_validate_path_failure_does_not_exist(self):
        """
        Test whether overriden validate_path method validates that submitted path exists
        in internal storage.
        """
        pacsfiles_serializer = PACSFileSerializer()
        path = 'SERVICES/PACS/MyPACS/123456-Jorge/brain/brain_mri/file1.dcm'

        with mock.patch.object(SwiftManager, 'obj_exists',
                               return_value=False) as obj_exists_mock:
            with self.assertRaises(serializers.ValidationError):
                pacsfiles_serializer.validate_path(path)
            obj_exists_mock.assert_called_with(path.strip(' ').strip('/'))

    @tag('integration')
    def test_integration_validate_path_failure_does_not_exist(self):
        """
        Test whether overriden validate_path method validates that submitted path exists
        in internal storage.
        """
        pacsfiles_serializer = PACSFileSerializer()
        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file1.dcm'
        with self.assertRaises(serializers.ValidationError):
            pacsfiles_serializer.validate_path(path)

    @tag('integration')
    def test_integration_validate_path_success(self):
        """
        Test whether overriden validate_path method validates submitted path.
        """
        pacsfiles_serializer = PACSFileSerializer()
        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file1.dcm'
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        # upload file to Swift storage
        with io.StringIO("test file") as file1:
            swift_manager.upload_obj(path, file1.read(), content_type='text/plain')

        for _ in range(20):
            if swift_manager.obj_exists(path):
                break
            time.sleep(0.2)
        self.assertEqual(pacsfiles_serializer.validate_path(path), path)

        # delete file from Swift storage
        swift_manager.delete_obj(path)

    def test_validate_updates_validated_data(self):
        """
        Test whether overriden validate method updates validated data with a PACS object.
        """
        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file1.dcm'
        data = {'PatientID': '123456', 'PatientName': 'crazy',
                'StudyDate': '2020-07-15',
                'StudyInstanceUID': '1.1.3432.54.6545674765.765434',
                'StudyDescription': 'brain_crazy_study',
                'SeriesDescription': 'SAG T1 MPRAGE',
                'SeriesInstanceUID': '2.4.3432.54.845674765.763345',
                'pacs_name': 'MyPACS', 'path': path}
        pacsfiles_serializer = PACSFileSerializer()
        new_data = pacsfiles_serializer.validate(data)
        self.assertIn('pacs', new_data)

    def test_validate_validates_path_has_not_already_been_registered(self):
        """
        Test whether overriden validate method validates that the submitted path
        has not been already registered.
        """
        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file1.dcm'
        data = {'PatientID': '123456', 'PatientName': 'crazy',
                'StudyDate': '2020-07-15',
                'StudyInstanceUID': '1.1.3432.54.6545674765.765434',
                'StudyDescription': 'brain_crazy_study',
                'SeriesDescription': 'SAG T1 MPRAGE',
                'SeriesInstanceUID': '2.4.3432.54.845674765.763345',
                'pacs_name': 'MyPACS', 'path': path}
        pacs = PACS(identifier='MyPACS')
        pacs.save()
        pacs_file = PACSFile(PatientID='123456',
                             StudyDate='2020-07-15',
                             StudyInstanceUID='1.1.3432.54.6545674765.765434',
                             SeriesInstanceUID='2.4.3432.54.845674765.763345',
                             pacs=pacs)
        pacs_file.fname.name = path
        pacs_file.save()
        with self.assertRaises(serializers.ValidationError):
            pacsfiles_serializer = PACSFileSerializer()
            pacsfiles_serializer.validate(data)


# chris_backend/pacsfiles/tests/test_views.py


import logging
import json
import io
from unittest import mock

from django.test import TestCase, tag
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse

from rest_framework import status

from core.swiftmanager import SwiftManager
from pacsfiles.models import PACS, PACSFile
from pacsfiles import views


class PACSFileViewTests(TestCase):
    """
    Generic pacsfile view tests' setup and tearDown.
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'
        self.username = 'test'
        self.password = 'testpass'

        User.objects.create_user(username=self.username, password=self.password)

        # create a PACS file in the DB "already registered" to the server)
        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        # upload file to Swift storage
        self.path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file1.dcm'
        with io.StringIO("test file") as file1:
            self.swift_manager.upload_obj(self.path, file1.read(),
                                          content_type='text/plain')
        pacs = PACS(identifier='MyPACS')
        pacs.save()
        pacs_file = PACSFile(PatientID='123456',
                             PatientName='crazy',
                             StudyDate='2020-07-15',
                             StudyInstanceUID='1.1.3432.54.6545674765.765434',
                             StudyDescription='brain_crazy_study',
                             SeriesInstanceUID='2.4.3432.54.845674765.763345',
                             SeriesDescription='SAG T1 MPRAGE',
                             pacs=pacs)
        pacs_file.fname.name = self.path
        pacs_file.save()

    def tearDown(self):
        # delete file from Swift storage
        self.swift_manager.delete_obj(self.path)
        # re-enable logging
        logging.disable(logging.NOTSET)


class PACSFileListViewTests(PACSFileViewTests):
    """
    Test the pacsfile-list view.
    """

    def setUp(self):
        super(PACSFileListViewTests, self).setUp()
        self.create_read_url = reverse("pacsfile-list")
        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file2.dcm'
        self.post = json.dumps(
            {"template": {"data": [{"name": "path", "value": path},
                                   {"name": "PatientID", "value": "123456"},
                                   {"name": "PatientName", "value": "crazy"},
                                   {"name": "StudyDate", "value": '2020-07-15'},
                                   {"name": "StudyInstanceUID",
                                    "value": '1.1.3432.54.6545674765.765434'},
                                   {"name": "StudyDescription", "value": "brain_crazy_study"},
                                   {"name": "SeriesInstanceUID",
                                    "value": "2.4.3432.54.845674765.763345"},
                                   {"name": "SeriesDescription", "value": "SAG T1 MPRAGE"},
                                   {"name": "pacs_name", "value": "MyPACS"}]}})

    def tearDown(self):
        super(PACSFileListViewTests, self).tearDown()

    @tag('integration')
    def test_integration_pacsfile_create_success(self):
        chris_username = 'chris'
        chris_password = 'chris1234'
        User.objects.create_user(username=chris_username, password=chris_password)

        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file2.dcm'
        # upload file to Swift storage
        with io.StringIO("test file") as file1:
            self.swift_manager.upload_obj(path, file1.read(), content_type='text/plain')

        # make the POST request using the chris user
        self.client.login(username=chris_username, password=chris_password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # delete file from Swift storage
        self.swift_manager.delete_obj(path)

    def test_pacsfile_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pacsfile_create_failure_already_exists(self):
        chris_username = 'chris'
        chris_password = 'chris1234'
        User.objects.create_user(username=chris_username, password=chris_password)
        self.client.login(username=chris_username, password=chris_password)
        path = 'SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRAGE/file2.dcm'
        pacs = PACS.objects.get(identifier='MyPACS')
        pacs_file = PACSFile(PatientID='123456',
                             StudyDate='2020-07-15',
                             StudyInstanceUID='1.1.3432.54.6545674765.765434',
                             SeriesInstanceUID='2.4.3432.54.845674765.763345',
                             pacs=pacs)
        pacs_file.fname.name = path
        pacs_file.save()
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_pacsfile_create_failure_permission_denied_not_chris_user(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_pacsfile_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, self.path)

    def test_pacsfile_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PACSFileDetailViewTests(PACSFileViewTests):
    """
    Test the pacsfile-detail view.
    """

    def setUp(self):
        super(PACSFileDetailViewTests, self).setUp()
        pacs_file = PACSFile.objects.get(PatientID=123456)
        self.read_url = reverse("pacsfile-detail",
                                kwargs={"pk": pacs_file.id})

    def test_pacsfile_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, self.path)

    def test_pacsfile_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PACSFileResourceViewTests(PACSFileViewTests):
    """
    Test the pacsfile-resource view.
    """

    def setUp(self):
        super(PACSFileResourceViewTests, self).setUp()
        pacs_file = PACSFile.objects.get(PatientID=123456)
        self.download_url = reverse("pacsfile-resource",
                                    kwargs={"pk": pacs_file.id}) + 'file1.dcm'

    def tearDown(self):
        super(PACSFileResourceViewTests, self).tearDown()

    def test_pacsfileresource_get(self):
        pacs_file = PACSFile.objects.get(PatientID=123456)
        fileresource_view_inst = mock.Mock()
        fileresource_view_inst.get_object = mock.Mock(return_value=pacs_file)
        request_mock = mock.Mock()
        with mock.patch('pacsfiles.views.FileResponse') as response_mock:
            views.PACSFileResource.get(fileresource_view_inst, request_mock)
            response_mock.assert_called_with(pacs_file.fname)

    @tag('integration')
    def test_integration_pacsfileresource_download_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, 200)
        content = [c for c in response.streaming_content][0].decode('utf-8')
        self.assertEqual(content, "test file")

    def test_fileresource_download_failure_unauthenticated(self):
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/pipelineinstances/migrations/0001_initial.py

# Generated by Django 2.1.4 on 2019-02-21 18:03

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('pipelines', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='PipelineInstance',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(blank=True, max_length=100)),
                ('description', models.CharField(blank=True, max_length=800)),
                ('pipeline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='pipelines.Pipeline')),
            ],
            options={
                'ordering': ('pipeline',),
            },
        ),
    ]


# chris_backend/pipelineinstances/migrations/0002_pipelineinstance_owner.py

# Generated by Django 2.1.4 on 2019-07-17 18:20

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('pipelineinstances', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='pipelineinstance',
            name='owner',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
            preserve_default=False,
        ),
    ]


# chris_backend/pipelineinstances/migrations/0003_auto_20190719_2018.py

# Generated by Django 2.1.4 on 2019-07-19 20:18

from django.db import migrations, models
import plugins.fields


class Migration(migrations.Migration):

    dependencies = [
        ('pipelineinstances', '0002_pipelineinstance_owner'),
    ]

    operations = [
        migrations.AddField(
            model_name='pipelineinstance',
            name='cpu_limit',
            field=plugins.fields.CPUField(null=True),
        ),
        migrations.AddField(
            model_name='pipelineinstance',
            name='gpu_limit',
            field=models.IntegerField(null=True),
        ),
        migrations.AddField(
            model_name='pipelineinstance',
            name='memory_limit',
            field=plugins.fields.MemoryField(null=True),
        ),
        migrations.AddField(
            model_name='pipelineinstance',
            name='number_of_workers',
            field=models.IntegerField(null=True),
        ),
    ]


# chris_backend/pipelineinstances/services/manager.py

"""
Pipeline instance manager module that provides functionality to run and check the
execution status of a pipeline instance.
"""

import time

from django.conf import settings


class PipelineInstanceManager(object):

    @staticmethod
    def run_pipeline_instance(pipeline_inst, parameter_dict):
        """
        Run a pipeline instance.
        """
        pass

    @staticmethod
    def check_pipeline_instance_exec_status(plugin_inst):
        """
        Check a pipeline instance execution status. It connects to the remote
        service to determine pipeline's jobs status.
        """
        pass

    @staticmethod
    def cancel_pipeline_instance_exec(plugin_inst):
        """
        Cancel a pipeline instance execution. It connects to the remote service to
        cancel pipeline's jobs.
        """
        pass


# chris_backend/pipelineinstances/tests/test_serializers.py


import logging
from unittest import mock

from django.test import TestCase
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import serializers

from plugininstances.models import PluginInstance
from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter, DefaultStrParameter, DefaultIntParameter
from pipelines.models import Pipeline, PluginPiping
from pipelineinstances.models import PipelineInstance
from pipelineinstances.serializers import PipelineInstanceSerializer


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class SerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_fs_name = "simplefsapp"
        self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True,
                                             'default': "./"}}
        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'dummyInt': {'type': 'integer', 'optional': True,
                                                  'default': 111111}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_fs_name, type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add plugins' parameters
        (plg_param_fs, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type=self.plugin_fs_parameters['dir']['type'],
            optional=self.plugin_fs_parameters['dir']['optional'])
        default = self.plugin_fs_parameters['dir']['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param_fs,
                                                   value=default)  # set plugin parameter default

        # add a parameter with a default
        (plg_param_ds, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type=self.plugin_ds_parameters['dummyInt']['type'],
            optional=self.plugin_ds_parameters['dummyInt']['optional']
        )
        default = self.plugin_ds_parameters['dummyInt']['default']
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=default)  # set plugin parameter default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        (pipeline, tf) = Pipeline.objects.get_or_create(name=self.pipeline_name,
                                                        owner=user, category='test')

        # create two plugin pipings
        self.pips = []
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline)
        self.pips.append(pip)
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, previous=pip,
                                                       pipeline=pipeline)
        self.pips.append(pip)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PipelineInstanceSerializerTests(SerializerTests):

    def setUp(self):
        super(PipelineInstanceSerializerTests, self).setUp()

    def test_create(self):
        """
        Test whether overriden 'create' method successfully creates a new pipeline
        instance after deleting 'previous_plugin_inst_id' from serializer data as it's
        not a model field.
        """
        owner = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        (pl_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=owner, compute_resource=plugin.compute_resources.all()[0])
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        data = {'title': 'PipelineInst1', 'previous_plugin_inst_id': pl_inst.id}
        pipeline_inst_serializer = PipelineInstanceSerializer(data=data)
        pipeline_inst_serializer.is_valid(raise_exception=True)
        pipeline_inst_serializer.validated_data['pipeline'] = pipeline
        pipeline_inst_serializer.validated_data['owner'] = owner
        pipeline_inst = pipeline_inst_serializer.create(pipeline_inst_serializer.validated_data)
        self.assertNotIn('previous_plugin_inst_id', pipeline_inst_serializer.validated_data)
        self.assertIsInstance(pipeline_inst, PipelineInstance)

    def test_validate_previous_plugin_inst(self):
        """
        Test whether custom validate_previous_plugin_inst method validates that an integer
        id is provided for previous instance. Then checks that the id exists in the DB and
        that the user can run plugins within the corresponding feed.
        """
        owner = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        (pl_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=owner, compute_resource=plugin.compute_resources.all()[0])
        data = {'title': 'PipelineInst1', 'previous_plugin_inst_id': pl_inst.id}
        pipeline_inst_serializer = PipelineInstanceSerializer(data=data)
        pipeline_inst_serializer.context['request'] = mock.Mock()
        pipeline_inst_serializer.context['request'].user = owner
        with self.assertRaises(serializers.ValidationError):
            pipeline_inst_serializer.validate_previous_plugin_inst(None)
        with self.assertRaises(serializers.ValidationError):
            pipeline_inst_serializer.validate_previous_plugin_inst(pl_inst.id+1)
        # create another user
        another_user = User.objects.create_user(username='boo', password='far')
        with self.assertRaises(serializers.ValidationError):
            pipeline_inst_serializer.context['request'].user = another_user
            pipeline_inst_serializer.validate_previous_plugin_inst(pl_inst.id)

    def test_parse_parameters(self):
        """
        Test whether custom parse_parameters method properly parses the pipeline instance
        parameters in the request data dictionary.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        pipeline_inst_serializer = PipelineInstanceSerializer()
        pipeline_inst_serializer.context['request'] = mock.Mock()
        # parameters name in the request have the form
        # < plugin.id > _ < piping.id > _ < previous_piping.id > _ < param.name >
        param_name = "%s_%s_%s_%s" % (plugin.id, self.pips[1].id, self.pips[0].id, 'dummy_Int')
        pipeline_inst_serializer.context['request'].data = {param_name: 1122}
        parsed_params_dict = pipeline_inst_serializer.parse_parameters()
        self.assertEqual(parsed_params_dict, {self.pips[1].id: {'dummy_Int': 1122}})


# chris_backend/pipelineinstances/tests/test_views.py


import logging
import json
from unittest import mock

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import status

from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter, DefaultStrParameter, DefaultIntParameter
from plugininstances.models import PluginInstance
from pipelines.models import Pipeline, PluginPiping
from pipelineinstances.models import PipelineInstance


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ViewTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'

        self.plugin_fs_name = "simplefsapp"
        self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True,
                                             'default': "./"}}
        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'dummyInt': {'type': 'integer', 'optional': True,
                                                  'default': 111111}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_fs_name, type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add plugins' parameters
        (plg_param_fs, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type=self.plugin_fs_parameters['dir']['type'],
            optional=self.plugin_fs_parameters['dir']['optional'])
        default = self.plugin_fs_parameters['dir']['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param_fs,
                                                   value=default)  # set plugin parameter default

        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type=self.plugin_ds_parameters['dummyInt']['type'],
            optional=self.plugin_ds_parameters['dummyInt']['optional']
        )
        default = self.plugin_ds_parameters['dummyInt']['default']
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=default)  # set plugin parameter default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        (pipeline, tf) = Pipeline.objects.get_or_create(name=self.pipeline_name,
                                                        owner=user, category='test')

        # create two plugin pipings
        self.pips = []
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline)
        self.pips.append(pip)
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, previous=pip,
                                                       pipeline=pipeline)
        self.pips.append(pip)

        # create another user
        self.other_username = 'boo'
        self.other_password = 'far'
        User.objects.create_user(username=self.other_username, password=self.other_password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PipelineInstanceListViewTests(ViewTests):
    """
    Test the pipelineinstance-list view.
    """

    def setUp(self):
        super(PipelineInstanceListViewTests, self).setUp()
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        self.create_read_url = reverse("pipelineinstance-list", kwargs={"pk": pipeline.id})
        owner = User.objects.get(username=self.username)
        plugin_fs = Plugin.objects.get(meta__name=self.plugin_fs_name)
        (self.pl_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin_fs, owner=owner, compute_resource=plugin_fs.compute_resources.all()[0])

    def test_pipeline_instance_create_success(self):

        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        param_name = "%s_%s_%s_%s" % (plugin_ds.id, self.pips[1].id, self.pips[0].id, 'dummyInt')
        post = json.dumps(
            {"template": {"data": [{"name": "title", "value": "PipelineInst1"},
                                   {"name": "previous_plugin_inst_id", "value": self.pl_inst.id},
                                   {"name": param_name, "value": 333333}]}})
        # make API request
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=post, content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_pipeline_instance_list_success(self):
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        PipelineInstance.objects.get_or_create(title="PipelineInst1", pipeline=pipeline,
                                               owner=owner)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "PipelineInst1")

    def test_pipeline_instance_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PipelineInstanceListQuerySearchViewTests(ViewTests):
    """
    Test the pipelineinstance-list-query-search view.
    """

    def setUp(self):
        super(PipelineInstanceListQuerySearchViewTests, self).setUp()

        self.query_url1 = reverse("allpipelineinstance-list-query-search") + "?title=PipelineInst"
        self.query_url2 = reverse("allpipelineinstance-list-query-search") + "?pipeline_name=" + \
                          self.pipeline_name

    def test_pipeline_instance_query_search_list_success(self):
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        PipelineInstance.objects.get_or_create(title="PipelineInst1", pipeline=pipeline,
                                               owner=owner)
        PipelineInstance.objects.get_or_create(title="PipelineMyInst", pipeline=pipeline,
                                               owner=owner)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.query_url1)
        # response should only contain the instances that match the query
        self.assertContains(response, "PipelineInst1")
        self.assertNotContains(response, "PipelineMyInst")
        response = self.client.get(self.query_url2)
        self.assertContains(response, "PipelineInst1")
        self.assertContains(response, "PipelineMyInst")

    def test_pipeline_instance_query_search_list_failure_unauthenticated(self):
        response = self.client.get(self.query_url1)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PipelineInstanceDetailViewTests(ViewTests):
    """
    Test the pipelineinstance-detail view.
    """

    def setUp(self):
        super(PipelineInstanceDetailViewTests, self).setUp()
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        (pipeline_inst, tf) = PipelineInstance.objects.get_or_create(title="PipelineInst1",
                                                                     pipeline=pipeline,
                                                                     owner=owner)
        self.read_update_delete_url = reverse("pipelineinstance-detail", kwargs={"pk": pipeline_inst.id})

    def test_pipeline_instance_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "PipelineInst1")

    def test_pipeline_instance_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pipeline_instance_update_success(self):
        put = json.dumps({
            "template": {"data": [{"name": "title", "value": "Test pipeline instance"}]}})

        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertContains(response, "Test pipeline instance")

    def test_pipeline_instance_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_delete_url, data={},
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pipeline_instance_update_failure_access_denied(self):
        put = json.dumps({
            "template": {"data": [{"name": "title", "value": "Test pipeline instance"}]}})

        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_pipeline_instance_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(PipelineInstance.objects.count(), 0)

    def test_pipeline_instance_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pipeline_instance_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class PipelineInstancePluginInstanceListViewTests(ViewTests):
    """
    Test the pipelineinstance-plugininstance-list view.
    """

    def setUp(self):
        super(PipelineInstancePluginInstanceListViewTests, self).setUp()
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        (self.pipeline_inst, tf) = PipelineInstance.objects.get_or_create(title="PipelineInst1",
                                                                          pipeline=pipeline,
                                                                          owner=owner)
        self.list_url = reverse("pipelineinstance-plugininstance-list",
                                kwargs={"pk": self.pipeline_inst.id})

    def test_pipeline_instance_plugin_instance_list_success(self):
        plugin_fs = Plugin.objects.get(meta__name=self.plugin_fs_name)
        owner = User.objects.get(username=self.username)
        (previous_plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin_fs, owner=owner, compute_resource=plugin_fs.compute_resources.all()[0])
        pipeline = Pipeline.objects.get(name=self.pipeline_name)

        for pip in pipeline.plugin_pipings.all():
            plugin_ds = pip.plugin
            (pl_inst_ds, tf) = PluginInstance.objects.get_or_create(
                plugin=plugin_ds, title="PluginInst" + str(pip.id), owner=owner,
                pipeline_inst=self.pipeline_inst, previous=previous_plg_inst,
                compute_resource=plugin_fs.compute_resources.all()[0])
            previous_plg_inst = pl_inst_ds

        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        for pip in pipeline.plugin_pipings.all():
            self.assertContains(response, "PluginInst" + str(pip.id))

    def test_pipeline_instance_plugin_instance_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/pipelines/migrations/0001_initial.py

# Generated by Django 2.1.4 on 2019-02-21 18:03

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('plugins', '0028_auto_20190221_1803'),
    ]

    operations = [
        migrations.CreateModel(
            name='DefaultPipingBoolParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.BooleanField(blank=True, default=False)),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_piping_default', to='plugins.PluginParameter')),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingFloatParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.FloatField(blank=True, default=1.0)),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_piping_default', to='plugins.PluginParameter')),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingIntParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.IntegerField(blank=True, default=1)),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_piping_default', to='plugins.PluginParameter')),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingPathParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_piping_default', to='plugins.PluginParameter')),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingStrParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_piping_default', to='plugins.PluginParameter')),
            ],
        ),
        migrations.CreateModel(
            name='Pipeline',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('modification_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(max_length=100, unique=True)),
                ('locked', models.BooleanField(default=True)),
                ('authors', models.CharField(blank=True, max_length=200)),
                ('category', models.CharField(blank=True, max_length=100)),
                ('description', models.CharField(blank=True, max_length=800)),
                ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'ordering': ('category',),
            },
        ),
        migrations.CreateModel(
            name='PluginPiping',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('pipeline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugin_pipings', to='pipelines.Pipeline')),
                ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.Plugin')),
                ('previous', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='next', to='pipelines.PluginPiping')),
            ],
            options={
                'ordering': ('pipeline',),
            },
        ),
        migrations.AddField(
            model_name='pipeline',
            name='plugins',
            field=models.ManyToManyField(related_name='pipelines', through='pipelines.PluginPiping', to='plugins.Plugin'),
        ),
        migrations.AddField(
            model_name='defaultpipingstrparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_param', to='pipelines.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingpathparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_param', to='pipelines.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingintparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_param', to='pipelines.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingfloatparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_param', to='pipelines.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingboolparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_param', to='pipelines.PluginPiping'),
        ),
    ]


# chris_backend/pipelines/migrations/0002_auto_20190221_2113.py

# Generated by Django 2.1.4 on 2019-02-21 21:13

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('pipelines', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='defaultpipingboolparameter',
            name='value',
            field=models.BooleanField(null=True),
        ),
        migrations.AlterField(
            model_name='defaultpipingfloatparameter',
            name='value',
            field=models.FloatField(null=True),
        ),
        migrations.AlterField(
            model_name='defaultpipingintparameter',
            name='value',
            field=models.IntegerField(null=True),
        ),
        migrations.AlterField(
            model_name='defaultpipingpathparameter',
            name='value',
            field=models.CharField(max_length=200, null=True),
        ),
        migrations.AlterField(
            model_name='defaultpipingstrparameter',
            name='value',
            field=models.CharField(max_length=200, null=True),
        ),
    ]


# chris_backend/pipelines/migrations/0003_auto_20190524_1700.py

# Generated by Django 2.1.4 on 2019-05-24 17:00

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0029_auto_20190423_1448'),
        ('pipelines', '0002_auto_20190221_2113'),
    ]

    operations = [
        migrations.AlterUniqueTogether(
            name='defaultpipingboolparameter',
            unique_together={('plugin_piping', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='defaultpipingfloatparameter',
            unique_together={('plugin_piping', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='defaultpipingintparameter',
            unique_together={('plugin_piping', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='defaultpipingpathparameter',
            unique_together={('plugin_piping', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='defaultpipingstrparameter',
            unique_together={('plugin_piping', 'plugin_param')},
        ),
    ]


# chris_backend/pipelines/migrations/0004_auto_20200211_1602.py

# Generated by Django 2.1.4 on 2020-02-11 16:02

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('pipelines', '0003_auto_20190524_1700'),
    ]

    operations = [
        migrations.AlterField(
            model_name='defaultpipingpathparameter',
            name='value',
            field=models.CharField(max_length=1024, null=True),
        ),
        migrations.AlterField(
            model_name='defaultpipingstrparameter',
            name='value',
            field=models.CharField(max_length=600, null=True),
        ),
    ]


# chris_backend/pipelines/migrations/0005_auto_20200213_0040.py

# Generated by Django 2.1.4 on 2020-02-13 00:40

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('pipelines', '0004_auto_20200211_1602'),
    ]

    operations = [
        migrations.AlterUniqueTogether(
            name='defaultpipingpathparameter',
            unique_together=set(),
        ),
        migrations.RemoveField(
            model_name='defaultpipingpathparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='defaultpipingpathparameter',
            name='plugin_piping',
        ),
        migrations.DeleteModel(
            name='DefaultPipingPathParameter',
        ),
    ]


# chris_backend/pipelines/services/manager.py

"""
Pipeline manager module that provides functionality to add, modify and delete pipelines.
"""

import os
import sys
from argparse import ArgumentParser

if __name__ == '__main__':
    # django needs to be loaded when this script is run standalone from the command line
    sys.path.append(os.path.join(os.path.dirname(__file__), '../../'))
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
    import django
    django.setup()

from django.contrib.auth.models import User
from pipelines.models import Pipeline
from pipelines.serializers import PipelineSerializer


class PipelineManager(object):

    def __init__(self):
        parser = ArgumentParser(description='Manage pipelines')
        subparsers = parser.add_subparsers(dest='subparser_name', title='subcommands',
                                           description='valid subcommands',
                                           help='sub-command help')

        # create the parser for the "add" command
        parser_add = subparsers.add_parser('add', help='Add a new pipeline')
        parser_add.add_argument('name', help="Pipeline's name")
        parser_add.add_argument('owner', help="Pipeline's owner username")
        parser_add.add_argument('plugintree',
                           help="A json string with the plugin tree for the pipeline")
        parser_add.add_argument('--authors', help="Pipeline's authors string")
        parser_add.add_argument('--category', help="Pipeline's category")
        parser_add.add_argument('--description', help="Pipeline's description")
        parser_add.add_argument('--unlock', action='store_true',
                                help="Unlock pipeline to make it immutable and visible "
                                     "to other users")

        # create the parser for the "modify" command
        parser_modify = subparsers.add_parser('modify', help='Modify existing pipeline')
        parser_modify.add_argument('id', type=int, help="Plugin's id")
        parser_modify.add_argument('--name', help="Pipeline's name")
        parser_modify.add_argument('--authors', help="Pipeline's authors string")
        parser_modify.add_argument('--category', help="Pipeline's category")
        parser_modify.add_argument('--description', help="Pipeline's description")
        parser_modify.add_argument('--unlock', action='store_true',
                                help="Unlock pipeline to make it immutable and visible "
                                     "to other users")

        # create the parser for the "remove" command
        parser_remove = subparsers.add_parser('remove', help='Remove an existing pipeline')
        parser_remove.add_argument('id', type=int, help="Plugin's id")

        self.parser = parser

    def add_pipeline(self, args):
        """
        Add a new pipeline to the system.
        """
        data = {'name': args.name, 'plugin_tree': args.plugintree}
        if args.authors:
            data['authors'] = args.authors
        if args.category:
            data['category'] = args.category
        if args.description:
            data['description'] = args.description
        if args.unlock:
            data['locked'] = False
        pipeline_serializer = PipelineSerializer(data=data)
        pipeline_serializer.is_valid(raise_exception=True)
        owner = User.objects.get(username=args.owner)
        pipeline_serializer.save(owner=owner)

    def modify_pipeline(self, args):
        """
        Modify an existing pipeline.
        """
        pipeline = self.get_pipeline(args.id)
        data = {}
        if args.name:
            data['name'] = args.name
        if args.authors:
            data['authors'] = args.authors
        if args.category:
            data['category'] = args.category
        if args.description:
            data['description'] = args.description
        if args.unlock:
            data['locked'] = False
        pipeline_serializer = PipelineSerializer(pipeline, data=data)
        pipeline_serializer.is_valid(raise_exception=True)
        pipeline_serializer.save()

    def remove_pipeline(self, args):
        """
        Remove an existing pipeline from the system.
        """
        pipeline = self.get_pipeline(args.id)
        pipeline.delete()

    def run(self, args=None):
        """
        Parse the arguments passed to the manager and perform the appropriate action.
        """
        options = self.parser.parse_args(args)
        if options.subparser_name == 'add':
            self.add_pipeline(options)
        elif options.subparser_name == 'modify':
            self.modify_pipeline(options)
        elif options.subparser_name == 'remove':
            self.remove_pipeline(options)

    @staticmethod
    def get_pipeline(id):
        """
        Get an existing pipeline.
        """
        try:
            pipeline = Pipeline.objects.get(pk=id)
        except Pipeline.DoesNotExist:
            raise NameError("Couldn't find pipeline with id '%s' in the system" % id)
        return pipeline


# ENTRYPOINT
if __name__ == "__main__":
    manager = PipelineManager()
    manager.run()


# chris_backend/pipelines/tests/test_manager.py


import logging
from unittest import mock

from django.contrib.auth.models import User
from django.test import TestCase, tag
from django.conf import settings

from plugins.models import PluginMeta, Plugin, PluginParameter, DefaultIntParameter
from plugins.models import ComputeResource
from pipelines.models import Pipeline, PluginPiping
from pipelines.services import manager


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class PipelineManagerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_version = "0.1"
        self.plugin_ds_parameters = {'dummyInt': {'type': 'integer', 'optional': True,
                                                  'default': 111111}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version=self.plugin_ds_version)
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type=self.plugin_ds_parameters['dummyInt']['type'],
            optional=self.plugin_ds_parameters['dummyInt']['optional']
        )
        default = self.plugin_ds_parameters['dummyInt']['default']
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=default)  # set plugin parameter default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        (pipeline, tf) = Pipeline.objects.get_or_create(name=self.pipeline_name, owner=user, category='test')

        # create two plugin pipings
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, pipeline=pipeline)
        PluginPiping.objects.get_or_create(plugin=plugin_ds, previous=pip, pipeline=pipeline)

        self.pipeline_manager = manager.PipelineManager()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_mananger_can_add_pipeline(self):
        """
        Test whether the manager can add a new pipeline to the system.
        """
        plugin_tree = '[{"plugin_name": "simpledsapp", "plugin_version": "0.1", "previous_index": null}, ' \
                      '{"plugin_name": "simpledsapp", "plugin_version": "0.1", "previous_index": 0}]'
        self.pipeline_manager.run(['add', 'Pipeline2', self.username, plugin_tree, '--unlock'])
        self.assertEqual(Pipeline.objects.count(), 2)

    def test_mananger_can_modify_pipeline(self):
        """
        Test whether the manager can modify an existing pipeline.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        initial_modification_date = pipeline.modification_date
        self.pipeline_manager.run(['modify', str(pipeline.id), '--name', 'Pipeline2'])
        pipeline = Pipeline.objects.get(pk=pipeline.id)
        self.assertTrue(pipeline.modification_date > initial_modification_date)
        self.assertEqual(pipeline.name, 'Pipeline2')

    def test_mananger_can_remove_pipeline(self):
        """
        Test whether the manager can remove an existing pipeline from the system.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        self.pipeline_manager.run(['remove', str(pipeline.id)])
        self.assertEqual(Pipeline.objects.count(), 0)
        self.assertEqual(PluginPiping.objects.count(), 0)

    def test_mananger_can_get_pipeline(self):
        """
        Test whether the manager can return a pipeline object.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        self.assertEqual(pipeline, self.pipeline_manager.get_pipeline(pipeline.id))


# chris_backend/pipelines/tests/test_models.py


import logging

from django.test import TestCase
from django.contrib.auth.models import User
from django.conf import settings

from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter, DefaultIntParameter
from pipelines.models import Pipeline
from pipelines.models import PluginPiping
from pipelines.models import DEFAULT_PIPING_PARAMETER_MODELS


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ModelTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'prefix': {'type': 'string', 'optional': False}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add plugin's parameters
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='prefix',
            type=self.plugin_ds_parameters['prefix']['type'],
            optional=self.plugin_ds_parameters['prefix']['optional'])  # this plugin parameter has no default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        (pipeline, tf) = Pipeline.objects.get_or_create(name=self.pipeline_name, owner=user)

        # create two plugin pipings
        self.pips = []
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, pipeline=pipeline)
        self.pips.append(pip)
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, previous=pip,
                                                        pipeline=pipeline)
        self.pips.append(pip)

        # create default values for the piping parameters as the corresponding plugin
        # didn't set a default
        param_type = self.plugin_ds_parameters['prefix']['type']
        default_model_class = DEFAULT_PIPING_PARAMETER_MODELS[param_type]
        for i in range(2):
            (default_piping_param, tf) = default_model_class.objects.get_or_create(
                plugin_piping=self.pips[i], plugin_param=plg_param_ds)
            default_piping_param.value = "test" + str(i)
            default_piping_param.save()


    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PipelineModelTests(ModelTests):

    def test_get_pipings_parameters_names(self):
        """
        Test whether custom get_pipings_parameters_names method returns the list of all
        the plugin parameter names for all the associated plugin pipings. The name of the
        parameters should be transformed to have the plugin id, piping id and previous
        piping id as a prefix.
        """
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        param = plugin_ds.parameters.get(name='prefix')
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        param_names = pipeline.get_pipings_parameters_names()
        self.assertEqual(len(param_names), 2)
        self.assertEqual(param_names[0], "%s_%s_%s_%s" %
                         (plugin_ds.id, self.pips[0].id, "null", param.name))
        self.assertEqual(param_names[1], "%s_%s_%s_%s" %
                         (plugin_ds.id, self.pips[1].id, self.pips[0].id, param.name))

    def test_get_pipings_tree(self):
        """
        Test whether custom get_pipings_tree method returns a dictionary containing a
        dictionary representing a tree of pipings and the id of the piping that is the
        root of the tree. The keys of the dictionary tree should be the pipings' ids and
        the values the dictionaries containing the piping and the list of child pipings' ids.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        tree_dict = pipeline.get_pipings_tree()
        root_id = tree_dict['root_id']
        tree = tree_dict['tree']
        self.assertEqual(root_id, self.pips[0].id)
        self.assertEqual(tree[root_id], {'piping': self.pips[0], 'child_ids': [self.pips[1].id]})

    def test_check_parameter_defaults(self):
        """
        Test whether custom check_parameter_defaults method raises an exception if
        any of the plugin parameters associated to any of the pipings in the pipeline
        doesn't have a default value.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a new plugin piping to pipeline but do not set a default value for
        # the plugin parameter
        PluginPiping.objects.get_or_create(plugin=plugin_ds, pipeline=pipeline,
                                           previous=self.pips[1])
        with self.assertRaises(ValueError):
            pipeline.check_parameter_defaults()

    def test_get_accesible_pipelines(self):
        """
        Test whether custom get_accesible_pipelines method returns a filtered queryset
        with all the pipelines that are accessible to a given user (not locked or
        otherwise own by the user).
        """
        user1 = User.objects.get(username=self.username)
        # create new user
        user2 = User.objects.create_user(username='testuser', password='testuser-pass')
        accessible_pipelines_user1 = Pipeline.get_accesible_pipelines(user1)
        self.assertEqual(len(accessible_pipelines_user1),1)
        self.assertEqual(accessible_pipelines_user1[0], Pipeline.objects.all()[0])
        self.assertEqual(len(Pipeline.get_accesible_pipelines(user2)), 0)


class PluginPipingModelTests(ModelTests):

    def test_save(self):
        """
        Test whether overriden save method saves the default plugin parameters' values
        associated with this piping.
        """
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type='integer',
            optional=True
        )
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds, value=1)  # set plugin parameter default
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pip = PluginPiping()
        pip.plugin = plugin_ds
        pip.pipeline = pipeline
        pip.save()
        defaults = pip.integer_param.all()
        self.assertEqual(len(defaults), 1)
        self.assertEqual(defaults[0].value, 1)

    def test_check_parameter_defaults(self):
        """
        Test whether custom check_parameter_defaults method raises an exception if
        any of the plugin parameters associated to the piping doesn't have a default value.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a new plugin piping to pipeline but do not set a default value for
        # the plugin parameter
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, pipeline=pipeline,
                                           previous=self.pips[1])
        with self.assertRaises(ValueError):
            pip.check_parameter_defaults()


# chris_backend/pipelines/tests/test_serializers.py


import logging
from unittest import mock

from django.test import TestCase
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import serializers

from plugininstances.models import PluginInstance
from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter, DefaultStrParameter, DefaultIntParameter
from pipelines.models import Pipeline
from pipelines.serializers import PipelineSerializer


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class SerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_fs_name = "simplefsapp"
        self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True,
                                             'default': "./"}}
        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'dummyInt': {'type': 'integer', 'optional': True,
                                                  'default': 111111}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_fs_name, type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add plugins' parameters
        (plg_param_fs, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type=self.plugin_fs_parameters['dir']['type'],
            optional=self.plugin_fs_parameters['dir']['optional'])
        default = self.plugin_fs_parameters['dir']['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param_fs,
                                                   value=default)  # set plugin parameter default

        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type=self.plugin_ds_parameters['dummyInt']['type'],
            optional=self.plugin_ds_parameters['dummyInt']['optional']
        )
        default = self.plugin_ds_parameters['dummyInt']['default']
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=default)  # set plugin parameter default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        Pipeline.objects.get_or_create(name=self.pipeline_name, owner=user, category='test')

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PipelineSerializerTests(SerializerTests):

    def setUp(self):
        super(PipelineSerializerTests, self).setUp()

    def test_create(self):
        """
        Test whether overriden 'create' method successfully creates a new pipeline
        with a tree of associated plugins.
        """
        plugin_ds1 = Plugin.objects.get(meta__name=self.plugin_ds_name)
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        owner = User.objects.get(username=self.username)
        plugin_tree = '[{"plugin_id": ' + str(plugin_ds1.id) + \
                         ', "previous_index": null}, {"plugin_id": ' + \
                         str(plugin_ds2.id) + ', "previous_index": 0}]'
        data = {'name': 'Pipeline2', 'plugin_tree': plugin_tree}

        pipeline_serializer = PipelineSerializer(data=data)
        pipeline_serializer.is_valid(raise_exception=True)
        pipeline_serializer.validated_data['owner'] = owner
        pipeline = pipeline_serializer.create(pipeline_serializer.validated_data)
        pipeline_plg_names = [plugin.meta.name for plugin in pipeline.plugins.all()]
        self.assertIn(self.plugin_ds_name, pipeline_plg_names)
        self.assertIn("mri_analyze", pipeline_plg_names)

    def test_update(self):
        """
        Test whether overriden 'update' method successfully updates an existing pipeline
        even when 'validated_data' argument contains 'plugin_tree' and
        'plugin_inst_id'.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        validated_data = {'name': 'Pipeline2', 'plugin_tree': {'root_index': 0},
                          'plugin_inst_id': 1}
        pipeline_serializer.update(pipeline, validated_data)
        self.assertEqual(pipeline.name, 'Pipeline2')

    def test_validate_validates_required_fields_on_create(self):
        """
        Test whether overriden validate method validates that at least one of the fields
        'plugin_tree' or 'plugin_inst_id' must be provided when creating a new
        pipeline.
        """
        owner = User.objects.get(username=self.username)
        data = {'name': 'Pipeline2', 'owner': owner}
        pipeline_serializer = PipelineSerializer(data=data)
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate(data)

    def test_validate_validates_all_defaults_can_be_defined_if_pipeline_unlocked(self):
        """
        Test whether overriden validate method validates that all parameter defaults
        for the pipeline can be defined if the pipeline is unlocked.
        """
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a parameter without a default
        PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyFloat',
            type='float',
            optional=False
        )
        owner = User.objects.get(username=self.username)
        plugin_tree = {'root_index': 0, 'tree': [{'plugin_id': plugin_ds.id,
                                                  'child_indices': [],
                                                  'plugin_parameter_defaults': []}]}
        data = {'name': 'Pipeline2', 'owner': owner, 'plugin_tree': plugin_tree, 'locked': False}
        pipeline_serializer = PipelineSerializer(data=data)
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate(data)

    def test_validate_plugin_inst_id(self):
        """
        Test whether overriden validate_plugin_inst_id method validates that the plugin
        instance id corresponds to a plugin instance that is in the DB and is not of type
        'fs'.
        """
        owner = User.objects.get(username=self.username)
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_fs = Plugin.objects.get(meta__name=self.plugin_fs_name)
        (plugin_fs_inst, tf) = PluginInstance.objects.get_or_create(plugin=plugin_fs,
                                                                    owner=owner,
                                                compute_resource=self.compute_resource)
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate_plugin_inst_id(plugin_fs_inst.id + 1)
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate_plugin_inst_id(plugin_fs_inst.id)

    def test_validate_plugin_tree_is_json_string(self):
        """
        Test whether overriden validate_plugin_tree method validates that the plugin
        tree string is a proper JSON string.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        tree = '[{plugin_id: 8, "previous_index": null}]'
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate_plugin_tree(tree)

    def test_validate_plugin_tree_does_not_contain_empty_list(self):
        """
        Test whether overriden validate_plugin_tree method validates that the plugin
        tree is not an empty list.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        tree = '[]'
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate_plugin_tree(tree)

    def test_validate_plugin_tree_plugins_exist_and_not_fs(self):
        """
        Test whether overriden validate_plugin_tree method validates that the plugin
        tree contains existing plugins that are not of type 'fs'.
        """
        plugin_fs = Plugin.objects.get(meta__name=self.plugin_fs_name)
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        tree = '[{"plugin_id": ' + str(plugin_fs.id + 100) + ', "previous_index": null}]'
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate_plugin_tree(tree)
        tree = '[{"plugin_id": ' + str(plugin_fs.id) + ', "previous_index": null}]'
        with self.assertRaises(serializers.ValidationError):
            pipeline_serializer.validate_plugin_tree(tree)

    def test_validate_plugin_tree_raises_validation_error_if_get_tree_raises_value_error(self):
        """
        Test whether overriden validate_plugin_tree method raises ValidationError if
        internal call to get_tree method raises ValueError exception.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        tree = '[{"plugin_id": ' + str(plugin_ds.id) + ', "previous_index": null}]'
        with mock.patch('pipelines.serializers.PipelineSerializer.get_tree') as get_tree_mock:
            get_tree_mock.side_effect = ValueError
            with self.assertRaises(serializers.ValidationError):
                pipeline_serializer.validate_plugin_tree(tree)
            get_tree_mock.assert_called_with([{"plugin_id": plugin_ds.id,
                                               "plugin_parameter_defaults": [],
                                               "previous_index": None}])

    def test_validate_plugin_tree_raises_validation_error_if_validate_tree_raises_value_error(self):
        """
        Test whether overriden validate_plugin_tree method raises ValidationError if
        internal call to validate_tree method raises ValueError exception.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        tree = '[{"plugin_id": ' + str(plugin_ds.id) + ', "previous_index": null}]'
        tree_dict = {'root_index': 0, 'tree': [{"plugin_id": plugin_ds.id, "child_indices": []}]}
        with mock.patch('pipelines.serializers.PipelineSerializer.validate_tree') as validate_tree_mock:
            validate_tree_mock.side_effect = ValueError
            with self.assertRaises(serializers.ValidationError):
                pipeline_serializer.validate_plugin_tree(tree)
                validate_tree_mock.assert_called_with(tree_dict)

    def test_validate_plugin_parameter_defaults_raises_validation_error_if_missing_name_or_default(self):
        """
        Test whether custom validate_plugin_parameter_defaults method raises ValidationError if
        'name' or 'default' properties are not included.
        """
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        parameter_defaults = [{'name': 'dummyInt'}]
        with self.assertRaises(serializers.ValidationError):
            PipelineSerializer.validate_plugin_parameter_defaults(plugin_ds, parameter_defaults)
        parameter_defaults = [{'default': 3}]
        with self.assertRaises(serializers.ValidationError):
            PipelineSerializer.validate_plugin_parameter_defaults(plugin_ds, parameter_defaults)
        parameter_defaults = [{'name': 'dummyInt', 'default': 3}]
        PipelineSerializer.validate_plugin_parameter_defaults(plugin_ds, parameter_defaults)

    def test_validate_plugin_parameter_defaults_raises_validation_error_if_parameter_not_found(self):
        """
        Test whether custom validate_plugin_parameter_defaults method raises ValidationError if
        a parameter name is not found.
        """
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        parameter_defaults = [{'name': 'randomInt', 'default': 3}]
        with self.assertRaises(serializers.ValidationError):
            PipelineSerializer.validate_plugin_parameter_defaults(plugin_ds, parameter_defaults)

    def test_validate_plugin_parameter_defaults_raises_validation_error_if_invalid_default_value(self):
        """
        Test whether custom validate_plugin_parameter_defaults method raises ValidationError if
        an invalid default value is provided for a parameter.
        """
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        parameter_defaults = [{'name': 'dummyInt', 'default': True}]
        with self.assertRaises(serializers.ValidationError):
            PipelineSerializer.validate_plugin_parameter_defaults(plugin_ds, parameter_defaults)

    def test_get_tree(self):
        """
        Test whether custom get_tree method creates a proper dictionary tree from
        a tree list.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds1 = Plugin.objects.get(meta__name=self.plugin_ds_name)

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        tree_list = [{"plugin_id": plugin_ds1.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": None},
                {"plugin_id": plugin_ds2.id,
                 "plugin_parameter_defaults": [],
                 "previous_index": 0},
                {"plugin_id": plugin_ds1.id,
                 "plugin_parameter_defaults": [],
                 "previous_index": 1}]

        tree = [{"plugin_id": plugin_ds1.id,
                 "plugin_parameter_defaults": [],
                 "child_indices": [1]},
                {"plugin_id": plugin_ds2.id,
                 "plugin_parameter_defaults": [],
                 "child_indices": [2]},
                {"plugin_id": plugin_ds1.id,
                 "plugin_parameter_defaults": [],
                 "child_indices": []}]
        expected_tree_dict = {'root_index': 0, 'tree': tree}

        tree_dict = pipeline_serializer.get_tree(tree_list)
        self.assertEqual(tree_dict, expected_tree_dict)

    def test_get_tree_raises_value_error_if_no_root_found(self):
        """
        Test whether custom get_tree method raises ValueError if the passed tree list
        represents a tree with no root node.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds1 = Plugin.objects.get(meta__name=self.plugin_ds_name)

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        tree_list = [{"plugin_id": plugin_ds1.id, "previous_index": 0},
                {"plugin_id": plugin_ds2.id, "previous_index": 0},
                {"plugin_id": plugin_ds1.id, "previous_index": 1}]
        with self.assertRaises(ValueError):
            pipeline_serializer.get_tree(tree_list)

    def test_get_tree_raises_value_error_if_it_finds_invalid_previous_index(self):
        """
        Test whether custom get_tree method raises TypeError if the passed tree list
        has a node with None as previous index for a node that is not the root.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds1 = Plugin.objects.get(meta__name=self.plugin_ds_name)

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        tree_list = [{"plugin_id": plugin_ds1.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": None},
                     {"plugin_id": plugin_ds2.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": 0},
                     {"plugin_id": plugin_ds1.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": None}]
        with self.assertRaises(ValueError):
            pipeline_serializer.get_tree(tree_list)

        tree_list = [{"plugin_id": plugin_ds1.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": None},
                     {"plugin_id": plugin_ds2.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": 3},
                     {"plugin_id": plugin_ds1.id,
                      "plugin_parameter_defaults": [],
                      "previous_index": 1}]
        with self.assertRaises(ValueError):
            pipeline_serializer.get_tree(tree_list)

    def test_validate_tree(self):
        """
        Test whether custom validate_tree method raises ValueError if the passed
        dictionary represents a tree that is not connected.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds1 = Plugin.objects.get(meta__name=self.plugin_ds_name)

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        tree = [{"plugin_id": plugin_ds1.id, "child_indices": []},
                {"plugin_id": plugin_ds2.id, "child_indices": [2]},
                {"plugin_id": plugin_ds1.id, "child_indices": [1]}]
        tree_dict = {'root_index': 0, 'tree': tree}
        with self.assertRaises(ValueError):
            pipeline_serializer.validate_tree(tree_dict)

    def test__add_plugin_tree_to_pipeline(self):
        """
        Test whether custom internal _add_plugin_tree_to_pipeline method properly
        associates a tree of plugins to a pipeline in the DB.
        """
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        pipeline_serializer = PipelineSerializer(pipeline)
        plugin_ds1 = Plugin.objects.get(meta__name=self.plugin_ds_name)

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        tree = [{"plugin_id": plugin_ds1.id,
                 "plugin_parameter_defaults": [],
                 "child_indices": [1]},
                {"plugin_id": plugin_ds2.id,
                 "plugin_parameter_defaults": [],
                 "child_indices": [2]},
                {"plugin_id": plugin_ds1.id,
                 "plugin_parameter_defaults": [],
                 "child_indices": []}]
        tree_dict = {'root_index': 0, 'tree': tree}

        pipeline_serializer._add_plugin_tree_to_pipeline(pipeline, tree_dict)
        pipeline_plg_names = [plugin.meta.name for plugin in pipeline.plugins.all()]
        self.assertEqual(len(pipeline_plg_names), 3)
        self.assertEqual(len([name for name in pipeline_plg_names if name == self.plugin_ds_name]), 2)


# chris_backend/pipelines/tests/test_views.py


import logging
import json

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import status

from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter
from plugins.models import DefaultStrParameter, DefaultBoolParameter
from plugins.models import DefaultFloatParameter, DefaultIntParameter
from pipelines.models import Pipeline, PluginPiping


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ViewTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'

        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'dummyInt': {'type': 'integer', 'optional': True,
                                                  'default': 111111}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type=self.plugin_ds_parameters['dummyInt']['type'],
            optional=self.plugin_ds_parameters['dummyInt']['optional']
        )
        default = self.plugin_ds_parameters['dummyInt']['default']
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=default)  # set plugin parameter default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        (pipeline, tf) = Pipeline.objects.get_or_create(name=self.pipeline_name,
                                                        owner=user, category='test')

        # create two plugin pipings
        self.pips = []
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline)
        self.pips.append(pip)
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, previous=pip,
                                                       pipeline=pipeline)
        self.pips.append(pip)

        # create another user
        self.other_username = 'boo'
        self.other_password = 'far'
        User.objects.create_user(username=self.other_username,
                                 password=self.other_password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PipelineViewTests(ViewTests):
    """
    Generic tests for pipeline views.
    """

    def setUp(self):
        super(PipelineViewTests, self).setUp()


class PipelineListViewTests(PipelineViewTests):
    """
    Test the pipeline-list view.
    """

    def setUp(self):
        super(PipelineListViewTests, self).setUp()
        self.create_read_url = reverse("pipeline-list")

    def test_pipeline_create_success(self):
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds1, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds1.compute_resources.set([self.compute_resource])
        plugin_ds1.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        plugin_tree = '[{"plugin_id": ' + str(plugin_ds1.id) + \
                         ', "previous_index": null}, {"plugin_id": ' + \
                         str(plugin_ds2.id) + ', "previous_index": 0}]'
        post = json.dumps(
            {"template": {"data": [{"name": "name", "value": "Pipeline2"},
                                   {"name": "plugin_tree", "value": plugin_tree}]}})

        # make API request
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_pipeline_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "Pipeline1")

    def test_pipeline_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PipelineListQuerySearchViewTests(PipelineViewTests):
    """
    Test the pipeline-list-query-search view.
    """

    def setUp(self):
        super(PipelineListQuerySearchViewTests, self).setUp()
        self.list_url = reverse("pipeline-list-query-search") + '?name=Pipeline1'

    def test_plugin_list_query_search_success(self):
        owner = User.objects.get(username=self.username)
        Pipeline.objects.get_or_create(name='Pipeline2', owner=owner,
                                       category='test')
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "Pipeline1")
        self.assertNotContains(response, "Pipeline2")
        list_url = reverse("pipeline-list-query-search") + '?category=test'
        response = self.client.get(list_url)
        self.assertContains(response, "Pipeline1")
        self.assertContains(response, "Pipeline2")

    def test_plugin_list_query_search_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PipelineDetailViewTests(PipelineViewTests):
    """
    Test the pipeline-detail view.
    """

    def setUp(self):
        super(PipelineDetailViewTests, self).setUp()
        pipeline = Pipeline.objects.get(name="Pipeline1")

        self.read_update_delete_url = reverse("pipeline-detail", kwargs={"pk": pipeline.id})
        self.put = json.dumps(
            {"template": {"data": [{"name": "name", "value": "Pipeline2"}]}})

    def test_pipeline_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "Pipeline1")

    def test_pipeline_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pipeline_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "Pipeline2")

    def test_pipeline_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pipeline_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_delete_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_pipeline_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Pipeline.objects.count(), 0)

    def test_pipeline_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_pipeline_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class PipelinePluginListViewTests(PipelineViewTests):
    """
    Test the pipeline-plugin-list view.
    """

    def setUp(self):
        super(PipelinePluginListViewTests, self).setUp()
        self.pipeline = Pipeline.objects.get(name="Pipeline1")
        self.list_url = reverse("pipeline-plugin-list", kwargs={"pk": self.pipeline.id})

    def test_pipeline_plugin_list_success(self):
        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds1, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds1.compute_resources.set([self.compute_resource])
        plugin_ds1.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_analyze', type='ds')
        (plugin_ds2, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds2.compute_resources.set([self.compute_resource])
        plugin_ds2.save()

        # pipe one plugin into pipeline
        PluginPiping.objects.get_or_create(pipeline=self.pipeline, plugin=plugin_ds1,
                                           previous=None)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "mri_convert")
        self.assertNotContains(response, "mri_analyze")  # plugin list is pipe-specific

    def test_pipeline_plugin_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PipelinePluginPipingListViewTests(PipelineViewTests):
    """
    Test the pipeline-pluginpiping-list view.
    """

    def setUp(self):
        super(PipelinePluginPipingListViewTests, self).setUp()
        self.pipeline = Pipeline.objects.get(name="Pipeline1")
        self.list_url = reverse("pipeline-pluginpiping-list",
                                kwargs={"pk": self.pipeline.id})

    def test_pipeline_plugin_piping_list_success(self):
        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # pipe one plugin into pipeline
        PluginPiping.objects.get_or_create(pipeline=self.pipeline, plugin=plugin_ds,
                                           previous=None)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "plugin_id")

    def test_pipeline_plugin_piping_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PipelineDefaultParameterListViewTests(PipelineViewTests):
    """
    Test the pipeline-defaultparameter-list view.
    """

    def setUp(self):
        super(PipelineDefaultParameterListViewTests, self).setUp()
        self.pipeline = Pipeline.objects.get(name="Pipeline1")
        self.list_url = reverse("pipeline-defaultparameter-list",
                                kwargs={"pk": self.pipeline.id})

    def test_pipeline_default_parameter_list_success(self):
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        param = plugin_ds.parameters.get(name='dummyInt')
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, self.pips[0].id)
        self.assertContains(response, self.pips[1].id)
        self.assertContains(response, param.name)
        self.assertContains(response, plugin_ds.meta.name)
        self.assertContains(response, 111111)

    def test_pipeline_default_parameter_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginPipingDetailViewTests(PipelineViewTests):
    """
    Test the pluginpiping-detail view.
    """

    def setUp(self):
        super(PluginPipingDetailViewTests, self).setUp()

        self.read_url = reverse("pluginpiping-detail", kwargs={"pk": self.pips[0].id})

    def test_plugin_piping_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, "plugin_id")
        self.assertContains(response, "pipeline_id")

    def test_plugin_piping_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class DefaultPipingStrParameterDetailViewTests(ViewTests):
    """
    Test the defaultpipingstrparameter-detail view.
    """

    def setUp(self):
        super(DefaultPipingStrParameterDetailViewTests, self).setUp()

        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyStr',
            type='string',
            optional=True
        )
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value='test')  # set plugin parameter default
        pipeline = Pipeline.objects.get(name="Pipeline1")
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline, previous=self.pips[1])
        default_param = pip.string_param.get(plugin_piping=pip)
        self.read_update_url = reverse("defaultpipingstrparameter-detail", kwargs={"pk": default_param.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "value", "value": "updated"}]}})

    def test_default_piping_str_parameter_detail_success_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, "test")
        #self.assertTrue(response.data["feed"].endswith(self.corresponding_feed_url))

    def test_default_piping_str_parameter_detail_failure_access_denied_pipeline_locked(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_default_piping_str_parameter_detail_success_pipeline_unlocked(self):
        pipeline = Pipeline.objects.get(name="Pipeline1")
        pipeline.locked = False
        pipeline.save()
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, "test")

    def test_default_piping_str_parameter_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_str_parameter_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "updated")

    def test_default_piping_str_parameter_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_str_parameter_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class DefaultPipingIntParameterDetailViewTests(ViewTests):
    """
    Test the defaultpipingintparameter-detail view.
    """

    def setUp(self):
        super(DefaultPipingIntParameterDetailViewTests, self).setUp()

        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        pipeline = Pipeline.objects.get(name="Pipeline1")
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline, previous=self.pips[1])
        default_param = pip.integer_param.get(plugin_piping=pip)
        self.read_update_url = reverse("defaultpipingintparameter-detail", kwargs={"pk": default_param.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "value", "value": 222222}]}})

    def test_default_piping_int_parameter_detail_success_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, 111111)

    def test_default_piping_int_parameter_detail_failure_access_denied_pipeline_locked(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_default_piping_int_parameter_detail_success_pipeline_unlocked(self):
        pipeline = Pipeline.objects.get(name="Pipeline1")
        pipeline.locked = False
        pipeline.save()
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, 111111)

    def test_default_piping_int_parameter_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_int_parameter_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, 222222)

    def test_default_piping_int_parameter_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_int_parameter_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class DefaultPipingFloatParameterDetailViewTests(ViewTests):
    """
    Test the defaultpipingfloatparameter-detail view.
    """

    def setUp(self):
        super(DefaultPipingFloatParameterDetailViewTests, self).setUp()

        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyFloat',
            type='float',
            optional=True
        )
        DefaultFloatParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=1.11111)  # set plugin parameter default
        pipeline = Pipeline.objects.get(name="Pipeline1")
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline, previous=self.pips[1])
        default_param = pip.float_param.get(plugin_piping=pip)
        self.read_update_url = reverse("defaultpipingfloatparameter-detail", kwargs={"pk": default_param.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "value", "value": 1.22222}]}})

    def test_default_piping_float_parameter_detail_success_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, 1.11111)

    def test_default_piping_float_parameter_detail_failure_access_denied_pipeline_locked(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_default_piping_float_parameter_detail_success_pipeline_unlocked(self):
        pipeline = Pipeline.objects.get(name="Pipeline1")
        pipeline.locked = False
        pipeline.save()
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, 1.11111)

    def test_default_piping_float_parameter_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_float_parameter_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, 1.22222)

    def test_default_piping_float_parameter_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_float_parameter_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class DefaultPipingBoolParameterDetailViewTests(ViewTests):
    """
    Test the defaultpipingboolparameter-detail view.
    """

    def setUp(self):
        super(DefaultPipingBoolParameterDetailViewTests, self).setUp()

        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyBool',
            type='boolean',
            optional=True
        )
        DefaultBoolParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=False)  # set plugin parameter default
        pipeline = Pipeline.objects.get(name="Pipeline1")
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline, previous=self.pips[1])
        default_param = pip.boolean_param.get(plugin_piping=pip)
        self.read_update_url = reverse("defaultpipingboolparameter-detail", kwargs={"pk": default_param.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "value", "value": "true"}]}})

    def test_default_piping_bool_parameter_detail_success_owner(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, "false")

    def test_default_piping_bool_parameter_detail_failure_access_denied_pipeline_locked(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_default_piping_bool_parameter_detail_success_pipeline_unlocked(self):
        pipeline = Pipeline.objects.get(name="Pipeline1")
        pipeline.locked = False
        pipeline.save()
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, "false")

    def test_default_piping_bool_parameter_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_bool_parameter_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, "true")

    def test_default_piping_bool_parameter_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_default_piping_bool_parameter_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


# chris_backend/plugininstances/migrations/0001_initial.py

# Generated by Django 2.1.4 on 2019-01-16 19:23

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plugins.fields


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('feeds', '0008_auto_20190114_1437'),
        ('plugins', '0024_auto_20190116_1917'),
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='BoolParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.BooleanField(blank=True, default=False)),
            ],
        ),
        migrations.CreateModel(
            name='FloatParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.FloatField()),
            ],
        ),
        migrations.CreateModel(
            name='IntParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.IntegerField()),
            ],
        ),
        migrations.CreateModel(
            name='PathParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
            ],
        ),
        migrations.CreateModel(
            name='PluginInstance',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(blank=True, max_length=100)),
                ('start_date', models.DateTimeField(auto_now_add=True)),
                ('end_date', models.DateTimeField(auto_now_add=True)),
                ('status', models.CharField(default='started', max_length=30)),
                ('cpu_limit', plugins.fields.CPUField(null=True)),
                ('memory_limit', plugins.fields.MemoryField(null=True)),
                ('number_of_workers', models.IntegerField(null=True)),
                ('gpu_limit', models.IntegerField(null=True)),
                ('compute_resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugin_instances', to='plugins.ComputeResource')),
                ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugin_instances', to='feeds.Feed')),
                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
                ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='plugins.Plugin')),
                ('previous', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='next', to='plugininstances.PluginInstance')),
            ],
            options={
                'ordering': ('-start_date',),
            },
        ),
        migrations.CreateModel(
            name='PluginInstanceFile',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('fname', models.FileField(max_length=2048, upload_to='')),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='plugininstances.PluginInstance')),
            ],
            options={
                'ordering': ('fname',),
            },
        ),
        migrations.CreateModel(
            name='StringParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_param', to='plugininstances.PluginInstance')),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_inst', to='plugins.PluginParameter')),
            ],
        ),
        migrations.AddField(
            model_name='pathparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_param', to='plugininstances.PluginInstance'),
        ),
        migrations.AddField(
            model_name='pathparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_inst', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='intparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_param', to='plugininstances.PluginInstance'),
        ),
        migrations.AddField(
            model_name='intparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_inst', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='floatparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_param', to='plugininstances.PluginInstance'),
        ),
        migrations.AddField(
            model_name='floatparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_inst', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='boolparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_param', to='plugininstances.PluginInstance'),
        ),
        migrations.AddField(
            model_name='boolparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_inst', to='plugins.PluginParameter'),
        ),
    ]


# chris_backend/plugininstances/migrations/0002_auto_20190201_1940.py

# Generated by Django 2.1.4 on 2019-02-01 19:40

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0026_auto_20190128_1615'),
        ('plugininstances', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='PipelineInstance',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(blank=True, max_length=100)),
                ('description', models.CharField(blank=True, max_length=800)),
                ('pipeline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='plugins.Pipeline')),
            ],
            options={
                'ordering': ('pipeline',),
            },
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='pipeline_inst',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plugin_instances', to='plugininstances.PipelineInstance'),
        ),
    ]


# chris_backend/plugininstances/migrations/0003_auto_20190212_1818.py

# Generated by Django 2.1.4 on 2019-02-12 18:18

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0027_auto_20190212_1818'),
        ('plugininstances', '0002_auto_20190201_1940'),
    ]

    operations = [
        migrations.RenameModel(
            old_name='StringParameter',
            new_name='StrParameter',
        ),
    ]


# chris_backend/plugininstances/migrations/0004_auto_20190213_2320.py

# Generated by Django 2.1.4 on 2019-02-13 23:20

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0003_auto_20190212_1818'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='pipeline_inst',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_instances', to='plugininstances.PipelineInstance'),
        ),
    ]


# chris_backend/plugininstances/migrations/0005_auto_20190221_1803.py

# Generated by Django 2.1.4 on 2019-02-21 18:03

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0004_auto_20190213_2320'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='pipelineinstance',
            name='pipeline',
        ),
        migrations.RemoveField(
            model_name='plugininstance',
            name='pipeline_inst',
        ),
        migrations.DeleteModel(
            name='PipelineInstance',
        ),
    ]


# chris_backend/plugininstances/migrations/0006_plugininstance_pipeline_inst.py

# Generated by Django 2.1.4 on 2019-02-21 18:03

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('pipelineinstances', '0001_initial'),
        ('plugininstances', '0005_auto_20190221_1803'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugininstance',
            name='pipeline_inst',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_instances', to='pipelineinstances.PipelineInstance'),
        ),
    ]


# chris_backend/plugininstances/migrations/0007_auto_20190222_1532.py

# Generated by Django 2.1.4 on 2019-02-22 15:32

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0006_plugininstance_pipeline_inst'),
    ]

    operations = [
        migrations.AlterField(
            model_name='boolparameter',
            name='value',
            field=models.BooleanField(),
        ),
    ]


# chris_backend/plugininstances/migrations/0008_auto_20190524_1700.py

# Generated by Django 2.1.4 on 2019-05-24 17:00

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0029_auto_20190423_1448'),
        ('plugininstances', '0007_auto_20190222_1532'),
    ]

    operations = [
        migrations.AlterUniqueTogether(
            name='boolparameter',
            unique_together={('plugin_inst', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='floatparameter',
            unique_together={('plugin_inst', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='intparameter',
            unique_together={('plugin_inst', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='pathparameter',
            unique_together={('plugin_inst', 'plugin_param')},
        ),
        migrations.AlterUniqueTogether(
            name='strparameter',
            unique_together={('plugin_inst', 'plugin_param')},
        ),
    ]


# chris_backend/plugininstances/migrations/0009_auto_20190715_1706.py

# Generated by Django 2.1.4 on 2019-07-15 17:06

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0008_auto_20190524_1700'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='plugininstancefile',
            options={'ordering': ('-fname',)},
        ),
    ]


# chris_backend/plugininstances/migrations/0010_auto_20200131_1619.py

# Generated by Django 2.1.4 on 2020-01-31 16:19

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0009_auto_20190715_1706'),
    ]

    operations = [
        migrations.AlterField(
            model_name='pathparameter',
            name='value',
            field=models.CharField(max_length=200),
        ),
    ]


# chris_backend/plugininstances/migrations/0011_auto_20200211_1555.py

# Generated by Django 2.1.4 on 2020-02-11 15:55

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0010_auto_20200131_1619'),
    ]

    operations = [
        migrations.AlterField(
            model_name='pathparameter',
            name='value',
            field=models.CharField(max_length=16000),
        ),
        migrations.AlterField(
            model_name='plugininstancefile',
            name='fname',
            field=models.FileField(max_length=4000, upload_to=''),
        ),
        migrations.AlterField(
            model_name='strparameter',
            name='value',
            field=models.CharField(blank=True, max_length=600),
        ),
    ]


# chris_backend/plugininstances/migrations/0012_auto_20200213_0040.py

# Generated by Django 2.1.4 on 2020-02-13 00:40

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0032_auto_20200213_0040'),
        ('plugininstances', '0011_auto_20200211_1555'),
    ]

    operations = [
        migrations.CreateModel(
            name='UnextpathParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(max_length=16000)),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unextpath_param', to='plugininstances.PluginInstance')),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unextpath_inst', to='plugins.PluginParameter')),
            ],
        ),
        migrations.AlterUniqueTogether(
            name='unextpathparameter',
            unique_together={('plugin_inst', 'plugin_param')},
        ),
    ]


# chris_backend/plugininstances/migrations/0013_auto_20200516_1543.py

# Generated by Django 2.2.12 on 2020-05-16 19:43

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0012_auto_20200213_0040'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='compute_resource',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_instances', to='plugins.ComputeResource'),
        ),
    ]


# chris_backend/plugininstances/migrations/0014_auto_20200610_1922.py

# Generated by Django 2.2.12 on 2020-06-10 23:22

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0013_auto_20200516_1543'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugininstance',
            name='raw',
            field=models.TextField(blank=True, default=''),
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='summary',
            field=models.CharField(blank=True, default='', max_length=4000),
        ),
    ]


# chris_backend/plugininstances/migrations/0015_auto_20200925_2001.py

# Generated by Django 2.2.12 on 2020-09-26 00:01

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0014_auto_20200610_1922'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='status',
            field=models.CharField(choices=[('created', 'Default initial'), ('waitingForPrevious', 'Waiting for previous to finish'), ('scheduled', 'Scheduled to the worker'), ('started', 'Sent to remote compute'), ('finishedSuccessfully', 'Finished successfully'), ('finishedWithError', 'Finished with error'), ('cancelled', 'Cancelled')], default='created', max_length=30),
        ),
    ]


# chris_backend/plugininstances/migrations/0016_auto_20201006_1309.py

# Generated by Django 2.2.12 on 2020-10-06 17:09

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0015_auto_20200925_2001'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='status',
            field=models.CharField(choices=[('created', 'Default initial'), ('waitingForPrevious', 'Waiting for previous to finish'), ('scheduled', 'Scheduled to the worker'), ('started', 'Sent to remote compute'), ('registeringFiles', 'Registering output files'), ('finishedSuccessfully', 'Finished successfully'), ('finishedWithError', 'Finished with error'), ('cancelled', 'Cancelled')], default='created', max_length=30),
        ),
        migrations.AlterField(
            model_name='plugininstancefile',
            name='fname',
            field=models.FileField(max_length=1024, unique=True, upload_to=''),
        ),
    ]


# chris_backend/plugininstances/migrations/0017_auto_20201203_1415.py

# Generated by Django 2.2.12 on 2020-12-03 19:15

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0016_auto_20201006_1309'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='status',
            field=models.CharField(choices=[('created', 'Default initial'), ('waiting', 'Waiting to be scheduled'), ('scheduled', 'Scheduled to the worker'), ('started', 'Sent to remote compute'), ('registeringFiles', 'Registering output files'), ('finishedSuccessfully', 'Finished successfully'), ('finishedWithError', 'Finished with error'), ('cancelled', 'Cancelled')], default='created', max_length=30),
        ),
    ]


# chris_backend/plugininstances/migrations/0018_plugininstancelock.py

# Generated by Django 2.2.12 on 2020-12-16 21:54

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0017_auto_20201203_1415'),
    ]

    operations = [
        migrations.CreateModel(
            name='PluginInstanceLock',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('plugin_inst', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='lock', to='plugininstances.PluginInstance')),
            ],
        ),
    ]


# chris_backend/plugininstances/migrations/0019_auto_20210209_1311.py

# Generated by Django 2.2.12 on 2021-02-09 18:11

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0018_plugininstancelock'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='status',
            field=models.CharField(choices=[('created', 'Default initial'), ('waiting', 'Waiting to be scheduled'), ('scheduled', 'Scheduled on worker'), ('started', 'Started on compute env'), ('registeringFiles', 'Registering output files'), ('finishedSuccessfully', 'Finished successfully'), ('finishedWithError', 'Finished with error'), ('cancelled', 'Cancelled')], default='created', max_length=30),
        ),
    ]


# chris_backend/plugininstances/migrations/0020_plugininstancesplit.py

# Generated by Django 2.2.12 on 2021-02-10 04:24

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0019_auto_20210209_1311'),
    ]

    operations = [
        migrations.CreateModel(
            name='PluginInstanceSplit',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('filter', models.CharField(blank=True, max_length=600)),
                ('created_plugin_inst_ids', models.CharField(max_length=600)),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='splits', to='plugininstances.PluginInstance')),
            ],
            options={
                'ordering': ('plugin_inst', '-creation_date'),
            },
        ),
    ]


# chris_backend/plugininstances/migrations/0021_plugininstance_error_code.py

# Generated by Django 2.2.12 on 2021-02-22 18:05

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0020_plugininstancesplit'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugininstance',
            name='error_code',
            field=models.CharField(blank=True, max_length=7),
        ),
    ]


# chris_backend/plugininstances/migrations/0022_auto_20211119_1857.py

# Generated by Django 2.2.24 on 2021-11-19 23:57

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0021_plugininstance_error_code'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugininstance',
            name='size',
            field=models.IntegerField(default=0),
        ),
        migrations.AlterField(
            model_name='plugininstance',
            name='raw',
            field=models.TextField(blank=True),
        ),
        migrations.AlterField(
            model_name='plugininstance',
            name='summary',
            field=models.CharField(blank=True, max_length=4000),
        ),
    ]


# chris_backend/plugininstances/migrations/0023_auto_20220211_1937.py

# Generated by Django 2.2.24 on 2022-02-12 00:37

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0022_auto_20211119_1857'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='size',
            field=models.BigIntegerField(default=0),
        ),
    ]


# chris_backend/plugininstances/services/manager.py

"""
Plugin instance app manager module that provides functionality to run and check the
execution status of a plugin instance's app (ChRIS / pfcon interface).

NOTE:

    This module is executed as part of an asynchronous celery worker.
    For instance, to debug 'check_plugin_instance_app_exec_status' method synchronously
    with pudb.set_trace() you need to:

    1. Once CUBE is running, and assuming some plugininstance has been POSTed, start a
    python shell on the manage.py code (note <IMAGE> below is the chris:dev container):

    docker exec -ti <IMAGE> python manage.py shell

    You should now be in a python shell.

    2. To simulate operations on a given plugin with id <id>,
    instantiate the relevant objects (for ex, for id=1):

    from plugininstances.models import PluginInstance
    from plugininstances.services import manager

    plg_inst = PluginInstance.objects.get(id=1)
    plg_inst_manager = manager.PluginInstanceManager(plg_inst)

    3. And finally, call the method:

    plg_inst_manager.check_plugin_instance_app_exec_status()

    Any pudb.set_trace() calls in this method will now be handled by the pudb debugger.

    4. Finally, after each change to this method, reload this module:

    import importlib
    importlib.reload(manager)

    and also re-instantiate the service:

    plg_inst_manager = manager.PluginInstanceManager(plg_inst)
"""

import logging
import os
import re
import io
import time
import json
import zipfile
from pfconclient import client as pfcon
from pfconclient.exceptions import (PfconRequestException,
                                    PfconRequestInvalidTokenException)

from django.utils import timezone
from django.conf import settings
from django.db.utils import IntegrityError

from core.swiftmanager import SwiftManager, ClientException
from core.utils import json_zip2str
from core.models import ChrisInstance
from plugininstances.models import PluginInstance, PluginInstanceFile, PluginInstanceLock

if settings.DEBUG:
    import pdb, pudb, rpudb
    from celery.contrib import rdb


logger = logging.getLogger(__name__)


class PluginInstanceManager(object):

    def __init__(self, plugin_instance):

        self.c_plugin_inst = plugin_instance

        self.l_plugin_inst_param_instances = self.c_plugin_inst.get_parameter_instances()

        self.str_job_id = ChrisInstance.load().job_id_prefix + str(plugin_instance.id)

        cr = self.c_plugin_inst.compute_resource
        self.pfcon_client = pfcon.Client(cr.compute_url, cr.compute_auth_token)

        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                          settings.SWIFT_CONNECTION_PARAMS)

    def _refresh_compute_resource_auth_token(self):
        """
        Get a new auth token from a remote pfcon service and update the DB.
        """
        cr = self.c_plugin_inst.compute_resource
        token = pfcon.Client.get_auth_token(cr.compute_auth_url, cr.compute_user,
                                            cr.compute_password)
        self.pfcon_client.set_auth_token(token)
        cr.compute_auth_token = token
        cr.save()

    def run_plugin_instance_app(self):
        """
        Run the plugin instance's app via a call to a remote pfcon service.
        """
        if self.c_plugin_inst.status == 'cancelled':
            return

        plugin = self.c_plugin_inst.plugin
        plugin_type = plugin.meta.type
        inputdirs = []
        try:
            if plugin_type == 'ds':
                inputdirs.append(self.get_previous_output_path())
            else:
                inputdirs.append(self.manage_plugin_instance_app_empty_inputdir())
        except Exception:
            self.c_plugin_inst.status = 'cancelled'  # giving up
            self.save_plugin_instance_final_status()
            return

        d_unextpath_params, d_path_params = self.get_plugin_instance_path_parameters()
        for path_param_value in [param_value for param_value in d_path_params.values()]:
            # the value of each parameter of type 'path' is a string
            # representing a comma-separated list of paths in obj storage
            inputdirs = inputdirs + path_param_value.split(',')

        # create data file to transmit
        try:
            zip_file = self.create_zip_file(inputdirs)
        except Exception:
            self.c_plugin_inst.status = 'cancelled'  # giving up
            self.save_plugin_instance_final_status()
            return

        # create job description dictionary
        cmd_args = self.get_plugin_instance_app_cmd_args()
        cmd_path_flags = list(d_unextpath_params.keys()) + list(d_path_params.keys())
        job_descriptors = {
            'cmd_args': ' '.join(cmd_args),
            'cmd_path_flags': ','.join(cmd_path_flags),
            'auid': self.c_plugin_inst.owner.username,
            'number_of_workers': self.c_plugin_inst.number_of_workers,
            'cpu_limit': self.c_plugin_inst.cpu_limit,
            'memory_limit': self.c_plugin_inst.memory_limit,
            'gpu_limit': self.c_plugin_inst.gpu_limit,
            'image': plugin.dock_image,
            'selfexec': plugin.selfexec,
            'selfpath': plugin.selfpath,
            'execshell': plugin.execshell,
            'type': plugin_type
        }
        pfcon_url = self.pfcon_client.url
        job_id = self.str_job_id
        logger.info(f'Submitting job {job_id} to pfcon url -->{pfcon_url}<--, '
                    f'description: {json.dumps(job_descriptors, indent=4)}')
        try:
            d_resp = self._submit_job(job_id, job_descriptors, zip_file.getvalue())
        except PfconRequestException as e:
            logger.error(f'[CODE01,{job_id}]: Error submitting job to pfcon url '
                         f'-->{pfcon_url}<--, detail: {str(e)}')
            self.c_plugin_inst.error_code = 'CODE01'
            self.c_plugin_inst.status = 'cancelled'  # giving up
            self.save_plugin_instance_final_status()
        else:
            logger.info(f'Successfully submitted job {job_id} to pfcon url '
                        f'-->{pfcon_url}<--, response: {json.dumps(d_resp, indent=4)}')
            # update the job status and summary
            self.c_plugin_inst.status = 'started'
            self.c_plugin_inst.summary = self.get_job_status_summary()  # initial status
            self.c_plugin_inst.raw = json_zip2str(d_resp)
            self.c_plugin_inst.save()

    def _submit_job(self, job_id, job_descriptors, dfile, timeout=9000):
        """
        Submit job to a remote pfcon service.
        """
        try:
            d_resp = self.pfcon_client.submit_job(job_id, job_descriptors, dfile, timeout)
        except PfconRequestInvalidTokenException:
            logger.info(f'Auth token has expired while submitting job {job_id} to pfcon '
                        f'url -->{self.pfcon_client.url}<--')
            self._refresh_compute_resource_auth_token()
            d_resp = self.pfcon_client.submit_job(job_id, job_descriptors, dfile, timeout)
        return d_resp

    def check_plugin_instance_app_exec_status(self):
        """
        Check a plugin instance's app execution status. If the associated job's
        execution time exceeds the maximum set for the remote compute environment then
        the job is cancelled. Otherwise the job's execution status is fetched from the
        remote and if finished without error then the job's zip file is downloaded and
        unpacked and the output files registered with the DB. Finally a delete request
        is made to remove the job from the remote environment.
        """
        if self.c_plugin_inst.status == 'started':
            job_id = self.str_job_id

            delta_exec_time = timezone.now() - self.c_plugin_inst.start_date
            delta_seconds = delta_exec_time.total_seconds()
            max_exec_seconds = self.c_plugin_inst.compute_resource.max_job_exec_seconds
            if delta_seconds > max_exec_seconds:
                logger.error(f'[CODE13,{job_id}]: Error, job exceeded maximum execution '
                             f'time ({max_exec_seconds} seconds)')
                self.c_plugin_inst.error_code = 'CODE13'
                self.cancel_plugin_instance_app_exec()
                return self.c_plugin_inst.status

            pfcon_url = self.pfcon_client.url
            logger.info(f'Sending job status request to pfcon url -->{pfcon_url}<-- for '
                        f'job {job_id}')
            try:
                d_resp = self._get_job_status(job_id)
            except PfconRequestException as e:
                logger.error(f'[CODE02,{job_id}]: Error getting job status at pfcon '
                             f'url -->{pfcon_url}<--, detail: {str(e)}')
                return self.c_plugin_inst.status  # return, CUBE will retry later

            logger.info(f'Successful job status response from pfcon url -->{pfcon_url}<--'
                        f' for job {job_id}: {json.dumps(d_resp, indent=4)}')
            status = d_resp['compute']['status']
            logger.info(f'Current job {job_id} remote status = {status}')
            logger.info(f'Current job {job_id} DB status = {self.c_plugin_inst.status}')

            summary = self.get_job_status_summary(d_resp)
            self.c_plugin_inst.summary = summary
            raw = json_zip2str(d_resp)
            self.c_plugin_inst.raw = raw
            # only update (atomically) if status='started' to avoid concurrency problems
            PluginInstance.objects.filter(
                id=self.c_plugin_inst.id,
                status='started').update(summary=summary, raw=raw)

            if status == 'finishedSuccessfully':
                self._handle_finished_successfully_status()
            elif status == 'finishedWithError':
                self._handle_finished_with_error_status()
            elif status == 'undefined':
                self._handle_undefined_status()
        return self.c_plugin_inst.status

    def _get_job_status(self, job_id, timeout=200):
        """
        Get job status from a remote pfcon service.
        """
        try:
            d_resp = self.pfcon_client.get_job_status(job_id, timeout)
        except PfconRequestInvalidTokenException:
            logger.info(f'Auth token has expired while getting status for job {job_id} '
                        f'from pfcon url -->{self.pfcon_client.url}<--')
            self._refresh_compute_resource_auth_token()
            d_resp = self.pfcon_client.get_job_status(job_id, timeout)
        return d_resp

    def cancel_plugin_instance_app_exec(self):
        """
        Cancel a plugin instance's app execution. It connects to the remote service
        to cancel job.
        """
        self.c_plugin_inst.status = 'cancelled'
        self.delete_plugin_instance_job_from_remote()
        self.save_plugin_instance_final_status()

    def delete_plugin_instance_job_from_remote(self):
        """
        Delete a plugin instance's app job from the remote compute. It connects to the
        remote service to delete job.
        """
        pfcon_url = self.pfcon_client.url
        job_id = self.str_job_id
        logger.info(f'Deleting job {job_id} from pfcon at url '
                    f'-->{pfcon_url}<--')
        try:
            self._delete_job(job_id)
        except PfconRequestException as e:
            logger.error(f'[CODE12,{job_id}]: Error deleting job from '
                         f'pfcon at url -->{pfcon_url}<--, detail: {str(e)}')
        else:
            logger.info(f'Successfully deleted job {job_id} from pfcon at '
                        f'url -->{pfcon_url}<--')

    def _delete_job(self, job_id, timeout=500):
        """
        Delete a job from a remote pfcon service.
        """
        try:
            self.pfcon_client.delete_job(job_id, timeout)
        except PfconRequestInvalidTokenException:
            logger.info(f'Auth token has expired while requesting to delete job {job_id} '
                        f'from pfcon url -->{self.pfcon_client.url}<--')
            self._refresh_compute_resource_auth_token()
            self.pfcon_client.delete_job(job_id, timeout)

    def get_previous_output_path(self):
        """
        Get the previous plugin instance output directory. Make sure to deal with
        the eventual consistency.
        """
        job_id = self.str_job_id
        previous = self.c_plugin_inst.previous
        output_path = previous.get_output_path()
        fnames = [f.fname.name for f in previous.files.all()]
        for i in range(20):  # loop to deal with eventual consistency
            try:
                l_ls = self.swift_manager.ls(output_path)
            except ClientException as e:
                logger.error(f'[CODE06,{job_id}]: Error while listing swift '
                             f'storage files in {output_path}, detail: {str(e)}')
            else:
                if all(obj in l_ls for obj in fnames):
                    return output_path
            time.sleep(3)
        logger.error(f'[CODE11,{job_id}]: Error while listing swift storage files in '
                     f'{output_path}, detail: Presumable eventual consistency problem')
        self.c_plugin_inst.error_code = 'CODE11'
        raise NameError('Presumable eventual consistency problem.')

    def get_plugin_instance_app_cmd_args(self):
        """
        Get the list of the plugin instance app's cmd arguments.
        """
        # append flags to save input meta data (passed options) and
        # output meta data (output description)
        app_args = ['--saveinputmeta', '--saveoutputmeta']
        # append the parameters to app's argument list
        for param_inst in self.l_plugin_inst_param_instances:
            param = param_inst.plugin_param
            value = param_inst.value
            if param.action == 'store':
                app_args.append(param.flag)
                if param.type == 'string' and not value:
                    value = "''"  # handle empty string as a valid value for a flag
                app_args.append(str(value))  # convert all argument values to string
            elif param.action == 'store_true' and value:
                app_args.append(param.flag)
            elif param.action == 'store_false' and not value:
                app_args.append(param.flag)
        return app_args

    def get_plugin_instance_path_parameters(self):
        """
        Get the unextpath and path parameters dictionaries in a tuple. The keys and
        values in these dictionaries are parameters' flag and value respectively.
        """
        path_parameters_dict = {}
        unextpath_parameters_dict = {}
        for param_inst in self.l_plugin_inst_param_instances:
            param = param_inst.plugin_param
            value = param_inst.value
            if param.type == 'unextpath':
                unextpath_parameters_dict[param.flag] = value
            if param.type == 'path':
                path_parameters_dict[param.flag] = value
        return unextpath_parameters_dict, path_parameters_dict

    def get_ts_plugin_instance_input_objs(self):
        """
        Get a tuple whose first element is a dictionary with keys that are the ids of
        each input plugin instance to this 'ts' plugin instance. The values of
        this dictionary are also dictionaries containing the output dir of the plugin
        instances and the list of all the objects under the output dir that match a
        regular expression. The second element of the tuple indicates the value of the
        'groupByInstance' flag for this 'ts' plugin instance.
        """
        job_id = self.str_job_id
        # extract the 'ts' plugin's special parameters from the DB
        plg_inst_ids = regexs = []
        group_by_instance = False
        if self.c_plugin_inst.plugin.meta.type == 'ts':
            for param_inst in self.l_plugin_inst_param_instances:
                if param_inst.plugin_param.name == 'plugininstances':
                    # string param that represents a comma-separated list of ids
                    plg_inst_ids = param_inst.value.split(',') if param_inst.value else []
                elif param_inst.plugin_param.name == 'filter':
                    # string param that represents a comma-separated list of regular expr
                    regexs = param_inst.value.split(',') if param_inst.value else []
                elif param_inst.plugin_param.name == 'groupByInstance':
                    group_by_instance = param_inst.value
        d_objs = {}
        for i, inst_id in enumerate(plg_inst_ids):
            try:
                plg_inst = PluginInstance.objects.get(pk=int(inst_id))
            except PluginInstance.DoesNotExist:
                logger.error(f"[CODE05,{job_id}]: Couldn't find any plugin instance with "
                             f"id {inst_id} while processing input instances to 'ts' "
                             f"plugin instance with id {self.c_plugin_inst.id}")
                self.c_plugin_inst.error_code = 'CODE05'
                raise
            output_path = plg_inst.get_output_path()
            try:
                l_ls = self.swift_manager.ls(output_path)
            except ClientException as e:
                logger.error(f'[CODE06,{job_id}]: Error while listing swift '
                             f'storage files in {output_path}, detail: {str(e)}')
                self.c_plugin_inst.error_code = 'CODE06'
                raise
            if (i < len(regexs)) and regexs[i]:
                r = re.compile(regexs[i])
                d_objs[plg_inst.id] = {'output_path': output_path,
                                       'objs': [obj for obj in l_ls if r.search(obj)]}
            else:
                d_objs[plg_inst.id] = {'output_path': output_path,
                                       'objs': l_ls}
        return d_objs, group_by_instance

    def manage_plugin_instance_app_empty_inputdir(self):
        """
        This method is responsible for managing the 'inputdir' in the case of
        FS and TS plugins. FS and TS plugins do not have an inputdir spec, since this
        is only a requirement for DS plugins. Nonetheless, the remote services do
        require some non-zero inputdir spec in order to operate correctly.

        The hack here is to store data somewhere in swift and accessing it as a
        "pseudo" inputdir for FS and TS plugins. We create a "dummy" inputdir with a
        small dummy text file in swift storage. This is then transmitted as an 'inputdir'
        to the compute environment and can be completely ignored by the plugin.
        """
        job_id = self.str_job_id
        data_dir = os.path.join(os.path.expanduser("~"), 'data')
        str_inputdir = os.path.join(data_dir, 'squashEmptyDir').lstrip('/')
        str_squashFile = os.path.join(str_inputdir, 'squashEmptyDir.txt')
        str_squashMsg = 'Empty input dir.'
        try:
            if not self.swift_manager.obj_exists(str_squashFile):
                with io.StringIO(str_squashMsg) as f:
                    self.swift_manager.upload_obj(str_squashFile, f.read(),
                                                  content_type='text/plain')
        except ClientException as e:
            logger.error(f'[CODE07,{job_id}]: Error while uploading file '
                         f'{str_squashFile} to swift storage, detail: {str(e)}')
            self.c_plugin_inst.error_code = 'CODE07'
            raise
        return str_inputdir

    def create_zip_file(self, swift_paths):
        """
        Create job zip file ready for transmission to the remote from a list of swift
        storage paths (prefixes).
        """
        job_id = self.str_job_id
        memory_zip_file = io.BytesIO()
        with zipfile.ZipFile(memory_zip_file, 'w', zipfile.ZIP_DEFLATED) as job_data_zip:
            for swift_path in swift_paths:
                try:
                    l_ls = self.swift_manager.ls(swift_path)
                except ClientException as e:
                    logger.error(f'[CODE06,{job_id}]: Error while listing swift '
                                 f'storage files in {swift_path}, detail: {str(e)}')
                    self.c_plugin_inst.error_code = 'CODE06'
                    raise
                for obj_path in l_ls:
                    try:
                        contents = self.swift_manager.download_obj(obj_path)
                    except ClientException as e:
                        logger.error(f'[CODE08,{job_id}]: Error while downloading file '
                                     f'{obj_path} from swift storage, detail: {str(e)}')
                        self.c_plugin_inst.error_code = 'CODE08'
                        raise
                    zip_path = obj_path.replace(swift_path, '', 1).lstrip('/')
                    job_data_zip.writestr(zip_path, contents)
        memory_zip_file.seek(0)
        return memory_zip_file

    def unpack_zip_file(self, zip_file_content):
        """
        Unpack job zip file from the remote into swift storage and register the
        extracted files with the DB.
        """
        job_id = self.str_job_id
        swift_filenames = []
        try:
            memory_zip_file = io.BytesIO(zip_file_content)
            with zipfile.ZipFile(memory_zip_file, 'r', zipfile.ZIP_DEFLATED) as job_zip:
                filenames = job_zip.namelist()
                logger.info(f'{len(filenames)} files to decompress for job {job_id}')
                output_path = self.c_plugin_inst.get_output_path() + '/'
                for fname in filenames:
                    content = job_zip.read(fname)
                    swift_fname = output_path + fname.lstrip('/')
                    try:
                        self.swift_manager.upload_obj(swift_fname, content)
                    except ClientException as e:
                        logger.error(f'[CODE07,{job_id}]: Error while uploading file '
                                     f'{swift_fname} to swift storage, detail: {str(e)}')
                        self.c_plugin_inst.error_code = 'CODE07'
                        raise
                    swift_filenames.append(swift_fname)
        except ClientException:
            raise
        except Exception as e:
            logger.error(f'[CODE04,{job_id}]: Received bad zip file from remote, '
                         f'detail: {str(e)}')
            self.c_plugin_inst.error_code = 'CODE04'
            raise
        self._register_output_files(swift_filenames)

    def save_plugin_instance_final_status(self):
        """
        Save to the DB and log the final status of the plugin instance.
        """
        job_id = self.str_job_id
        logger.info(f"Saving job {job_id} DB status as '{self.c_plugin_inst.status}'")
        self.c_plugin_inst.end_date = timezone.now()
        logger.info(f"Saving job {job_id} DB end_date as '{self.c_plugin_inst.end_date}'")
        self.c_plugin_inst.save()

    def _handle_app_unextpath_parameters(self, unextpath_parameters_dict):
        """
        Internal method to handle parameters of type 'unextpath' passed to the plugin
        instance app.
        """
        job_id = self.str_job_id
        outputdir = self.c_plugin_inst.get_output_path()
        obj_output_path_list = []
        for param_flag in unextpath_parameters_dict:
            # each parameter value is a string of one or more paths separated by comma
            path_list = unextpath_parameters_dict[param_flag].split(',')
            for path in path_list:
                try:
                    obj_list = self.swift_manager.ls(path)
                except ClientException as e:
                    logger.error(f'[CODE06,{job_id}]: Error while listing swift '
                                 f'storage files in {path}, detail: {str(e)}')
                    self.c_plugin_inst.error_code = 'CODE06'
                    raise
                for obj in obj_list:
                    obj_output_path = obj.replace(path.rstrip('/'), outputdir, 1)
                    if not obj_output_path.startswith(outputdir + '/'):
                        obj_output_path = outputdir + '/' + obj.split('/')[-1]
                    try:
                        if not self.swift_manager.obj_exists(obj_output_path):
                            self.swift_manager.copy_obj(obj, obj_output_path)
                    except ClientException as e:
                        logger.error(f'[CODE09,{job_id}]: Error while copying file '
                                     f'from {obj} to {obj_output_path} in swift storage, '
                                     f'detail: {str(e)}')
                        self.c_plugin_inst.error_code = 'CODE09'
                        raise
                    else:
                        obj_output_path_list.append(obj_output_path)
        logger.info('Registering output files not extracted from swift with job %s',
                    self.str_job_id)
        self._register_output_files(obj_output_path_list)

    def _handle_app_ts_unextracted_input_objs(self, d_ts_input_objs, group_by_instance):
        """
        Internal method to handle a 'ts' plugin's input instances' filtered objects
        (which are not extracted from object storage).
        """
        job_id = self.str_job_id
        outputdir = self.c_plugin_inst.get_output_path()
        obj_output_path_list = []
        for plg_inst_id in d_ts_input_objs:
            plg_inst_output_path = d_ts_input_objs[plg_inst_id]['output_path']
            obj_list = d_ts_input_objs[plg_inst_id]['objs']
            plg_inst_outputdir = outputdir
            if group_by_instance:
                plg_inst_outputdir = os.path.join(outputdir, str(plg_inst_id))
            for obj in obj_list:
                obj_output_path = obj.replace(plg_inst_output_path, plg_inst_outputdir, 1)
                try:
                    if not self.swift_manager.obj_exists(obj_output_path):
                        self.swift_manager.copy_obj(obj, obj_output_path)
                except ClientException as e:
                    logger.error(f'[CODE09,{job_id}]: Error while copying file '
                                 f'from {obj} to {obj_output_path} in swift storage, '
                                 f'detail: {str(e)}')
                    self.c_plugin_inst.error_code = 'CODE09'
                    raise
                obj_output_path_list.append(obj_output_path)
        logger.info("Registering 'ts' plugin's output files not extracted from swift with"
                    " job %s", self.str_job_id)
        self._register_output_files(obj_output_path_list)

    def _handle_finished_successfully_status(self):
        """
        Internal method to handle the 'finishedSuccessfully' status returned by the
        remote compute.
        """
        plg_inst_lock = PluginInstanceLock(plugin_inst=self.c_plugin_inst)
        try:
            plg_inst_lock.save()
        except IntegrityError:
            pass  # another async task has already entered here
        else:
            # only one concurrent async task should get here
            pfcon_url = self.pfcon_client.url
            job_id = self.str_job_id
            logger.info(f'Sending zip file request to pfcon url -->{pfcon_url}<-- '
                        f'for job {job_id}')
            try:
                zip_content = self._get_job_zip_data(job_id)
            except PfconRequestException as e:
                logger.error(f'[CODE03,{job_id}]: Error fetching zip from pfcon url '
                             f'-->{pfcon_url}<--, detail: {str(e)}')
                self.c_plugin_inst.error_code = 'CODE03'
                self.c_plugin_inst.status = 'cancelled'  # giving up
            else:
                # data successfully downloaded so update summary
                d_jobStatusSummary = json.loads(self.c_plugin_inst.summary)
                d_jobStatusSummary['pullPath']['status'] = True
                self.c_plugin_inst.summary = json.dumps(d_jobStatusSummary)

                logger.info('Registering output files from remote with job %s', job_id)
                self.c_plugin_inst.status = 'registeringFiles'
                self.c_plugin_inst.save()  # inform FE about status change
                try:
                    self.unpack_zip_file(zip_content)  # register files from remote

                    # register files from unextracted path parameters
                    d_unextpath_params, _ = self.get_plugin_instance_path_parameters()
                    if d_unextpath_params:
                        self._handle_app_unextpath_parameters(d_unextpath_params)

                    # register files from filtered input instance paths ('ts' plugins)
                    if self.c_plugin_inst.plugin.meta.type == 'ts':
                        d_ts_input_objs, tf = self.get_ts_plugin_instance_input_objs()
                        self._handle_app_ts_unextracted_input_objs(d_ts_input_objs, tf)
                except Exception:
                    self.c_plugin_inst.status = 'cancelled'  # giving up
                else:
                    self.c_plugin_inst.status = 'finishedSuccessfully'
            self.delete_plugin_instance_job_from_remote()
            self.save_plugin_instance_final_status()

    def _get_job_zip_data(self, job_id, timeout=9000):
        """
        Get job zip data from a remote pfcon service.
        """
        try:
            zip_content = self.pfcon_client.get_job_zip_data(job_id, timeout)
        except PfconRequestInvalidTokenException:
            logger.info(f'Auth token has expired while getting zip data for job {job_id} '
                        f'from pfcon url -->{self.pfcon_client.url}<--')
            self._refresh_compute_resource_auth_token()
            zip_content = self.pfcon_client.get_job_zip_data(job_id, timeout)
        return zip_content

    def _handle_finished_with_error_status(self):
        """
        Internal method to handle the 'finishedWithError' status returned by the
        remote compute.
        """
        self.c_plugin_inst.status = 'finishedWithError'
        self.delete_plugin_instance_job_from_remote()
        self.save_plugin_instance_final_status()

    def _handle_undefined_status(self):
        """
        Internal method to handle the 'undefined' status returned by the
        remote compute.
        """
        job_id = self.str_job_id
        logger.error(f'[CODE10,{job_id}]: Got undefined status from remote')
        self.c_plugin_inst.error_code = 'CODE10'
        self.cancel_plugin_instance_app_exec()

    def _register_output_files(self, filenames):
        """
        Internal method to register output files generated for the plugin instance with
        the DB. The 'filenames' arg is a list of obj names in object storage.
        """
        job_id = self.str_job_id
        total_size = 0
        for obj_name in filenames:
            logger.info(f'Registering file -->{obj_name}<-- for job {job_id}')
            plg_inst_file = PluginInstanceFile(plugin_inst=self.c_plugin_inst)
            plg_inst_file.fname.name = obj_name
            try:
                plg_inst_file.save()
            except IntegrityError:  # avoid re-register a file already registered
                logger.info(f'File -->{obj_name}<-- already registered for job {job_id}')
            else:
                total_size += plg_inst_file.fname.size
        self.c_plugin_inst.size += total_size

    @staticmethod
    def get_job_status_summary(d_response=None):
        """
        Get a job status summary JSON string from pfcon response.
        """
        # Still WIP about what is best summary...

        d_jobStatusSummary = {
            'pushPath': {
                'status': True
            },
            'pullPath': {
                'status': False
            },
            'compute': {
                'submit': {
                    'status': True
                },
                'return': {
                    'status': False,
                    'job_status': '',
                    'job_logs': ''
                }
            },
        }
        if d_response is not None:
            d_c = d_response['compute']
            if d_c['status'] in ('undefined', 'finishedSuccessfully',
                                 'finishedWithError'):
                d_jobStatusSummary['compute']['return']['status'] = True
            d_jobStatusSummary['compute']['return']['job_status'] = d_c['status']
            logs = d_jobStatusSummary['compute']['return']['job_logs'] = d_c['logs']

            # truncate logs, assuming worst case where every character needs to be escaped
            if len(logs) > 1800:
                d_jobStatusSummary['compute']['return']['job_logs'] = logs[-1800:]
        # PostgreSQL allows UTF-8, supports emojis, chinese, etc.
        return json.dumps(d_jobStatusSummary, ensure_ascii=False)


# chris_backend/plugininstances/tests/test_manager.py


import logging
import os
import io
import time
from unittest import mock

from django.test import TestCase, tag
from django.contrib.auth.models import User
from django.conf import settings

from core.swiftmanager import SwiftManager
from plugins.models import PluginMeta, Plugin
from plugins.models import PluginParameter
from plugininstances.models import PluginInstance, PathParameter, ComputeResource
from plugininstances.services import manager


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class PluginInstanceManagerTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                          settings.SWIFT_CONNECTION_PARAMS)

        self.plugin_fs_name = "simplefsapp"
        self.username = 'foo'
        self.password = 'foo-pass'

        plugin_parameters = [{'name': 'dir', 'type': 'path', 'action': 'store',
                              'optional': False, 'flag': '--dir', 'short_flag': '-d',
                              'help': 'test plugin', 'ui_exposed': True}]

        self.plg_data = {'description': 'A simple chris fs app demo',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-simplefsapp',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'simplefsapp'}

        self.plg_meta_data = {'name': self.plugin_fs_name,
                              'title': 'Dir plugin',
                              'license': 'MIT',
                              'type': 'fs',
                              'icon': 'http://github.com/plugin',
                              'category': 'Dir',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user='pfcon',
            compute_password='pfcon1234')

        # create a plugin
        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        # add plugin's parameters
        parameters = plugin_parameters
        PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'],
            optional=parameters[0]['optional'],
        )

        # create user
        User.objects.create_user(username=self.username, password=self.password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_mananger_can_run_registered_plugin_app(self):
        """
        Test whether the manager can run an already registered plugin app.
        """
        with mock.patch.object(manager, 'json_zip2str',
                               return_value='raw') as json_zip2str_mock:
            user = User.objects.get(username=self.username)
            plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
            (pl_inst, tf) = PluginInstance.objects.get_or_create(
                plugin=plugin, owner=user, status='scheduled',
                compute_resource=plugin.compute_resources.all()[0])
            pl_param = plugin.parameters.all()[0]
            PathParameter.objects.get_or_create(plugin_inst=pl_inst,
                                                plugin_param=pl_param,
                                                value=self.username)
            plg_inst_manager = manager.PluginInstanceManager(pl_inst)
            plg_inst_manager.get_job_status_summary = mock.Mock(return_value='summary')
            plg_inst_manager.pfcon_client.submit_job = mock.Mock(
                return_value='dictionary')

            # call run_plugin_instance_app method
            plg_inst_manager.run_plugin_instance_app()

            self.assertEqual(pl_inst.status, 'started')
            self.assertEqual(pl_inst.summary, 'summary')
            self.assertEqual(pl_inst.raw, 'raw')
            plg_inst_manager.pfcon_client.submit_job.assert_called_once()
            plg_inst_manager.get_job_status_summary.assert_called_once()
            json_zip2str_mock.assert_called_once()

    def test_mananger_can_check_plugin_instance_app_exec_status(self):
        """
        Test whether the manager can check a plugin's app execution status
        """
        pass
        #    response_mock = mock.Mock()
        #    response_mock.status_code = 200
        #    with mock.patch.object(manager.requests, 'post',
        #                          return_value=response_mock) as post_mock:
        #
        #     user = User.objects.get(username=self.username)
        #     plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        #     (pl_inst, tf) = PluginInstance.objects.get_or_create(
        #         plugin=plugin, owner=user,
        #         compute_resource=plugin.compute_resources.all()[0])
        #     pl_param = plugin.parameters.all()[0]
        #     PathParameter.objects.get_or_create(plugin_inst=pl_inst,
        #                                         plugin_param=pl_param,
        #                                         value=self.username)
        #     plg_inst_manager = manager.PluginInstanceManager(pl_inst)
        #     plg_inst_manager.check_plugin_instance_app_exec_status()
        #     self.assertEqual(pl_inst.status, 'started')
        #     msg = {
        #         "action": "status",
        #         "meta": {
        #             "remote": {
        #                 "key": plg_inst_manager.str_job_id
        #             }
        #         }
        #     }
        #     post_mock.assert_called_with(msg)

    @tag('integration', 'error-pman')
    def test_integration_mananger_can_run_and_check_plugin_instance_app_exec_status(self):
        """
        Test whether the manager can check a plugin's app execution status.

        NB: Note the directory overrides on input and output dirs! This
            is file system space in the plugin container, and thus by hardcoding
            this here we are relying on totally out-of-band knowledge! 

            This must be fixed in later versions!
        """
        # upload a file to the Swift storage user's space
        user_space_path = '%s/uploads/' % self.username
        with io.StringIO('Test file') as f:
            self.swift_manager.upload_obj(user_space_path + 'test.txt', f.read(),
                                          content_type='text/plain')

        # create a plugin's instance
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, status='scheduled',
            compute_resource=plugin.compute_resources.all()[0])
        pl_param = plugin.parameters.all()[0]
        PathParameter.objects.get_or_create(plugin_inst=pl_inst, plugin_param=pl_param,
                                            value=user_space_path)

        # run the plugin instance
        plg_inst_manager = manager.PluginInstanceManager(pl_inst)
        plg_inst_manager.run_plugin_instance_app()
        self.assertEqual(pl_inst.status, 'started')

        # In the following we keep checking the status until the job ends with
        # 'finishedSuccessfully'. The code runs in a lazy loop poll with a
        # max number of attempts at 10 second intervals.
        maxLoopTries = 10
        currentLoop = 1
        b_checkAgain = True
        time.sleep(10)
        while b_checkAgain:
            str_responseStatus = plg_inst_manager.check_plugin_instance_app_exec_status()
            if str_responseStatus == 'finishedSuccessfully':
                b_checkAgain = False
            elif currentLoop < maxLoopTries:
                time.sleep(10)
            if currentLoop == maxLoopTries:
                b_checkAgain = False
            currentLoop += 1
        self.assertEqual(pl_inst.status, 'finishedSuccessfully')

        str_fileCreatedByPlugin = os.path.join(pl_inst.get_output_path(), 'out.txt')
        # make sure str_fileCreatedByPlugin file was created in Swift storage
        self.assertTrue(self.swift_manager.obj_exists(str_fileCreatedByPlugin))

        # delete files from swift storage
        self.swift_manager.delete_obj(user_space_path + 'test.txt')
        # obj_paths = self.swift_manager.ls(pl_inst.get_output_path())
        # for path in obj_paths:
        #     self.swift_manager.delete_obj(path)


# chris_backend/plugininstances/tests/test_models.py


import logging
import os
from unittest import mock

from django.test import TestCase, tag
from django.contrib.auth.models import User
from django.conf import settings

from feeds.models import Feed
from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter, DefaultStrParameter
from plugininstances.models import PluginInstance, PluginInstanceFile
from plugininstances.models import PluginInstanceFilter


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ModelTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_fs_name = "simplecopyapp"
        self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True,
                                             'default': "./"}}
        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'prefix': {'type': 'string', 'optional': False}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user='pfcon',
            compute_password='pfcon1234')

        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_fs_name,
                                                         type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name,
                                                         type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add plugins' parameters
        (plg_param, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type=self.plugin_fs_parameters['dir']['type'],
            optional=self.plugin_fs_parameters['dir']['optional'])
        default = self.plugin_fs_parameters['dir']['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param, value=default)

        PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='prefix',
            type=self.plugin_ds_parameters['prefix']['type'],
            optional=self.plugin_ds_parameters['prefix']['optional'])

        # create user
        User.objects.create_user(username=self.username,
                                 password=self.password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PluginInstanceModelTests(ModelTests):

    def test_save_creates_new_feed_just_after_fs_plugininstance_is_created(self):
        """
        Test whether overriden save method creates a feed just after an 'fs' plugin 
        instance is created.
        """
        # create an 'fs' plugin instance that in turn should create a new feed
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        self.assertEqual(Feed.objects.count(), 1)
        self.assertEqual(pl_inst.feed.name, pl_inst.plugin.meta.name)

    def test_save_does_not_create_new_feed_just_after_ds_plugininstance_is_created(self):
        """
        Test whether overriden save method does not create a feed just after a 'ds' plugin
        instance is created.
        """
        # create a 'fs' plugin instance
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        plg_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        # create a 'ds' plugin instance whose previous is the previous 'fs' plugin instance
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        PluginInstance.objects.create(plugin=plugin, owner=user, previous=plg_inst,
                                      compute_resource=plugin.compute_resources.all()[0])
        # the new 'ds' plugin instance shouldn't create a new feed
        self.assertEqual(Feed.objects.count(), 1)

    def test_get_root_instance(self):
        """
        Test whether custom get_root_instance method returns the root 'fs' plugin 
        instance for a give plugin instance.
        """
        # create a 'fs' plugin instance 
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        plg_inst_root = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        # create a 'ds' plugin instance whose root is the previous 'fs' plugin instance
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        plg_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user, previous=plg_inst_root,
            compute_resource=plugin.compute_resources.all()[0])
        root_instance = plg_inst.get_root_instance()
        self.assertEqual(root_instance, plg_inst_root)

    def test_get_descendant_instances(self):
        """
        Test whether custom get_descendant_instances method returns all the plugin
        instances that are a descendant of a plugin instance
        """
        # create a 'fs' plugin instance
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        plg_inst_root = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        # create a 'ds' plugin instance whose previous is the previous 'fs' plugin instance
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        plg_inst1 = PluginInstance.objects.create(
            plugin=plugin, owner=user, previous=plg_inst_root,
            compute_resource=plugin.compute_resources.all()[0])
        # create another 'ds' plugin instance whose previous is the previous 'ds' plugin
        # instance
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        plg_inst2 = PluginInstance.objects.create(
            plugin=plugin, owner=user, previous=plg_inst1,
            compute_resource=plugin.compute_resources.all()[0])
        decend_instances = plg_inst_root.get_descendant_instances()
        self.assertEqual(len(decend_instances), 3)
        self.assertEqual(decend_instances[0], plg_inst_root)
        self.assertEqual(decend_instances[1], plg_inst1)
        self.assertEqual(decend_instances[2], plg_inst2)

    def test_get_output_path(self):
        """
        Test whether custom get_output_path method returns appropriate output paths
        for both 'fs' and 'ds' plugins.
        """
        # create an 'fs' plugin instance 
        user = User.objects.get(username=self.username)
        plugin_fs = Plugin.objects.get(meta__name=self.plugin_fs_name)
        pl_inst_fs = PluginInstance.objects.create(
            plugin=plugin_fs,
            owner=user,
            compute_resource=plugin_fs.compute_resources.all()[0])
        # 'fs' plugins will output files to:
        # SWIFT_CONTAINER_NAME/<username>/feed_<id>/plugin_name_plugin_inst_<id>/data
        fs_output_path = '{0}/feed_{1}/{2}_{3}/data'.format( self.username,
                                                             pl_inst_fs.feed.id,
                                                             pl_inst_fs.plugin.meta.name,
                                                             pl_inst_fs.id) 
        self.assertEqual(pl_inst_fs.get_output_path(), fs_output_path)

        # create a 'ds' plugin instance 
        user = User.objects.get(username=self.username)
        plugin_ds = Plugin.objects.get(meta__name=self.plugin_ds_name)
        pl_inst_ds = PluginInstance.objects.create(
            plugin=plugin_ds, owner=user, previous=pl_inst_fs,
            compute_resource=plugin_ds.compute_resources.all()[0])
        # 'ds' plugins will output files to:
        # SWIFT_CONTAINER_NAME/<username>/feed_<id>/...
        #/previous_plugin_name_plugin_inst_<id>/plugin_name_plugin_inst_<id>/data
        ds_output_path = os.path.join(os.path.dirname(fs_output_path),
                                      '{0}_{1}/data'.format(pl_inst_ds.plugin.meta.name,
                                                            pl_inst_ds.id))
        self.assertEqual(pl_inst_ds.get_output_path(), ds_output_path)


class PluginInstanceFilterModelTests(ModelTests):

    def test_filter_by_root_id(self):
        """
        Test whether custom filter_by_root_id method returns the plugin instances in a
        queryset with a common root plugin instance.
        """
        # create a 'fs' plugin instance
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        plg_inst_root = PluginInstance.objects.create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])
        # create a 'ds' plugin instance whose previous is the previous 'fs' plugin instance
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        plg_inst1 = PluginInstance.objects.create(
            plugin=plugin, owner=user, previous=plg_inst_root,
            compute_resource=plugin.compute_resources.all()[0])
        # create another 'ds' plugin instance whose previous is the previous 'ds' plugin
        # instance
        plugin = Plugin.objects.get(meta__name=self.plugin_ds_name)
        plg_inst2 = PluginInstance.objects.create(
            plugin=plugin, owner=user, previous=plg_inst1,
            compute_resource=plugin.compute_resources.all()[0])
        queryset = PluginInstance.objects.all()
        value = plg_inst1.id
        filter = PluginInstanceFilter()
        filtered_queryset = filter.filter_by_root_id(queryset, "", value)
        self.assertEqual(len(filtered_queryset), 2)
        self.assertEqual(filtered_queryset[0], plg_inst1)
        self.assertEqual(filtered_queryset[1], plg_inst2)


# chris_backend/plugininstances/tests/test_serializers.py


import logging
from unittest import mock

from django.test import TestCase
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import serializers

from plugins.models import PluginMeta, Plugin, PluginParameter, ComputeResource
from plugininstances.models import PluginInstance
from plugininstances.serializers import SwiftManager
from plugininstances.serializers import PluginInstanceSerializer
from plugininstances.serializers import (PathParameterSerializer,
                                         UnextpathParameterSerializer)


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class SerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.username = 'foo'
        self.password = 'foopassword'
        self.plugin_name = "simplefsapp"
        plugin_parameters = [{'name': 'dir', 'type': 'string', 'action': 'store',
                              'optional': True, 'flag': '--dir', 'short_flag': '-d',
                              'default': '/', 'help': 'test plugin', 'ui_exposed': True}]

        self.plg_data = {'description': 'A simple chris fs app demo',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-simplecopyapp',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'simplefsapp'}

        self.plg_meta_data = {'name': self.plugin_name,
                              'title': 'Dir plugin',
                              'license': 'MIT',
                              'type': 'fs',
                              'icon': 'http://github.com/plugin',
                              'category': 'Dir',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user='pfcon',
            compute_password='pfcon1234')

        # create a plugin
        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        # add plugin's parameters
        parameters = plugin_parameters
        PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'],
            short_flag=parameters[0]['short_flag']
        )

        # create user
        User.objects.create_user(username=self.username, password=self.password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PluginInstanceSerializerTests(SerializerTests):

    def setUp(self):
        super(PluginInstanceSerializerTests, self).setUp()
        self.plugin = Plugin.objects.get(meta__name=self.plugin_name)
        self.user = User.objects.get(username=self.username)
        self.data = {'plugin': self.plugin,
                     'owner': self.user,
                     'compute_resource': self.plugin.compute_resources.all()[0]}

    def test_validate_previous(self):
        """
        Test whether custom validate_previous method returns a previous instance or
        raises a serializers.ValidationError.
        """
        plugin = self.plugin
        # create an 'fs' plugin instance
        pl_inst_fs = PluginInstance.objects.create(
            plugin=plugin, owner=self.user, compute_resource=plugin.compute_resources.all()[0])
        # create a 'ds' plugin
        data = self.plg_meta_data.copy()
        data['name'] = 'testdsapp'
        data['type'] = 'ds'
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()
        data = {'plugin': plugin,
                'owner': self.user,
                'compute_resource': plugin.compute_resources.all()[0]}

        # create serializer for a 'ds' plugin instance
        plg_inst_serializer = PluginInstanceSerializer(data=data)
        plg_inst_serializer.is_valid(raise_exception=True)
        plg_inst_serializer.context['view'] = mock.Mock()
        plg_inst_serializer.context['view'].get_object = mock.Mock(return_value=plugin)
        plg_inst_serializer.context['request'] = mock.Mock()
        plg_inst_serializer.context['request'].user = self.user
        previous = plg_inst_serializer.validate_previous(pl_inst_fs.id)

        self.assertEqual(previous, pl_inst_fs)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_previous('')

    def test_validate_compute_resource_name(self):
        """
        Test whether overriden validate_compute_resource_name method checks that the
        provided compute resource name is registered with the corresponding plugin.
        """
        plugin = self.plugin
        data = {'plugin': plugin,
                'owner': self.user,
                'compute_resource': plugin.compute_resources.all()[0]}

        # create serializer for a  plugin instance
        plg_inst_serializer = PluginInstanceSerializer(data=data)
        plg_inst_serializer.is_valid(raise_exception=True)
        plg_inst_serializer.context['view'] = mock.Mock()
        plg_inst_serializer.context['view'].get_object = mock.Mock(return_value=plugin)
        compute_resource_name = plg_inst_serializer.validate_compute_resource_name("host")
        self.assertEqual(compute_resource_name, "host")
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_compute_resource_name('new')

    def test_validate_status(self):
        """
        Test whether overriden validate_status method raises a serializers.ValidationError
        when the status is not 'cancelled' or the current instance status is
        'finishedSuccessfully' or 'finishedWithError.
        """
        plugin = self.plugin
        owner = self.user
        (plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=owner, compute_resource=plugin.compute_resources.all()[0])
        plg_inst_serializer = PluginInstanceSerializer(plg_inst)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_status('finishedSuccessfully')

        (plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=owner, compute_resource=plugin.compute_resources.all()[0])
        plg_inst.status = 'finishedSuccessfully'
        plg_inst_serializer = PluginInstanceSerializer(plg_inst)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_status('cancelled')
        plg_inst.status = 'finishedWithError'
        plg_inst_serializer = PluginInstanceSerializer(plg_inst)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_status('cancelled')

    def test_validate_gpu_limit(self):
        """
        Test whether custom validate_gpu_limit raises a serializers.ValidationError when
        the gpu_limit is not within the limits provided by the corresponding plugin.
        """
        data = self.data
        plugin = self.plugin
        plg_inst_serializer = PluginInstanceSerializer(data=data)
        plg_inst_serializer.is_valid(raise_exception=True)
        plg_inst_serializer.context['view'] = mock.Mock()
        plg_inst_serializer.context['view'].get_object = mock.Mock(return_value=plugin)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_gpu_limit(plugin.min_gpu_limit-1)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_gpu_limit(plugin.max_gpu_limit+1)

    def test_validate_number_of_workers(self):
        """
        Test whether custom validate_number_of_workers raises a serializers.ValidationError
        when the number_of_workers is not within the limits provided by the corresponding
        plugin.
        """
        data = self.data
        plugin = self.plugin
        plg_inst_serializer = PluginInstanceSerializer(data=data)
        plg_inst_serializer.is_valid(raise_exception=True)
        plg_inst_serializer.context['view'] = mock.Mock()
        plg_inst_serializer.context['view'].get_object = mock.Mock(return_value=plugin)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_number_of_workers(plugin.min_number_of_workers-1)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_number_of_workers(plugin.max_number_of_workers+1)

    def test_validate_cpu_limit(self):
        """
        Test whether custom validate_cpu_limit raises a serializers.ValidationError when
        the cpu_limit is not within the limits provided by the corresponding plugin.
        """
        data = self.data
        plugin = self.plugin
        plg_inst_serializer = PluginInstanceSerializer(data=data)
        plg_inst_serializer.is_valid(raise_exception=True)
        plg_inst_serializer.context['view'] = mock.Mock()
        plg_inst_serializer.context['view'].get_object = mock.Mock(return_value=plugin)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_cpu_limit(plugin.min_cpu_limit-1)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_cpu_limit(plugin.max_cpu_limit+1)

    def test_validate_memory_limit(self):
        """
        Test whether custom validate_memory_limit raises a serializers.ValidationError
        when the memory_limit is not within the limits provided by the corresponding
        plugin.
        """
        data = self.data
        plugin = self.plugin
        plg_inst_serializer = PluginInstanceSerializer(data=data)
        plg_inst_serializer.is_valid(raise_exception=True)
        plg_inst_serializer.context['view'] = mock.Mock()
        plg_inst_serializer.context['view'].get_object = mock.Mock(return_value=plugin)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_memory_limit(plugin.min_memory_limit-1)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_memory_limit(plugin.max_memory_limit+1)

    def test_validate_value_within_interval(self):
        """
        Test whether custom validate_value_within_interval raises a
        serializers.ValidationError when the first argument is not within the interval
        provided by the second and third argument.
        """
        plugin = self.plugin
        plg_inst_serializer = PluginInstanceSerializer(plugin)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_value_within_interval(1, 2, 4)
        with self.assertRaises(serializers.ValidationError):
            plg_inst_serializer.validate_value_within_interval(5, 2, 4)


class PathParameterSerializerTests(SerializerTests):

    def setUp(self):
        super(PathParameterSerializerTests, self).setUp()
        self.plugin = Plugin.objects.get(meta__name=self.plugin_name)
        self.user = User.objects.get(username=self.username)
        self.other_username = 'boo'
        self.other_password = 'far'

    def test_validate_value_fail_acces_denied_to_root(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access an invalid root path.
        """
        path_parm_serializer = PathParameterSerializer(user=self.user)
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value("./")
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(".")
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value("/")

    def test_validate_value_fail_denied_acces_other_user_space(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access another user's space.
        """
        path_parm_serializer = PathParameterSerializer(user=self.user)
        with self.assertRaises(serializers.ValidationError):
            value = "{}/uploads, anotheruser".format(self.username)
            path_parm_serializer.validate_value(value)
        with self.assertRaises(serializers.ValidationError):
            value = "{}, anotheruser/uploads".format(self.username, self.username)
            path_parm_serializer.validate_value(value)

    def test_validate_value_fail_invalid_feed_path(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access another user's invalid feed path.
        """
        user = User.objects.get(username=self.username)
        user1 = User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        plugin = self.plugin
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user1, compute_resource=plugin.compute_resources.all()[0])
        path_parm_serializer = PathParameterSerializer(user=user)
        # but feed id
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(self.other_username + '/feed_butnumber')
        # feed id does not exist in the DB
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(self.other_username + '/feed_%s' %
                                                (pl_inst.feed.id + 1))
        # user is not owner of this existing feed
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(self.other_username + '/feed_%s' %
                                                pl_inst.feed.id)

    def test_validate_value_fail_path_does_not_exist(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access a path that does not exist in the internal storage.
        """
        user = User.objects.get(username=self.username)
        path_parm_serializer = PathParameterSerializer(user=user)
        with mock.patch.object(SwiftManager, 'path_exists',
                               return_value=False) as path_exists_mock:
            with self.assertRaises(serializers.ValidationError):
                path_parm_serializer.validate_value(self.username)
            path_exists_mock.assert_called_with(self.username)

    def test_validate_value_success(self):
        """
        Test whether overriden validate_value method successfully returns a valid
        string of paths separated by comma.
        """
        user = User.objects.get(username=self.username)
        user1 = User.objects.create_user(username=self.other_username,
                                         password=self.other_password)
        plugin = self.plugin
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user1, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.owner.set([user1, user])
        path_parm_serializer = PathParameterSerializer(user=user)
        value = "{}, {}/feed_{} ".format(self.username, self.other_username,
                                         pl_inst.feed.id)
        with mock.patch.object(SwiftManager, 'path_exists',
                               return_value=True) as path_exists_mock:
            returned_value = path_parm_serializer.validate_value(value)
            self.assertEqual(returned_value, "{},{}/feed_{}".format(self.username,
                                                                    self.other_username,
                                                                    pl_inst.feed.id))


class UnextpathParameterSerializerTests(SerializerTests):

    def setUp(self):
        super(UnextpathParameterSerializerTests, self).setUp()
        self.plugin = Plugin.objects.get(meta__name=self.plugin_name)
        self.user = User.objects.get(username=self.username)
        self.other_username = 'boo'
        self.other_password = 'far'

    def test_validate_value_fail_acces_denied_to_root(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access an invalid root path.
        """
        path_parm_serializer = UnextpathParameterSerializer(user=self.user)
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value("./")
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(".")
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value("/")

    def test_validate_value_fail_denied_acces_other_user_space(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access another user's space.
        """
        path_parm_serializer = UnextpathParameterSerializer(user=self.user)
        with self.assertRaises(serializers.ValidationError):
            value = "{}/uploads, anotheruser".format(self.username)
            path_parm_serializer.validate_value(value)
        with self.assertRaises(serializers.ValidationError):
            value = "{}, anotheruser/uploads".format(self.username, self.username)
            path_parm_serializer.validate_value(value)

    def test_validate_value_fail_invalid_feed_path(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access another user's invalid feed path.
        """
        user = User.objects.get(username=self.username)
        user1 = User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        plugin = self.plugin
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user1, compute_resource=plugin.compute_resources.all()[0])
        path_parm_serializer = UnextpathParameterSerializer(user=user)
        # but feed id
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(self.other_username + '/feed_butnumber')
        # feed id does not exist in the DB
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(self.other_username + '/feed_%s' %
                                                (pl_inst.feed.id + 1))
        # user is not owner of this existing feed
        with self.assertRaises(serializers.ValidationError):
            path_parm_serializer.validate_value(self.other_username + '/feed_%s' %
                                                pl_inst.feed.id)

    def test_validate_value_fail_path_does_not_exist(self):
        """
        Test whether overriden validate_value method raises a serializers.ValidationError
        when user tries to access a path that does not exist in the internal storage.
        """
        user = User.objects.get(username=self.username)
        path_parm_serializer = UnextpathParameterSerializer(user=user)
        with mock.patch.object(SwiftManager, 'path_exists',
                               return_value=False) as path_exists_mock:
            with self.assertRaises(serializers.ValidationError):
                path_parm_serializer.validate_value(self.username)
            path_exists_mock.assert_called_with(self.username)

    def test_validate_value_success(self):
        """
        Test whether overriden validate_value method successfully returns a valid
        string of paths separated by comma.
        """
        user = User.objects.get(username=self.username)
        user1 = User.objects.create_user(username=self.other_username,
                                         password=self.other_password)
        plugin = self.plugin
        pl_inst = PluginInstance.objects.create(
            plugin=plugin, owner=user1, compute_resource=plugin.compute_resources.all()[0])
        pl_inst.feed.owner.set([user1, user])
        path_parm_serializer = UnextpathParameterSerializer(user=user)
        value = "{}, {}/feed_{} ".format(self.username, self.other_username,
                                         pl_inst.feed.id)
        with mock.patch.object(SwiftManager, 'path_exists',
                               return_value=True) as path_exists_mock:
            returned_value = path_parm_serializer.validate_value(value)
            self.assertEqual(returned_value, "{},{}/feed_{}".format(self.username,
                                                                    self.other_username,
                                                                    pl_inst.feed.id))


# chris_backend/plugininstances/tests/test_tasks.py


import logging
from unittest import mock, skip

from django.test import TestCase, tag
from django.contrib.auth.models import User
from django.conf import settings

from plugins.models import PluginMeta, Plugin, ComputeResource
from plugininstances.models import PluginInstance

from plugininstances import tasks


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class TasksTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.username = 'foo'
        self.password = 'bar'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user='pfcon',
            compute_password='pfcon1234')

        # create the chris user
        User.objects.create_user(username=self.username, password=self.password)

        # create two plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PluginInstanceTasksTests(TasksTests):
    """
    Test the plugin instance tasks.
    """

    def setUp(self):
        super(PluginInstanceTasksTests, self).setUp()
        plugin = Plugin.objects.get(meta__name="pacspull")
        # create a plugin's instance
        user = User.objects.get(username=self.username)
        (self.plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin,
            owner=user,
            compute_resource=plugin.compute_resources.all()[0])
        self.plg_inst.status = 'started'
        self.plg_inst.save()

    def test_task_run_plugin_instance(self):
        with mock.patch.object(tasks.PluginInstanceManager, 'run_plugin_instance_app',
                               return_value=None) as run_mock:
            tasks.run_plugin_instance(self.plg_inst.id)
            run_mock.assert_called_with()

    def test_task_check_plugin_instance_exec_status(self):
        with mock.patch.object(tasks.PluginInstanceManager,
                               'check_plugin_instance_app_exec_status',
                               return_value=None) as check_exec_status_mock:
            tasks.check_plugin_instance_exec_status(self.plg_inst.id)
            check_exec_status_mock.assert_called_with()

    def test_task_cancel_plugin_instance(self):
        with mock.patch.object(tasks.PluginInstanceManager,
                               'cancel_plugin_instance_app_exec',
                               return_value=None) as cancel_mock:
            tasks.cancel_plugin_instance(self.plg_inst.id)
            cancel_mock.assert_called_with()

    def test_task_check_started_plugin_instances_exec_status(self):
        with mock.patch.object(tasks.check_plugin_instance_exec_status, 'delay',
                               return_value=None) as delay_mock:
            tasks.check_started_plugin_instances_exec_status()

            # check that the check_plugin_instance_exec_status task was called with appropriate args
            delay_mock.assert_called_with(self.plg_inst.id)
            self.assertEqual(self.plg_inst.status, 'started')


# chris_backend/plugininstances/tests/test_views.py


import logging
import json
import time
import io
import os
from unittest import mock, skip

from django.test import TestCase, TransactionTestCase, tag
from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import status

from celery.contrib.testing.worker import start_worker

from core.celery import app as celery_app
from core.celery import task_routes
from core.swiftmanager import SwiftManager
from plugins.models import PluginMeta, Plugin, PluginParameter, ComputeResource
from plugininstances.models import PluginInstance, PluginInstanceFile
from plugininstances.models import PathParameter, FloatParameter
from plugininstances.services.manager import PluginInstanceManager
from plugininstances import views


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ViewTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.chris_username = 'chris'
        self.chris_password = 'chris12'
        self.username = 'foo'
        self.password = 'bar'
        self.other_username = 'boo'
        self.other_password = 'far'
        self.compute_user = 'pfcon'
        self.compute_password = 'pfcon1234'

        self.content_type = 'application/vnd.collection+json'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user=self.compute_user,
            compute_password=self.compute_password)

        # create the chris superuser and two additional users
        User.objects.create_user(username=self.chris_username,
                                 password=self.chris_password)
        User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        User.objects.create_user(username=self.username,
                                 password=self.password)
        
        # create two plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class TasksViewTests(TransactionTestCase):

    @classmethod
    def setUpClass(cls):
        logging.disable(logging.WARNING)
        super().setUpClass()
        # route tasks to this worker by using the default 'celery' queue
        # that is exclusively used for the automated tests
        celery_app.conf.update(task_routes=None)
        cls.celery_worker = start_worker(celery_app,
                                         concurrency=1,
                                         perform_ping_check=False)
        cls.celery_worker.__enter__()

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
        cls.celery_worker.__exit__(None, None, None)
        # reset routes to the original queues
        celery_app.conf.update(task_routes=task_routes)
        logging.disable(logging.NOTSET)

    def setUp(self):

        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                          settings.SWIFT_CONNECTION_PARAMS)
        self.chris_username = 'chris'
        self.chris_password = 'chris12'
        self.username = 'foo'
        self.password = 'bar'
        self.other_username = 'boo'
        self.other_password = 'far'
        self.compute_user = 'pfcon'
        self.compute_password = 'pfcon1234'
        self.content_type = 'application/vnd.collection+json'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user=self.compute_user,
            compute_password=self.compute_password)

        # create the chris superuser and two additional users
        User.objects.create_user(username=self.chris_username,
                                 password=self.chris_password)
        User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        user = User.objects.create_user(username=self.username,
                                        password=self.password)

        # create two plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # create pacspull fs plugin instance
        (self.pl_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin_fs, owner=user,
            compute_resource=plugin_fs.compute_resources.all()[0])

        # create mri_convert ds plugin instance
        PluginInstance.objects.get_or_create(
            plugin=plugin_ds, owner=user, previous=self.pl_inst,
            compute_resource=plugin_ds.compute_resources.all()[0])


class PluginInstanceListViewTests(TasksViewTests):
    """
    Test the plugininstance-list view.
    """

    def setUp(self):
        super(PluginInstanceListViewTests, self).setUp()
        plugin = Plugin.objects.get(meta__name="pacspull")
        self.create_read_url = reverse("plugininstance-list", kwargs={"pk": plugin.id})
        self.user_space_path = '%s/uploads/' % self.username
        self.post = json.dumps(
            {"template": {"data": [{"name": "dir", "value": self.user_space_path},
                                   {"name": "title", "value": 'test1'}]}})

    def test_plugin_instance_create_success(self):
        # add parameters to the plugin before the POST request
        plugin = Plugin.objects.get(meta__name="pacspull")
        PluginParameter.objects.get_or_create(plugin=plugin, name='dir', type='string',
                                              optional=False)

        # first test 'fs' plugin instance (has no previous plugin instance)

        with mock.patch.object(views.run_plugin_instance, 'delay',
                               return_value=None) as delay_mock:
            # make API request
            self.client.login(username=self.username, password=self.password)
            response = self.client.post(self.create_read_url, data=self.post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)

            # check that the run_plugin_instance task was called with appropriate args
            delay_mock.assert_called_with(response.data['id'])
            self.assertEqual(response.data['status'], 'scheduled')

        # now test 'ds' plugin instance (has previous plugin instance)

        previous_plg_inst = PluginInstance.objects.get(title='test1')
        plugin = Plugin.objects.get(meta__name="mri_convert")
        create_read_url = reverse("plugininstance-list", kwargs={"pk": plugin.id})
        post = json.dumps(
            {"template": {"data": [{"name": "previous_id", "value": previous_plg_inst.id}]}})

        previous_plg_inst.status = 'finishedSuccessfully'
        previous_plg_inst.save()
        with mock.patch.object(views.run_plugin_instance, 'delay',
                               return_value=None) as delay_mock:
            self.client.login(username=self.username, password=self.password)
            response = self.client.post(create_read_url, data=post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)

            # check that the run_plugin_instance task was called with appropriate args
            delay_mock.assert_called_with(response.data['id'])
            self.assertEqual(response.data['status'], 'scheduled')

        previous_plg_inst.status = 'started'
        previous_plg_inst.save()
        with mock.patch.object(views.run_plugin_instance, 'delay',
                               return_value=None) as delay_mock:
            self.client.login(username=self.username, password=self.password)
            response = self.client.post(create_read_url, data=post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)

            # check that the run_plugin_instance task was not called
            delay_mock.assert_not_called()
            self.assertEqual(response.data['status'], 'waiting')

        previous_plg_inst.status = 'finishedWithError'
        previous_plg_inst.save()
        with mock.patch.object(views.run_plugin_instance, 'delay',
                               return_value=None) as delay_mock:
            self.client.login(username=self.username, password=self.password)
            response = self.client.post(create_read_url, data=post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)

            # check that the run_plugin_instance task was not called
            delay_mock.assert_not_called()
            self.assertEqual(response.data['status'], 'cancelled')

    @tag('integration')
    def test_integration_plugin_instance_create_success(self):

        # add an FS plugin to the system
        plugin_parameters = [{'name': 'dir', 'type': 'path', 'action': 'store',
                              'optional': False, 'flag': '--dir', 'short_flag': '-d',
                              'help': 'test plugin', 'ui_exposed': True}]

        self.plg_data = {'description': 'A simple chris fs app demo',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-simplefsapp',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'simplefsapp'}

        self.plg_meta_data = {'name': 'simplefsapp',
                              'title': 'Dir plugin',
                              'license': 'MIT',
                              'type': 'fs',
                              'icon': 'http://github.com/plugin',
                              'category': 'Dir',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters

        (compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user=self.compute_user,
            compute_password=self.compute_password)

        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([compute_resource])
        plugin.save()

        # add plugin's parameters
        parameters = plugin_parameters
        PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'])

        # upload a file to the Swift storage user's space
        with io.StringIO('Test file') as f:
            self.swift_manager.upload_obj(self.user_space_path + 'test.txt', f.read(),
                                          content_type='text/plain')

        # make POST API request to create a plugin instance
        create_read_url = reverse("plugininstance-list", kwargs={"pk": plugin.id})
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # instance must be 'started' before checking its status
        pl_inst = PluginInstance.objects.get(pk=response.data['id'])
        for _ in range(10):
            time.sleep(3)
            pl_inst.refresh_from_db()
            if pl_inst.status == 'started': break
        self.assertEqual(pl_inst.status, 'started')  # instance must be started

        # In the following we keep checking the status until the job ends with
        # 'finishedSuccessfully'. The code runs in a lazy loop poll with a
        # max number of attempts at 10 second intervals.
        plg_inst_manager = PluginInstanceManager(pl_inst)
        maxLoopTries = 10
        currentLoop = 1
        b_checkAgain = True
        time.sleep(10)
        while b_checkAgain:
            str_responseStatus = plg_inst_manager.check_plugin_instance_app_exec_status()
            if str_responseStatus == 'finishedSuccessfully':
                b_checkAgain = False
            elif currentLoop < maxLoopTries:
                time.sleep(10)
            if currentLoop == maxLoopTries:
                b_checkAgain = False
            currentLoop += 1
        self.assertEqual(pl_inst.status, 'finishedSuccessfully')

        # delete files from swift storage
        self.swift_manager.delete_obj(self.user_space_path + 'test.txt')

    @tag('integration')
    def test_integration_ts_plugin_instance_create_success(self):
        # create an FS plugin instance
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name="pacspull")
        (fs_plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])

        # upload FS plugin instace output file to Swift storage
        path = os.path.join(fs_plg_inst.get_output_path(), 'test.txt')
        with io.StringIO("test file") as test_file:
            self.swift_manager.upload_obj(path, test_file.read(),
                                          content_type='text/plain')
        (fs_plg_inst_file, tf) = PluginInstanceFile.objects.get_or_create(plugin_inst=fs_plg_inst)
        fs_plg_inst_file.fname.name = path
        fs_plg_inst_file.save()
        fs_plg_inst.status = 'finishedSuccessfully'
        fs_plg_inst.save()

        # add a TS plugin to the system
        plugin_parameters = [{'name': 'plugininstances', 'type': 'string',
                              'action': 'store', 'optional': True,
                              'flag': '--plugininstances', 'short_flag': '--plugininstances',
                              'help': 'test plugin parameter', 'ui_exposed': True},

                             {'name': 'filter', 'type': 'string',
                              'action': 'store', 'optional': True,
                              'flag': '--filter',
                              'short_flag': '-f',
                              'help': 'test plugin parameter', 'ui_exposed': True}
                             ]
        self.plg_data = {'description': 'A toplological copy ts plugin',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-topologicalcopy',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'topologicalcopy'}

        self.plg_meta_data = {'name': 'topologicalcopy',
                              'title': 'TS copy plugin',
                              'license': 'MIT',
                              'type': 'ts',
                              'icon': 'http://github.com/plugin',
                              'category': 'Utility',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters

        (compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user=self.compute_user,
            compute_password=self.compute_password)

        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([compute_resource])
        plugin.save()

        # add plugin's parameters
        parameters = plugin_parameters
        PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'])

        # make POST API request to create a ts plugin instance
        create_read_url = reverse("plugininstance-list", kwargs={"pk": plugin.id})
        post = json.dumps(
            {"template": {"data": [{"name": "previous_id", "value": fs_plg_inst.id},
                                   {"name": "plugininstances", "value": str(fs_plg_inst.id)},
                                   {"name": "filter", "value": ".txt$"}]}})
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(create_read_url, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # instance must be 'started' before checking its status
        pl_inst = PluginInstance.objects.get(pk=response.data['id'])
        for _ in range(10):
            time.sleep(3)
            pl_inst.refresh_from_db()
            if pl_inst.status == 'started': break
        self.assertEqual(pl_inst.status, 'started')  # instance must be started

        # In the following we keep checking the status until the job ends with
        # 'finishedSuccessfully'. The code runs in a lazy loop poll with a
        # max number of attempts at 10 second intervals.
        plg_inst_manager = PluginInstanceManager(pl_inst)
        maxLoopTries = 10
        currentLoop = 1
        b_checkAgain = True
        time.sleep(10)
        while b_checkAgain:
            str_responseStatus = plg_inst_manager.check_plugin_instance_app_exec_status()
            if str_responseStatus == 'finishedSuccessfully':
                b_checkAgain = False
            elif currentLoop < maxLoopTries:
                time.sleep(10)
            if currentLoop == maxLoopTries:
                b_checkAgain = False
            currentLoop += 1
        self.assertEqual(pl_inst.status, 'finishedSuccessfully')
        self.assertEqual(pl_inst.files.count(), 3)

        # delete files from swift storage
        self.swift_manager.delete_obj(path)

    def test_plugin_instance_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_instance_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "pacspull")

    def test_plugin_instance_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginInstanceDetailViewTests(TasksViewTests):
    """
    Test the plugininstance-detail view.
    """

    def setUp(self):
        super(PluginInstanceDetailViewTests, self).setUp()

        self.read_update_delete_url = reverse("plugininstance-detail",
                                              kwargs={"pk": self.pl_inst.id})

    def test_plugin_instance_detail_success(self):
        self.pl_inst.status = 'started'
        self.pl_inst.save()
        # make API request
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "pacspull")
        self.assertEqual(response.data['status'], 'started')
        # with mock.patch.object(views.check_plugin_instance_exec_status, 'delay',
        #                        return_value=None) as delay_mock:
        #     # make API request
        #     self.client.login(username=self.username, password=self.password)
        #     response = self.client.get(self.read_update_delete_url)
        #     self.assertContains(response, "pacspull")
        #     self.assertEqual(response.data['status'], 'started')
        #     # check that the check_plugin_instance_exec_status task was called with appropriate args
        #     delay_mock.assert_called_with(self.pl_inst.id)

    @tag('integration', 'error-pman')
    def test_integration_plugin_instance_detail_success(self):

        # add an FS plugin to the system
        plugin_parameters = [{'name': 'dir', 'type': 'path', 'action': 'store',
                              'optional': False, 'flag': '--dir', 'short_flag': '-d',
                              'help': 'test plugin', 'ui_exposed': True}]

        self.plg_data = {'description': 'A simple chris fs app demo',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-simplefsapp',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'simplefsapp'}

        self.plg_meta_data = {'name': 'simplefsapp',
                              'title': 'Dir plugin',
                              'license': 'MIT',
                              'type': 'fs',
                              'icon': 'http://github.com/plugin',
                              'category': 'Dir',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters

        (compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL, compute_user=self.compute_user,
            compute_password=self.compute_password)

        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([compute_resource])
        plugin.save()

        # add plugin's parameters
        parameters = plugin_parameters
        (pl_param, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'])

        # upload a file to the Swift storage user's space
        user_space_path = '%s/uploads/' % self.username
        with io.StringIO('Test file') as f:
            self.swift_manager.upload_obj(user_space_path + 'test.txt', f.read(),
                                          content_type='text/plain')

        # create a plugin's instance
        user = User.objects.get(username=self.username)
        (pl_inst, tf) = PluginInstance.objects.get_or_create(
            title='test2', plugin=plugin, owner=user, status='scheduled',
            compute_resource=plugin.compute_resources.all()[0])
        PathParameter.objects.get_or_create(plugin_inst=pl_inst, plugin_param=pl_param,
                                            value=user_space_path)
        read_update_delete_url = reverse("plugininstance-detail",
                                         kwargs={"pk": pl_inst.id})

        # run the plugin instance
        plg_inst_manager = PluginInstanceManager(pl_inst)
        plg_inst_manager.run_plugin_instance_app()

        # In the following we keep checking the status until the job ends with
        # 'finishedSuccessfully'. The code runs in a lazy loop poll with a
        # max number of attempts at 10 second intervals.
        self.client.login(username=self.username, password=self.password)
        maxLoopTries = 10
        currentLoop = 1
        b_checkAgain = True
        time.sleep(10)
        while b_checkAgain:
            plg_inst_manager.check_plugin_instance_app_exec_status()
            response = self.client.get(read_update_delete_url)
            str_responseStatus = response.data['status']
            if str_responseStatus == 'finishedSuccessfully':
                b_checkAgain = False
            elif currentLoop < maxLoopTries:
                time.sleep(10)
            if currentLoop == maxLoopTries:
                b_checkAgain = False
            currentLoop += 1
        self.assertContains(response, "finishedSuccessfully")
        self.assertContains(response, "simplefsapp")

        # delete files from swift storage
        self.swift_manager.delete_obj(user_space_path + 'test.txt')
        # obj_paths = self.swift_manager.ls(pl_inst.get_output_path())
        # for path in obj_paths:
        #     self.swift_manager.delete_obj(path)

    def test_plugin_instance_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_instance_update_success(self):
        put = json.dumps({
            "template": {"data": [{"name": "title", "value": "Test instance"},
                                  {"name": "status", "value": "cancelled"}]}})

        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertContains(response, "Test instance")
        self.assertContains(response, "cancelled")

    def test_plugin_instance_update_failure_current_status_is_finishedSuccessfully_or_finishedWithError(self):
        put = json.dumps({
            "template": {"data": [{"name": "status", "value": "cancelled"}]}})

        self.pl_inst.status = 'finishedSuccessfully'
        self.pl_inst.save()
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.pl_inst.status = 'finishedWithError'
        self.pl_inst.save()
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_plugin_instance_update_failure_status_can_only_be_changed_to_cancelled(self):
        put = json.dumps({
            "template": {"data": [{"name": "status", "value": "finishedSuccessfully"}]}})

        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_plugin_instance_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_delete_url, data={},
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_instance_update_failure_access_denied(self):
        put = json.dumps({
            "template": {"data": [{"name": "status", "value": "cancelled"}]}})

        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.put(self.read_update_delete_url, data=put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_plugin_instance_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(PluginInstance.objects.count(), 0)

    def test_plugin_instance_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_instance_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class PluginInstanceListQuerySearchViewTests(ViewTests):
    """
    Test the plugininstance-list-query-search view.
    """

    def setUp(self):
        super(PluginInstanceListQuerySearchViewTests, self).setUp()

        user = User.objects.get(username=self.username)
        
        # create two plugin instances
        plugin = Plugin.objects.get(meta__name="pacspull")
        (inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])

        plugin = Plugin.objects.get(meta__name="mri_convert")
        (inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, previous=inst,
            compute_resource=plugin.compute_resources.all()[0])
        # set second instance's status
        inst.status = 'finishedSuccessfully'
        inst.save()

        self.list_url = reverse("allplugininstance-list-query-search") + '?status=created'

    def test_plugin_instance_query_search_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        # response should only contain the instances that match the query
        self.assertContains(response, 'created')
        self.assertNotContains(response,'finishedSuccessfully')

    def test_plugin_instance_query_search_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginInstanceDescendantListViewTests(ViewTests):
    """
    Test the plugininstance-descendant-list view.
    """

    def setUp(self):
        super(PluginInstanceDescendantListViewTests, self).setUp()

        user = User.objects.get(username=self.username)

        # create an 'fs' plugin instance
        plugin = Plugin.objects.get(meta__name="pacspull")
        (fs_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])

        # create a tree of 'ds' plugin instances
        plugin = Plugin.objects.get(meta__name="mri_convert")
        PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, previous=fs_inst,
            compute_resource=plugin.compute_resources.all()[0])

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_info', type='ds')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()
        (ds_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, previous=fs_inst,
            compute_resource=plugin.compute_resources.all()[0])

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_surf', type='ds')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()
        PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, previous=ds_inst,
            compute_resource=plugin.compute_resources.all()[0])

        self.list_url = reverse("plugininstance-descendant-list", kwargs={"pk": fs_inst.id})

    def test_plugin_instance_descendant_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        # response should contain all the instances in the tree
        self.assertContains(response, "pacspull")
        self.assertContains(response, "mri_convert")
        self.assertContains(response, "mri_info")
        self.assertContains(response, "mri_surf")

    def test_plugin_instance_descendant_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginInstanceSplitListViewTests(ViewTests):
    """
    Test the plugininstancesplit-list view.
    """

    def setUp(self):
        super(PluginInstanceSplitListViewTests, self).setUp()

        user = User.objects.get(username=self.username)

        # create an 'fs' plugin instance
        plugin = Plugin.objects.get(meta__name="pacspull")
        (self.fs_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])

        # create a 'ts' plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pl-topologicalcopy', type='ts')
        (plugin_ts, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ts.compute_resources.set([self.compute_resource])
        plugin_ts.save()

        self.create_read_url = reverse("plugininstancesplit-list", kwargs={"pk": self.fs_inst.id})

    def test_plugin_instance_split_create_failure_access_denied(self):
        post = json.dumps({"template": {"data": [{"name": "filter", "value": ""}]}})
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.post(self.create_read_url, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_plugin_instance_split_create_success(self):
        post = json.dumps({"template": {"data": [{"name": "filter", "value": ""}]}})

        # add parameters to the plugin before the POST request
        plugin = Plugin.objects.get(meta__name="pl-topologicalcopy")
        PluginParameter.objects.get_or_create(plugin=plugin, name='filter', type='string')
        PluginParameter.objects.get_or_create(plugin=plugin, name='plugininstances',
                                              type='string')

        self.client.login(username=self.username, password=self.password)

        # make API requests
        response = self.client.post(self.create_read_url, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        self.fs_inst.status = 'finishedSuccessfully'
        self.fs_inst.save()
        with mock.patch.object(views.run_plugin_instance, 'delay',
                               return_value=None) as delay_mock:

            response = self.client.post(self.create_read_url, data=post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
            # check that the run_plugin_instance task was called with appropriate args
            delay_mock.assert_called_once()

    def test_plugin_instance_split_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        # response should contain all the instances in the tree
        self.assertContains(response, "filter")

    def test_plugin_instance_split_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginInstanceParameterListViewTests(ViewTests):
    """
    Test the plugininstance-parameter-list view.
    """

    def setUp(self):
        super(PluginInstanceParameterListViewTests, self).setUp()

        user = User.objects.get(username=self.username)

        # create a plugin
        plugin = Plugin.objects.get(meta__name="pacspull")
        parameters = [{"type": "path", "name": "param1", "flag": "--param1"},
                      {"type": "float", "name": "param2", "flag": "--param2"}]

        # add plugin's parameters
        (param1, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'])
        (param2, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[1]['name'],
            type=parameters[1]['type'],
            flag=parameters[1]['flag'])

        # create a plugin instance
        (inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])

        # create two plugin parameter instances associated to the plugin instance
        PathParameter.objects.get_or_create(plugin_inst=inst, plugin_param=param1,
                                            value=self.username)
        FloatParameter.objects.get_or_create(plugin_inst=inst, plugin_param=param2,
                                             value=3.14)

        self.list_url = reverse("plugininstance-parameter-list", kwargs={"pk": inst.id})

    def test_plugin_instance_parameter_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "param1")
        self.assertContains(response, self.username)
        self.assertContains(response, "param2")
        self.assertContains(response, 3.14)

    def test_plugin_instance_parameter_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginInstanceFileViewTests(ViewTests):
    """
    Generic plugin instance file view tests' setup and tearDown.
    """

    def setUp(self):
        super().setUp()
        # create a plugin instance
        user = User.objects.get(username=self.username)
        plugin = Plugin.objects.get(meta__name="pacspull")
        (self.plg_inst, tf) = PluginInstance.objects.get_or_create(
            plugin=plugin, owner=user, compute_resource=plugin.compute_resources.all()[0])

        # create a plugin instance file associated to the plugin instance
        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        # upload file to Swift storage
        self.path = 'tests/file1.txt'
        with io.StringIO("test file") as file1:
            self.swift_manager.upload_obj(self.path, file1.read(),
                                          content_type='text/plain')
        (self.plg_inst_file, tf) = PluginInstanceFile.objects.get_or_create(
            plugin_inst=self.plg_inst
        )
        self.plg_inst_file.fname.name = self.path
        self.plg_inst_file.save()

        # create test directory where files are created
        # self.test_dir = settings.MEDIA_ROOT + '/test'
        # settings.MEDIA_ROOT = self.test_dir
        # if not os.path.exists(self.test_dir):
        #     os.makedirs(self.test_dir)

    def tearDown(self):
        # delete file from Swift storage
        self.swift_manager.delete_obj(self.path)
        super().tearDown()

        # remove test directory
        # shutil.rmtree(self.test_dir)
        # settings.MEDIA_ROOT = os.path.dirname(self.test_dir)


class PluginInstanceFileListViewTests(PluginInstanceFileViewTests):
    """
    Test the plugininstancefile-list view.
    """

    def setUp(self):
        super(PluginInstanceFileListViewTests, self).setUp()

        self.list_url = reverse("plugininstancefile-list", kwargs={"pk": self.plg_inst.id})

    def test_plugin_instance_file_create_failure_post_not_allowed(self):
        self.client.login(username=self.username, password=self.password)
        # try to create a new plugin file with a POST request to the list
        # POST request using multipart/form-data to be able to upload file
        with io.StringIO("test file") as f:
            post = {"fname": f}
            response = self.client.post(self.list_url, data=post)
        self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

    def test_plugin_instance_file_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "file1.txt")

    def test_plugin_instance_file_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_instance_file_list_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class AllPluginInstanceFileListViewTests(PluginInstanceFileViewTests):
    """
    Test the allplugininstancefile-list view.
    """

    def setUp(self):
        super(AllPluginInstanceFileListViewTests, self).setUp()

        self.list_url = reverse("allplugininstancefile-list")

    def test_all_plugin_instance_file_create_failure_post_not_allowed(self):
        self.client.login(username=self.username, password=self.password)
        # try to create a new plugin file with a POST request to the list
        # POST request using multipart/form-data to be able to upload file
        with io.StringIO("test file") as f:
            post = {"fname": f}
            response = self.client.post(self.list_url, data=post)
        self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

    def test_all_plugin_instance_file_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "file1.txt")

    def test_all_plugin_instance_file_list_from_shared_feed_success(self):
        self.client.login(username=self.other_username, password=self.other_password)
        plg_inst = self.plg_inst
        user1 = User.objects.get(username=self.username)
        user2 = User.objects.get(username=self.other_username)
        plg_inst.feed.owner.set([user1, user2])
        response = self.client.get(self.list_url)
        self.assertContains(response, "file1.txt")

    def test_all_plugin_instance_file_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_all_plugin_instance_file_list_files_in_not_owned_feeds_inaccessible(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.list_url)
        self.assertNotContains(response, "file1.txt")


class AllPluginInstanceFileListQuerySearchViewTests(PluginInstanceFileViewTests):
    """
    Test the allplugininstancefile-list-query-search view.
    """

    def setUp(self):
        super(AllPluginInstanceFileListQuerySearchViewTests, self).setUp()

        self.list_url = reverse("allplugininstancefile-list-query-search") + '?id=' + \
                        str(self.plg_inst_file.id)

    def test_plugin_instance_query_search_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, 'file1.txt')

    def test_plugin_instance_query_search_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginInstanceFileDetailViewTests(PluginInstanceFileViewTests):
    """
    Test the plugininstancefile-detail view.
    """

    def setUp(self):
        super(PluginInstanceFileDetailViewTests, self).setUp()
        #self.corresponding_feed_url = reverse("feed-detail", kwargs={"pk": feed.id})
        self.corresponding_plugin_instance_url = reverse("plugininstance-detail",
                                                         kwargs={"pk": self.plg_inst.id})

        self.read_url = reverse("plugininstancefile-detail",
                                kwargs={"pk": self.plg_inst_file.id})

    def test_plugin_instance_file_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, "file1.txt")
        self.assertTrue(response.data["plugin_inst"].endswith(
            self.corresponding_plugin_instance_url))

    def test_plugin_instance_file_detail_success_user_chris(self):
        self.client.login(username=self.chris_username, password=self.chris_password)
        response = self.client.get(self.read_url)
        self.assertContains(response, "file1.txt")
        self.assertTrue(response.data["plugin_inst"].endswith(
            self.corresponding_plugin_instance_url))

    def test_plugin_instance_file_detail_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_plugin_instance_file_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class FileResourceViewTests(PluginInstanceFileViewTests):
    """
    Test the plugininstancefile-resource view.
    """

    def setUp(self):
        super(FileResourceViewTests, self).setUp()
        self.download_url = reverse("plugininstancefile-resource",
                                    kwargs={"pk": self.plg_inst_file.id}) + 'file1.txt'

    def test_fileresource_get(self):
        plg_inst_file = PluginInstanceFile.objects.get(fname="tests/file1.txt")
        fileresource_view_inst = mock.Mock()
        fileresource_view_inst.get_object = mock.Mock(return_value=plg_inst_file)
        request_mock = mock.Mock()
        with mock.patch('plugininstances.views.FileResponse') as response_mock:
            views.FileResource.get(fileresource_view_inst, request_mock)
            response_mock.assert_called_with(plg_inst_file.fname)

    @tag('integration')
    def test_integration_fileresource_download_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, 200)
        content = [c for c in response.streaming_content][0].decode('utf-8')
        self.assertEqual(content, "test file")

    def test_fileresource_download_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_fileresource_download_failure_unauthenticated(self):
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/plugins/migrations/0001_initial.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-06-02 18:19
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Plugin',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('modification_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(max_length=100)),
                ('type', models.CharField(default='ds', max_length=4)),
            ],
            options={
                'ordering': ('type',),
            },
        ),
    ]


# chris_backend/plugins/migrations/0002_auto_20160706_1417.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-06 18:17
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='BoolParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.BooleanField(default=False)),
            ],
        ),
        migrations.CreateModel(
            name='IntParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.IntegerField()),
            ],
        ),
        migrations.CreateModel(
            name='PluginInstance',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('start_date', models.DateTimeField(auto_now_add=True)),
                ('end_date', models.DateTimeField(auto_now_add=True)),
                ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugin_inst', to='plugins.Plugin')),
            ],
            options={
                'ordering': ('start_date',),
            },
        ),
        migrations.CreateModel(
            name='PluginParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('start_date', models.DateTimeField(auto_now_add=True)),
                ('end_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(max_length=100)),
                ('optional', models.BooleanField(default=False)),
                ('type', models.CharField(choices=[('string', 'String values'), ('double', 'Float values'), ('boolean', 'Boolean values'), ('integer', 'Integer values')], default='string', max_length=10)),
                ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameter', to='plugins.Plugin')),
            ],
            options={
                'ordering': ('start_date',),
            },
        ),
        migrations.CreateModel(
            name='StringParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(max_length=200)),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_param', to='plugins.PluginInstance')),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_inst', to='plugins.PluginInstance')),
            ],
        ),
        migrations.AddField(
            model_name='intparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='int_param', to='plugins.PluginInstance'),
        ),
        migrations.AddField(
            model_name='intparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='int_inst', to='plugins.PluginInstance'),
        ),
        migrations.AddField(
            model_name='boolparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bool_param', to='plugins.PluginInstance'),
        ),
        migrations.AddField(
            model_name='boolparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bool_inst', to='plugins.PluginInstance'),
        ),
    ]


# chris_backend/plugins/migrations/0003_auto_20160707_1057.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-07 14:57
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0002_auto_20160706_1417'),
    ]

    operations = [
        migrations.CreateModel(
            name='FloatParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.FloatField()),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_param', to='plugins.PluginInstance')),
                ('plugin_param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_inst', to='plugins.PluginInstance')),
            ],
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='type',
            field=models.CharField(choices=[('string', 'String values'), ('float', 'Float values'), ('boolean', 'Boolean values'), ('integer', 'Integer values')], default='string', max_length=10),
        ),
    ]


# chris_backend/plugins/migrations/0004_auto_20160708_1529.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-08 19:29
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('plugins', '0003_auto_20160707_1057'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='pluginparameter',
            options={'ordering': ('plugin',)},
        ),
        migrations.RemoveField(
            model_name='pluginparameter',
            name='end_date',
        ),
        migrations.RemoveField(
            model_name='pluginparameter',
            name='start_date',
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='owner',
            field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
            preserve_default=False,
        ),
    ]


# chris_backend/plugins/migrations/0005_auto_20160712_1438.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-12 18:38
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0004_auto_20160708_1529'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='plugin',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='plugins.Plugin'),
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='optional',
            field=models.BooleanField(default=True),
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='plugin',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='plugins.Plugin'),
        ),
    ]


# chris_backend/plugins/migrations/0006_auto_20160713_1534.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-13 19:34
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0005_auto_20160712_1438'),
    ]

    operations = [
        migrations.AlterField(
            model_name='boolparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bool_inst', to='plugins.PluginParameter'),
        ),
        migrations.AlterField(
            model_name='floatparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_inst', to='plugins.PluginParameter'),
        ),
        migrations.AlterField(
            model_name='intparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='int_inst', to='plugins.PluginParameter'),
        ),
        migrations.AlterField(
            model_name='stringparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_inst', to='plugins.PluginParameter'),
        ),
    ]


# chris_backend/plugins/migrations/0007_auto_20160725_1003.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-07-25 14:03
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0006_auto_20160713_1534'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='name',
            field=models.CharField(max_length=100, unique=True),
        ),
    ]


# chris_backend/plugins/migrations/0008_plugininstance_previous.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-08-02 20:38
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0007_auto_20160725_1003'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugininstance',
            name='previous',
            field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='plugins.PluginInstance'),
            preserve_default=False,
        ),
    ]


# chris_backend/plugins/migrations/0009_auto_20160802_1651.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-08-02 20:51
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0008_plugininstance_previous'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='previous',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='plugins.PluginInstance'),
        ),
    ]


# chris_backend/plugins/migrations/0010_auto_20160808_0950.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-08-08 13:50
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0009_auto_20160802_1651'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='previous',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='next', to='plugins.PluginInstance'),
        ),
    ]


# chris_backend/plugins/migrations/0011_auto_20160829_1538.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-08-29 19:38
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0010_auto_20160808_0950'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='type',
            field=models.CharField(choices=[('ds', 'Data plugin'), ('fs', 'Filesystem plugin')], default='ds', max_length=4),
        ),
    ]


# chris_backend/plugins/migrations/0012_plugininstance_status.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-09-07 19:32
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0011_auto_20160829_1538'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugininstance',
            name='status',
            field=models.CharField(default='finished', max_length=30),
            preserve_default=False,
        ),
    ]


# chris_backend/plugins/migrations/0013_auto_20160907_1556.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-09-07 19:56
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0012_plugininstance_status'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugininstance',
            name='status',
            field=models.CharField(default='started', max_length=30),
        ),
    ]


# chris_backend/plugins/migrations/0014_auto_20160921_1027.py

# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-09-21 14:27
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0013_auto_20160907_1556'),
    ]

    operations = [
        migrations.AddField(
            model_name='pluginparameter',
            name='default',
            field=models.CharField(blank=True, max_length=200),
        ),
        migrations.AddField(
            model_name='pluginparameter',
            name='help',
            field=models.TextField(blank=True),
        ),
        migrations.AlterField(
            model_name='stringparameter',
            name='value',
            field=models.CharField(blank=True, max_length=200),
        ),
    ]


# chris_backend/plugins/migrations/0015_plugin_dock_image.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2017-03-22 18:47
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0014_auto_20160921_1027'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugin',
            name='dock_image',
            field=models.CharField(default='fnndsc/pl-simplefsapp', max_length=500),
            preserve_default=False,
        ),
    ]


# chris_backend/plugins/migrations/0016_auto_20171018_1242.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2017-10-18 16:42
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0015_plugin_dock_image'),
    ]

    operations = [
        migrations.CreateModel(
            name='PathParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
                ('plugin_inst', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_param', to='plugins.PluginInstance')),
            ],
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='type',
            field=models.CharField(choices=[('string', 'String values'), ('float', 'Float values'), ('boolean', 'Boolean values'), ('integer', 'Integer values'), ('path', 'Path values')], default='string', max_length=10),
        ),
        migrations.AddField(
            model_name='pathparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_inst', to='plugins.PluginParameter'),
        ),
    ]


# chris_backend/plugins/migrations/0017_auto_20180125_1425.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2018-01-25 19:25
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0016_auto_20171018_1242'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugin',
            name='authors',
            field=models.CharField(blank=True, max_length=200),
        ),
        migrations.AddField(
            model_name='plugin',
            name='category',
            field=models.CharField(blank=True, max_length=100),
        ),
        migrations.AddField(
            model_name='plugin',
            name='description',
            field=models.CharField(blank=True, max_length=800),
        ),
        migrations.AddField(
            model_name='plugin',
            name='documentation',
            field=models.CharField(blank=True, max_length=800),
        ),
        migrations.AddField(
            model_name='plugin',
            name='license',
            field=models.CharField(blank=True, max_length=50),
        ),
        migrations.AddField(
            model_name='plugin',
            name='title',
            field=models.CharField(blank=True, max_length=400),
        ),
        migrations.AddField(
            model_name='plugin',
            name='version',
            field=models.CharField(blank=True, max_length=10),
        ),
    ]


# chris_backend/plugins/migrations/0018_auto_20180423_1440.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2018-04-23 18:40
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import plugins.fields


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0017_auto_20180125_1425'),
    ]

    operations = [
        migrations.CreateModel(
            name='ComputeResource',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('compute_resource_identifier', models.CharField(max_length=100)),
            ],
        ),
        migrations.AddField(
            model_name='plugin',
            name='max_cpu_limit',
            field=plugins.fields.CPUField(default=2147483647, null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='max_gpu_limit',
            field=models.IntegerField(null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='max_memory_limit',
            field=plugins.fields.MemoryField(default=2147483647, null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='max_number_of_workers',
            field=models.IntegerField(default=2147483647, null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='min_cpu_limit',
            field=plugins.fields.CPUField(default=1000, null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='min_gpu_limit',
            field=models.IntegerField(null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='min_memory_limit',
            field=plugins.fields.MemoryField(default=200, null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='min_number_of_workers',
            field=models.IntegerField(default=1, null=True),
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='cpu_limit',
            field=plugins.fields.CPUField(null=True),
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='gpu_limit',
            field=models.IntegerField(null=True),
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='memory_limit',
            field=plugins.fields.MemoryField(null=True),
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='number_of_workers',
            field=models.IntegerField(null=True),
        ),
        migrations.AddField(
            model_name='plugin',
            name='compute_resource',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='plugins.ComputeResource'),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='compute_resource',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='plugin_instances', to='plugins.ComputeResource'),
            preserve_default=False,
        ),
    ]


# chris_backend/plugins/migrations/0019_auto_20180430_1430.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2018-04-30 18:30
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0018_auto_20180423_1440'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugin',
            name='execshell',
            field=models.CharField(default='', max_length=50),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='plugin',
            name='selfexec',
            field=models.CharField(default='', max_length=50),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='plugin',
            name='selfpath',
            field=models.CharField(default='', max_length=512),
            preserve_default=False,
        ),
        migrations.AlterField(
            model_name='plugin',
            name='authors',
            field=models.CharField(max_length=200),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='category',
            field=models.CharField(max_length=100),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='description',
            field=models.CharField(max_length=800),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='license',
            field=models.CharField(max_length=50),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='title',
            field=models.CharField(max_length=400),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='version',
            field=models.CharField(max_length=10),
        ),
    ]


# chris_backend/plugins/migrations/0020_auto_20180502_1707.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2018-05-02 21:07
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0019_auto_20180430_1430'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='max_gpu_limit',
            field=models.IntegerField(default=2147483647, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='min_gpu_limit',
            field=models.IntegerField(default=0, null=True),
        ),
    ]


# chris_backend/plugins/migrations/0021_auto_20180504_1608.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2018-05-04 20:08
from __future__ import unicode_literals

from django.db import migrations, models
import plugins.fields


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0020_auto_20180502_1707'),
    ]

    operations = [
        migrations.AddField(
            model_name='pluginparameter',
            name='action',
            field=models.CharField(default='store', max_length=20),
        ),
        migrations.AddField(
            model_name='pluginparameter',
            name='flag',
            field=models.CharField(default='', max_length=52),
            preserve_default=False,
        ),
        migrations.AlterField(
            model_name='plugin',
            name='category',
            field=models.CharField(blank=True, max_length=100),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='max_cpu_limit',
            field=plugins.fields.CPUField(blank=True, default=2147483647, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='max_gpu_limit',
            field=models.IntegerField(blank=True, default=2147483647, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='max_memory_limit',
            field=plugins.fields.MemoryField(blank=True, default=2147483647, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='max_number_of_workers',
            field=models.IntegerField(blank=True, default=2147483647, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='min_cpu_limit',
            field=plugins.fields.CPUField(blank=True, default=1000, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='min_gpu_limit',
            field=models.IntegerField(blank=True, default=0, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='min_memory_limit',
            field=plugins.fields.MemoryField(blank=True, default=200, null=True),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='min_number_of_workers',
            field=models.IntegerField(blank=True, default=1, null=True),
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='name',
            field=models.CharField(max_length=50),
        ),
    ]


# chris_backend/plugins/migrations/0022_auto_20181010_1646.py

# Generated by Django 2.0.7 on 2018-10-10 16:46

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0021_auto_20180504_1608'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='plugininstance',
            options={'ordering': ('-start_date',)},
        ),
        migrations.AlterField(
            model_name='boolparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_param', to='plugins.PluginInstance'),
        ),
        migrations.AlterField(
            model_name='boolparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_inst', to='plugins.PluginParameter'),
        ),
        migrations.AlterField(
            model_name='intparameter',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_param', to='plugins.PluginInstance'),
        ),
        migrations.AlterField(
            model_name='intparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_inst', to='plugins.PluginParameter'),
        ),
    ]


# chris_backend/plugins/migrations/0023_auto_20190114_1437.py

# Generated by Django 2.1.4 on 2019-01-14 14:37

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('feeds', '0008_auto_20190114_1437'),
        ('plugins', '0022_auto_20181010_1646'),
    ]

    operations = [
        migrations.CreateModel(
            name='PluginInstanceFile',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('fname', models.FileField(max_length=2048, upload_to='')),
            ],
            options={
                'ordering': ('fname',),
            },
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='feed',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='plugin_instances', to='feeds.Feed'),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='plugininstance',
            name='title',
            field=models.CharField(blank=True, max_length=100),
        ),
        migrations.AlterField(
            model_name='boolparameter',
            name='value',
            field=models.BooleanField(blank=True, default=False),
        ),
        migrations.AddField(
            model_name='plugininstancefile',
            name='plugin_inst',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='plugins.PluginInstance'),
        ),
    ]


# chris_backend/plugins/migrations/0024_auto_20190116_1917.py

# Generated by Django 2.1.4 on 2019-01-16 19:17

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0023_auto_20190114_1437'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='boolparameter',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='boolparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='floatparameter',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='floatparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='intparameter',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='intparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='pathparameter',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='pathparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='plugininstance',
            name='compute_resource',
        ),
        migrations.RemoveField(
            model_name='plugininstance',
            name='feed',
        ),
        migrations.RemoveField(
            model_name='plugininstance',
            name='owner',
        ),
        migrations.RemoveField(
            model_name='plugininstance',
            name='plugin',
        ),
        migrations.RemoveField(
            model_name='plugininstance',
            name='previous',
        ),
        migrations.RemoveField(
            model_name='plugininstancefile',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='stringparameter',
            name='plugin_inst',
        ),
        migrations.RemoveField(
            model_name='stringparameter',
            name='plugin_param',
        ),
        migrations.DeleteModel(
            name='BoolParameter',
        ),
        migrations.DeleteModel(
            name='FloatParameter',
        ),
        migrations.DeleteModel(
            name='IntParameter',
        ),
        migrations.DeleteModel(
            name='PathParameter',
        ),
        migrations.DeleteModel(
            name='PluginInstance',
        ),
        migrations.DeleteModel(
            name='PluginInstanceFile',
        ),
        migrations.DeleteModel(
            name='StringParameter',
        ),
    ]


# chris_backend/plugins/migrations/0025_auto_20190122_1745.py

# Generated by Django 2.1.4 on 2019-01-22 17:45

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('plugins', '0024_auto_20190116_1917'),
    ]

    operations = [
        migrations.CreateModel(
            name='Pipeline',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('modification_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(max_length=100, unique=True)),
                ('authors', models.CharField(blank=True, max_length=200)),
                ('category', models.CharField(blank=True, max_length=100)),
                ('description', models.CharField(blank=True, max_length=800)),
                ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'ordering': ('category',),
            },
        ),
        migrations.CreateModel(
            name='PluginPiping',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('position', models.PositiveIntegerField()),
                ('pipeline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.Pipeline')),
                ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.Plugin')),
            ],
        ),
        migrations.AddField(
            model_name='pipeline',
            name='plugins',
            field=models.ManyToManyField(related_name='pipelines', through='plugins.PluginPiping', to='plugins.Plugin'),
        ),
        migrations.AlterUniqueTogether(
            name='pluginpiping',
            unique_together={('pipeline', 'position')},
        ),
    ]


# chris_backend/plugins/migrations/0026_auto_20190128_1615.py

# Generated by Django 2.1.4 on 2019-01-28 16:15

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0025_auto_20190122_1745'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='pluginpiping',
            options={'ordering': ('pipeline',)},
        ),
        migrations.AddField(
            model_name='pluginpiping',
            name='previous',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='next', to='plugins.PluginPiping'),
        ),
        migrations.AlterUniqueTogether(
            name='pluginpiping',
            unique_together=set(),
        ),
        migrations.RemoveField(
            model_name='pluginpiping',
            name='position',
        ),
    ]


# chris_backend/plugins/migrations/0027_auto_20190212_1818.py

# Generated by Django 2.1.4 on 2019-02-12 18:18

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0026_auto_20190128_1615'),
    ]

    operations = [
        migrations.CreateModel(
            name='DefaultBoolParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.BooleanField()),
            ],
        ),
        migrations.CreateModel(
            name='DefaultFloatParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.FloatField()),
            ],
        ),
        migrations.CreateModel(
            name='DefaultIntParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.IntegerField()),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPathParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingBoolParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.BooleanField(blank=True, default=False)),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingFloatParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.FloatField(blank=True, default=1.0)),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingIntParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.IntegerField(blank=True, default=1)),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingPathParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
            ],
        ),
        migrations.CreateModel(
            name='DefaultPipingStrParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
            ],
        ),
        migrations.CreateModel(
            name='DefaultStrParameter',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('value', models.CharField(blank=True, max_length=200)),
            ],
        ),
        migrations.RemoveField(
            model_name='pluginparameter',
            name='default',
        ),
        migrations.AddField(
            model_name='pipeline',
            name='locked',
            field=models.BooleanField(default=True),
        ),
        migrations.AlterField(
            model_name='pluginpiping',
            name='pipeline',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugin_pipings', to='plugins.Pipeline'),
        ),
        migrations.AddField(
            model_name='defaultstrparameter',
            name='plugin_param',
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='string_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultpipingstrparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_piping_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultpipingstrparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='string_param', to='plugins.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingpathparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_piping_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultpipingpathparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='path_param', to='plugins.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingintparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_piping_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultpipingintparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integer_param', to='plugins.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingfloatparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_piping_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultpipingfloatparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='float_param', to='plugins.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpipingboolparameter',
            name='plugin_param',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_piping_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultpipingboolparameter',
            name='plugin_piping',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_param', to='plugins.PluginPiping'),
        ),
        migrations.AddField(
            model_name='defaultpathparameter',
            name='plugin_param',
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='path_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultintparameter',
            name='plugin_param',
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='integer_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultfloatparameter',
            name='plugin_param',
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='float_default', to='plugins.PluginParameter'),
        ),
        migrations.AddField(
            model_name='defaultboolparameter',
            name='plugin_param',
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='boolean_default', to='plugins.PluginParameter'),
        ),
    ]


# chris_backend/plugins/migrations/0028_auto_20190221_1803.py

# Generated by Django 2.1.4 on 2019-02-21 18:03

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugininstances', '0005_auto_20190221_1803'),
        ('plugins', '0027_auto_20190212_1818'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='defaultpipingboolparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='defaultpipingboolparameter',
            name='plugin_piping',
        ),
        migrations.RemoveField(
            model_name='defaultpipingfloatparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='defaultpipingfloatparameter',
            name='plugin_piping',
        ),
        migrations.RemoveField(
            model_name='defaultpipingintparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='defaultpipingintparameter',
            name='plugin_piping',
        ),
        migrations.RemoveField(
            model_name='defaultpipingpathparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='defaultpipingpathparameter',
            name='plugin_piping',
        ),
        migrations.RemoveField(
            model_name='defaultpipingstrparameter',
            name='plugin_param',
        ),
        migrations.RemoveField(
            model_name='defaultpipingstrparameter',
            name='plugin_piping',
        ),
        migrations.RemoveField(
            model_name='pipeline',
            name='owner',
        ),
        migrations.RemoveField(
            model_name='pipeline',
            name='plugins',
        ),
        migrations.RemoveField(
            model_name='pluginpiping',
            name='pipeline',
        ),
        migrations.RemoveField(
            model_name='pluginpiping',
            name='plugin',
        ),
        migrations.RemoveField(
            model_name='pluginpiping',
            name='previous',
        ),
        migrations.DeleteModel(
            name='DefaultPipingBoolParameter',
        ),
        migrations.DeleteModel(
            name='DefaultPipingFloatParameter',
        ),
        migrations.DeleteModel(
            name='DefaultPipingIntParameter',
        ),
        migrations.DeleteModel(
            name='DefaultPipingPathParameter',
        ),
        migrations.DeleteModel(
            name='DefaultPipingStrParameter',
        ),
        migrations.DeleteModel(
            name='Pipeline',
        ),
        migrations.DeleteModel(
            name='PluginPiping',
        ),
    ]


# chris_backend/plugins/migrations/0029_auto_20190423_1448.py

# Generated by Django 2.1.4 on 2019-04-23 14:48

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0028_auto_20190221_1803'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='name',
            field=models.CharField(max_length=100),
        ),
        migrations.AlterUniqueTogether(
            name='plugin',
            unique_together={('name', 'version')},
        ),
    ]


# chris_backend/plugins/migrations/0030_auto_20190607_1946.py

# Generated by Django 2.1.4 on 2019-06-07 19:46

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0029_auto_20190423_1448'),
    ]

    operations = [
        migrations.AddField(
            model_name='pluginparameter',
            name='ui_exposed',
            field=models.BooleanField(default=True),
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='optional',
            field=models.BooleanField(default=False),
        ),
    ]


# chris_backend/plugins/migrations/0031_auto_20200211_1555.py

# Generated by Django 2.1.4 on 2020-02-11 15:55

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0030_auto_20190607_1946'),
    ]

    operations = [
        migrations.AlterField(
            model_name='defaultpathparameter',
            name='value',
            field=models.CharField(max_length=1024),
        ),
        migrations.AlterField(
            model_name='defaultstrparameter',
            name='value',
            field=models.CharField(blank=True, max_length=600),
        ),
    ]


# chris_backend/plugins/migrations/0032_auto_20200213_0040.py

# Generated by Django 2.1.4 on 2020-02-13 00:40

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0031_auto_20200211_1555'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='defaultpathparameter',
            name='plugin_param',
        ),
        migrations.AlterField(
            model_name='pluginparameter',
            name='type',
            field=models.CharField(choices=[('string', 'String values'), ('float', 'Float values'), ('boolean', 'Boolean values'), ('integer', 'Integer values'), ('path', 'Path values'), ('unextpath', 'Unextracted path values')], default='string', max_length=10),
        ),
        migrations.DeleteModel(
            name='DefaultPathParameter',
        ),
    ]


# chris_backend/plugins/migrations/0033_pluginparameter_short_flag.py

# Generated by Django 2.1.4 on 2020-03-20 18:28

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0032_auto_20200213_0040'),
    ]

    operations = [
        migrations.AddField(
            model_name='pluginparameter',
            name='short_flag',
            field=models.CharField(blank=True, max_length=52),
        ),
    ]


# chris_backend/plugins/migrations/0034_auto_20200420_2320.py

# Generated by Django 2.1.4 on 2020-04-20 23:20

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0033_pluginparameter_short_flag'),
    ]

    operations = [
        migrations.AlterField(
            model_name='computeresource',
            name='compute_resource_identifier',
            field=models.CharField(max_length=100, unique=True),
        ),
    ]


# chris_backend/plugins/migrations/0035_auto_20200514_2313.py

# Generated by Django 2.2.12 on 2020-05-15 03:13

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0034_auto_20200420_2320'),
    ]

    operations = [
        migrations.RenameField(
            model_name='computeresource',
            old_name='compute_resource_identifier',
            new_name='name',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='compute_resource',
        ),
        migrations.AddField(
            model_name='computeresource',
            name='creation_date',
            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='computeresource',
            name='description',
            field=models.CharField(default=django.utils.timezone.now, max_length=600),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='computeresource',
            name='modification_date',
            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='plugin',
            name='compute_resources',
            field=models.ManyToManyField(related_name='plugins', to='plugins.ComputeResource'),
        ),
    ]


# chris_backend/plugins/migrations/0036_plugin_icon.py

# Generated by Django 2.2.12 on 2020-05-15 23:19

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0035_auto_20200514_2313'),
    ]

    operations = [
        migrations.AddField(
            model_name='plugin',
            name='icon',
            field=models.URLField(blank=True, max_length=300),
        ),
    ]


# chris_backend/plugins/migrations/0037_auto_20200518_1959.py

# Generated by Django 2.2.12 on 2020-05-18 23:59

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0036_plugin_icon'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='type',
            field=models.CharField(choices=[('ds', 'Data synthesis'), ('fs', 'Feed synthesis')], default='ds', max_length=4),
        ),
    ]


# chris_backend/plugins/migrations/0038_auto_20200610_1922.py

# Generated by Django 2.2.12 on 2020-06-10 23:22

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0037_auto_20200518_1959'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='max_gpu_limit',
            field=models.IntegerField(blank=True, default=0, null=True),
        ),
    ]


# chris_backend/plugins/migrations/0039_auto_20200610_2319.py

# Generated by Django 2.2.12 on 2020-06-11 03:19

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0038_auto_20200610_1922'),
    ]

    operations = [
        migrations.CreateModel(
            name='PluginMeta',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('modification_date', models.DateTimeField(auto_now_add=True)),
                ('name', models.CharField(max_length=100, unique=True)),
                ('stars', models.IntegerField(default=0)),
                ('public_repo', models.URLField(blank=True, max_length=300)),
                ('license', models.CharField(blank=True, max_length=50)),
                ('type', models.CharField(choices=[('ds', 'Data synthesis'), ('fs', 'Feed synthesis')], default='ds', max_length=4)),
                ('icon', models.URLField(blank=True, max_length=300)),
                ('category', models.CharField(blank=True, max_length=100)),
                ('authors', models.CharField(blank=True, max_length=200)),
            ],
            options={
                'ordering': ('type', '-creation_date'),
            },
        ),
        migrations.AlterModelOptions(
            name='plugin',
            options={'ordering': ('meta', '-creation_date')},
        ),
        migrations.AlterField(
            model_name='plugin',
            name='description',
            field=models.CharField(blank=True, max_length=800),
        ),
        migrations.AlterField(
            model_name='plugin',
            name='title',
            field=models.CharField(blank=True, max_length=400),
        ),
        migrations.AddField(
            model_name='plugin',
            name='meta',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='plugins.PluginMeta'),
            preserve_default=False,
        ),
        migrations.AlterUniqueTogether(
            name='plugin',
            unique_together={('meta', 'version')},
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='authors',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='category',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='icon',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='license',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='modification_date',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='name',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='type',
        ),
    ]


# chris_backend/plugins/migrations/0040_auto_20200811_1830.py

# Generated by Django 2.2.12 on 2020-08-11 22:30

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0039_auto_20200610_2319'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='plugin',
            name='documentation',
        ),
        migrations.RemoveField(
            model_name='plugin',
            name='title',
        ),
        migrations.AddField(
            model_name='pluginmeta',
            name='documentation',
            field=models.CharField(blank=True, max_length=800),
        ),
        migrations.AddField(
            model_name='pluginmeta',
            name='title',
            field=models.CharField(blank=True, max_length=400),
        ),
    ]


# chris_backend/plugins/migrations/0041_auto_20200818_1739.py

# Generated by Django 2.2.12 on 2020-08-18 21:39

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0040_auto_20200811_1830'),
    ]

    operations = [
        migrations.AddField(
            model_name='computeresource',
            name='compute_url',
            field=models.URLField(default='http://pfcon.remote:5005/api/v1/', max_length=300),
            preserve_default=False,
        ),
        migrations.AlterField(
            model_name='computeresource',
            name='description',
            field=models.CharField(blank=True, max_length=600),
        ),
    ]


# chris_backend/plugins/migrations/0042_auto_20201130_1317.py

# Generated by Django 2.2.12 on 2020-11-30 18:17

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0041_auto_20200818_1739'),
    ]

    operations = [
        migrations.AlterField(
            model_name='plugin',
            name='description',
            field=models.CharField(blank=True, max_length=2000),
        ),
    ]


# chris_backend/plugins/migrations/0043_auto_20201208_1527.py

# Generated by Django 2.2.12 on 2020-12-08 20:27

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0042_auto_20201130_1317'),
    ]

    operations = [
        migrations.AlterUniqueTogether(
            name='plugin',
            unique_together={('meta', 'version'), ('meta', 'dock_image')},
        ),
    ]


# chris_backend/plugins/migrations/0044_auto_20210126_1615.py

# Generated by Django 2.2.12 on 2021-01-26 21:15

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0043_auto_20201208_1527'),
    ]

    operations = [
        migrations.AlterField(
            model_name='pluginmeta',
            name='type',
            field=models.CharField(choices=[('ds', 'Data synthesis'), ('fs', 'Feed synthesis'), ('ts', 'Topology synthesis')], default='ds', max_length=4),
        ),
    ]


# chris_backend/plugins/migrations/0045_computeresource_max_job_exec_seconds.py

# Generated by Django 2.2.24 on 2021-12-30 23:33

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0044_auto_20210126_1615'),
    ]

    operations = [
        migrations.AddField(
            model_name='computeresource',
            name='max_job_exec_seconds',
            field=models.IntegerField(blank=True, default=86400),
        ),
    ]


# chris_backend/plugins/migrations/0046_auto_20220122_1737.py

# Generated by Django 2.2.24 on 2022-01-22 22:37

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('plugins', '0045_computeresource_max_job_exec_seconds'),
    ]

    operations = [
        migrations.AddField(
            model_name='computeresource',
            name='compute_auth_token',
            field=models.CharField(blank=True, default='initial_token', max_length=500),
        ),
        migrations.AddField(
            model_name='computeresource',
            name='compute_auth_url',
            field=models.URLField(blank=True, max_length=350),
        ),
        migrations.AddField(
            model_name='computeresource',
            name='compute_password',
            field=models.CharField(max_length=100),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='computeresource',
            name='compute_user',
            field=models.CharField(max_length=32),
            preserve_default=False,
        ),
    ]


# chris_backend/plugins/services/manager.py

"""
Plugin manager module that provides functionality to add, modify and delete plugins
through the CLI.
"""

import os
import sys

if __name__ == '__main__':
    # django needs to be loaded when this script is run standalone from the command line
    sys.path.append(os.path.join(os.path.dirname(__file__), '../../'))
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
    import django
    django.setup()

from django.conf import settings
from django.utils import timezone

from argparse import ArgumentParser
from argparse import ArgumentError
from chrisstoreclient.client import StoreClient

from plugins.models import ComputeResource
from plugins.models import PluginMeta, Plugin
from plugins.serializers import ComputeResourceSerializer
from plugins.serializers import (PluginMetaSerializer, PluginSerializer,
                                 PluginParameterSerializer, DEFAULT_PARAMETER_SERIALIZERS)


class PluginManager(object):

    def __init__(self):
        parser = ArgumentParser(description='Manage plugins')
        subparsers = parser.add_subparsers(dest='subparser_name', title='subcommands',
                                           description='valid subcommands',
                                           help='sub-command help')

        # create the parser for the "add" command
        parser_add = subparsers.add_parser('add', help='add a new compute resource')
        parser_add.add_argument('computeresource',
                                help="compute resource where plugins' instances run")
        parser_add.add_argument('url', help="compute resource's url")
        parser_add.add_argument('--auth_url', help="compute resource's auth url")
        parser_add.add_argument('--auth_token', help="compute resource's auth token")
        parser_add.add_argument('--user', default='pfcon', help="compute resource's user")
        parser_add.add_argument('--password', default='pfcon1234',
                                help="compute resource's password")
        parser_add.add_argument('--description', default='',
                                help="compute resource's description")
        parser_add.add_argument('--maxjobexecseconds', type=int,
                                help="compute resource's max job execution time in "
                                     "seconds")

        # create the parser for the "modify" command
        parser_modify = subparsers.add_parser('modify',
                                              help='modify existing compute resource')
        parser_modify.add_argument('computeresource', help="compute resource")
        parser_modify.add_argument('--name', help="compute resource's new name")
        parser_modify.add_argument('--url', help="compute resource's new url")
        parser_modify.add_argument('--auth_url', help="compute resource's auth url")
        parser_modify.add_argument('--auth_token', help="compute resource's auth token")
        parser_modify.add_argument('--user', help="compute resource's user")
        parser_modify.add_argument('--password', help="compute resource's password")
        parser_modify.add_argument('--description', default='',
                                   help="compute resource's new description")
        parser_modify.add_argument('--maxjobexecseconds', type=int,
                                   help="compute resource's new max job execution time "
                                        "in seconds")

        # create parser for the "register" command
        parser_register = subparsers.add_parser(
            'register', help='register a plugin with a compute resource')
        parser_register.add_argument(
            'computeresource', help="compute resource where the plugin's instances run")
        group = parser_register.add_mutually_exclusive_group()
        group.add_argument('--pluginname', help="plugin's name")
        group.add_argument('--pluginurl', help="plugin's url")
        parser_register.add_argument('--pluginversion', help="plugin's version. If not "
                                                        "provided then the latest "
                                                        "version is fetched.")
        parser_register.add_argument('--storetimeout', help="ChRIS store request timeout")

        # create the parser for the "remove" command
        parser_remove = subparsers.add_parser(
            'remove', help='remove an existing plugin or compute')
        parser_remove.add_argument('resourcename', choices=['plugin', 'compute'],
                                help="resource name")
        parser_remove.add_argument('id', help="resource id")

        self.parser = parser

    def add_compute_resource(self, name, url, auth_url, auth_token, user, passw, desc,
                             maxjobexecsec):
        """
        Add a new compute resource to the system.
        """
        # validate compute resource name
        try:
            cr = ComputeResource.objects.get(name=name)
        except ComputeResource.DoesNotExist:
            data = {'name': name, 'compute_url': url, 'compute_user': user,
                    'compute_password': passw, 'description': desc}
            if auth_url:
                data['compute_auth_url'] = auth_url
            if auth_token:
                data['compute_auth_token'] = auth_token
            if maxjobexecsec:
                data['max_job_exec_seconds'] = maxjobexecsec
            compute_resource_serializer = ComputeResourceSerializer(data=data)
            compute_resource_serializer.is_valid(raise_exception=True)
            cr = compute_resource_serializer.save()
        return cr

    def modify_compute_resource(self, name, new_name, url, auth_url, auth_token, user,
                                passw, desc, maxjobexecsec):
        """
        Modify an existing compute resource and add the current date as a new
        modification date.
        """
        try:
            cr = ComputeResource.objects.get(name=name)
        except ComputeResource.DoesNotExist:
            raise NameError("Compute resource '%s' does not exists" % name)
        data = {}
        if new_name:
            data['name'] = new_name
        if url:
            data['compute_url'] = url
        if auth_url:
            data['compute_auth_url'] = auth_url
        if auth_token:
            data['compute_auth_token'] = auth_token
        if user:
            data['compute_user'] = user
        if passw:
            data['compute_password'] = passw
        if desc:
            data['description'] = desc
        if maxjobexecsec:
            data['max_job_exec_seconds'] = maxjobexecsec
        if data:
            compute_resource_serializer = ComputeResourceSerializer(cr, data=data)
            compute_resource_serializer.is_valid(raise_exception=True)
            cr = compute_resource_serializer.save()
            cr.modification_date = timezone.now()
            cr.save()
        return cr

    def register_plugin(self, name, version, compute_name, timeout=30):
        """
        Register/add a new plugin identified by its name and version from the ChRIS store
        into the system.
        """
        try:
            cr = ComputeResource.objects.get(name=compute_name)
        except ComputeResource.DoesNotExist:
            raise NameError("Compute resource '%s' does not exists" % compute_name)
        plg_repr = None
        if not version:
            plg_repr = self.get_plugin_representation_from_store(name, None, timeout)
            version = plg_repr['version']
        try:
            plugin = self.get_plugin(name, version)
        except NameError:
            # plugin doesn't exist in the system, let's create it
            if not plg_repr:
                plg_repr = self.get_plugin_representation_from_store(name, version,
                                                                     timeout)
            return self._create_plugin(plg_repr, cr)
        # plugin already in the system, register it with cr if not already registered
        plugin.compute_resources.add(cr)
        return plugin

    def register_plugin_by_url(self, url, compute_name, timeout=30):
        """
        Register/add a new plugin identified by its ChRIS store url from the store into
        the system.
        """
        try:
            cr = ComputeResource.objects.get(name=compute_name)
        except ComputeResource.DoesNotExist:
            raise NameError("Compute resource '%s' does not exists" % compute_name)
        plg_repr = self.get_plugin_representation_from_store_by_url(url, timeout)
        name = plg_repr.get('name')
        version = plg_repr.get('version')
        try:
            plugin = self.get_plugin(name, version)
        except NameError:
            # plugin doesn't exist in the system, let's create it
            return self._create_plugin(plg_repr, cr)
        # plugin already in the system, register it with cr if not already registered
        plugin.compute_resources.add(cr)
        return plugin

    def _create_plugin(self, plg_repr, compute_resource):
        """
        Private utility method to register/add a new plugin into the system.
        """
        meta_data = {'name': plg_repr.pop('name'),
                     'title': plg_repr.pop('title', ''),
                     'stars': plg_repr.pop('stars', 0),
                     'public_repo': plg_repr.pop('public_repo', ''),
                     'license': plg_repr.pop('license', ''),
                     'type': plg_repr.pop('type'),
                     'icon': plg_repr.pop('icon', ''),
                     'category': plg_repr.pop('category', ''),
                     'authors': plg_repr.pop('authors', ''),
                     'documentation': plg_repr.pop('documentation', '')}
        parameters_data = plg_repr.pop('parameters')

        # check whether plugin_name does not exist and validate the plugin meta data
        try:
            meta = PluginMeta.objects.get(name=meta_data['name'])
            meta_serializer = PluginMetaSerializer(meta, data=meta_data)
        except PluginMeta.DoesNotExist:
            meta_serializer = PluginMetaSerializer(data=meta_data)
        meta_serializer.is_valid(raise_exception=True)

        # validate the plugin's versioned data
        plg_serializer = PluginSerializer(data=plg_repr)
        plg_serializer.is_valid(raise_exception=True)

        # check DB constraints
        plg_name = meta_serializer.validated_data.get('name')
        self.validate_name_version(plg_serializer.validated_data.get('version'), plg_name)
        self.validate_name_image(plg_serializer.validated_data.get('dock_image'),
                                 plg_name)

        # collect and validate parameters
        parameters_serializers = []
        for parameter in parameters_data:
            default = parameter.pop('default', None)
            parameter_serializer = PluginParameterSerializer(data=parameter)
            parameter_serializer.is_valid(raise_exception=True)
            serializer_dict = {'serializer': parameter_serializer,
                               'default_serializer': None}
            if default is not None:
                param_type = parameter['type']
                default_param_serializer = DEFAULT_PARAMETER_SERIALIZERS[param_type](
                    data={'value': default})
                default_param_serializer.is_valid(raise_exception=True)
                serializer_dict['default_serializer'] = default_param_serializer
            parameters_serializers.append(serializer_dict)

        # if no validation errors at this point then save to the DB

        pl_meta = meta_serializer.save()
        plugin = plg_serializer.save(meta=pl_meta, compute_resources=[compute_resource])
        for param_serializer_dict in parameters_serializers:
            param = param_serializer_dict['serializer'].save(plugin=plugin)
            if param_serializer_dict['default_serializer'] is not None:
                param_serializer_dict['default_serializer'].save(plugin_param=param)
        return plugin

    def remove_plugin(self, id):
        """
        Remove an existing/registered plugin from the system. All the associated plugin
        instances are cancelled before they are deleted by the DB CASCADE.
        """
        try:
            plugin = Plugin.objects.get(id=id)
        except Plugin.DoesNotExist:
            raise NameError("Couldn't find plugin with id '%s'" % id)
        for plg_inst in plugin.instances.all():
            plg_inst.cancel()
        if plugin.meta.plugins.count() == 1:
            plugin.meta.delete()  # the cascade deletes the plugin too
        else:
            plugin.delete()  # the cascade deletes the plugin instances too

    def remove_compute_resource(self, id):
        """
        Remove an existing compute resource from the system.
        """
        try:
            cr = ComputeResource.objects.get(id=id)
        except ComputeResource.DoesNotExist:
            raise NameError("Couldn't find compute resource with id '%s'" % id)
        cr.delete()

    def run(self, args=None):
        """
        Parse the arguments passed to the manager and perform the appropriate action.
        """
        options = self.parser.parse_args(args)
        if options.subparser_name == 'add':
            self.add_compute_resource(options.computeresource, options.url,
                                      options.auth_url, options.auth_token, options.user,
                                      options.password, options.description,
                                      options.maxjobexecseconds)
        elif options.subparser_name == 'modify':
            self.modify_compute_resource(options.computeresource, options.name,
                                         options.url, options.auth_url,
                                         options.auth_token, options.user,
                                         options.password, options.description,
                                         options.maxjobexecseconds)
        elif options.subparser_name == 'register':
            if options.pluginurl:
                self.register_plugin_by_url(options.pluginurl, options.computeresource,
                                            options.storetimeout)
            elif options.pluginname:
                self.register_plugin(options.pluginname, options.pluginversion,
                                     options.computeresource, options.storetimeout)
            else:
                raise ArgumentError('Either a name or a url must be provided')
        elif options.subparser_name == 'remove':
            if options.resourcename == 'plugin':
                self.remove_plugin(options.id)
            if options.resourcename == 'compute':
                self.remove_compute_resource(options.id)

    @staticmethod
    def validate_name_version(version, name):
        """
        Custom method to check if plugin name and version are unique together.
        """
        try:
            Plugin.objects.get(meta__name=name, version=version)
        except Plugin.DoesNotExist:
            pass
        else:
            msg = "Plugin with name %s and version %s already exists" % (name, version)
            raise ValueError(msg)

    @staticmethod
    def validate_name_image(dock_image, name):
        """
        Custom method to check if plugin name and docker image are unique together.
        """
        try:
            Plugin.objects.get(meta__name=name, dock_image=dock_image)
        except Plugin.DoesNotExist:
            pass
        else:
            msg = "Docker image %s already used in a previous version of plugin with " \
                  "name %s" % (dock_image, name)
            raise ValueError(msg)

    @staticmethod
    def get_plugin_representation_from_store(name, version=None, timeout=30):
        """
        Get a plugin app representation from the ChRIS store.
        """
        store_url = settings.CHRIS_STORE_URL
        store_client = StoreClient(store_url, None, None, timeout)
        plg = store_client.get_plugin(name, version)
        plg['parameters'] = PluginManager.get_plugin_parameters_from_store(plg['id'],
                                                                           timeout)
        return plg

    @staticmethod
    def get_plugin_representation_from_store_by_url(url, timeout=30):
        """
        Get a plugin app representation from the ChRIS store given the url of the plugin.
        """
        store_url = settings.CHRIS_STORE_URL
        store_client = StoreClient(store_url, None, None, timeout)
        res = store_client.get(url)
        result = store_client.get_data_from_collection(res)

        if not result['data']:
            raise NameError(f"Could not find plugin with url '{url}'")

        plg = result['data'][0]
        parameters_url = res.items[0].links.get(rel='parameters').href
        plg['parameters'] = PluginManager.get_plugin_parameters_from_url(parameters_url, timeout)
        return plg

    @staticmethod
    def get_plugin(name, version):
        """
        Get an existing plugin.
        """
        try:
            plg_meta = PluginMeta.objects.get(name=name)
        except PluginMeta.DoesNotExist:
            raise NameError("Couldn't find any plugin named '%s' in the system" % name)
        try:
            plugin = Plugin.objects.get(meta=plg_meta, version=version)
        except Plugin.DoesNotExist:
            raise NameError("Couldn't find plugin '%s' with version '%s' in the system" %
                            (name, version))
        return plugin

    @staticmethod
    def get_plugin_parameters_from_store(plg_id, timeout=30):
        """
        Get a plugin's parameters representation from the ChRIS store.
        """
        store_url = settings.CHRIS_STORE_URL
        store_client = StoreClient(store_url, None, None, timeout)
        parameters = []
        offset = 0
        limit = 50
        while True:
            result = store_client.get_plugin_parameters(plg_id, {'limit': limit,
                                                                 'offset': offset})
            parameters.extend(result['data'])
            offset += limit
            if not result['hasNextPage']: break
        return parameters

    @staticmethod
    def get_plugin_parameters_from_url(parameters_url, timeout=30):
        """
        Get a plugin's parameters representation from a ChRIS store by URL.
        """
        store_url = settings.CHRIS_STORE_URL
        store_client = StoreClient(store_url, None, None, timeout)
        parameters = []
        offset = 0
        limit = 50
        while True:
            res = store_client.get(parameters_url, {'limit': limit, 'offset': offset})
            result = store_client.get_data_from_collection(res)
            parameters.extend(result['data'])
            offset += limit
            if not result['hasNextPage']: break
        return parameters


# ENTRYPOINT
if __name__ == "__main__":
    manager = PluginManager()
    manager.run()


# chris_backend/plugins/tests/test_admin.py


import logging
import io
import json
from unittest import mock
from unittest.mock import ANY

from django.test import TestCase
from django import forms
from django.utils import timezone
from django.conf import settings
from django.urls import reverse
from django.contrib.auth.models import User
from rest_framework import status

from plugins import admin as pl_admin


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ComputeResourceAdminTests(TestCase):
    """
    Test ComputeResourceAdmin.
    """

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_add_view(self):
        """
        Test whether overriden add_view view only shows the proper fields in
        the add compute resource page and in editable mode.
        """
        compute_resource_admin = pl_admin.ComputeResourceAdmin(pl_admin.ComputeResource,
                                                               pl_admin.admin.site)
        request_mock = mock.Mock()
        with mock.patch.object(pl_admin.admin.ModelAdmin, 'add_view',
                               return_value=None) as add_view_mock:
            compute_resource_admin.add_view(request_mock)
            self.assertIn('name', compute_resource_admin.fields)
            self.assertIn('description', compute_resource_admin.fields)
            self.assertNotIn('creation_date', compute_resource_admin.fields)
            self.assertNotIn('modification_date', compute_resource_admin.fields)
            add_view_mock.assert_called_with(compute_resource_admin, request_mock,
                                             '', None)

    def test_change_view(self):
        """
        Test whether overriden change_view view shows all compute resource fields and
        the proper read-only and editable fields.
        """
        compute_resource_admin = pl_admin.ComputeResourceAdmin(pl_admin.ComputeResource,
                                                               pl_admin.admin.site)
        request_mock = mock.Mock()
        with mock.patch.object(pl_admin.admin.ModelAdmin, 'change_view',
                               return_value=None) as change_view_mock:
            compute_resource_admin.change_view(request_mock, 1)
            self.assertIn('name', compute_resource_admin.fields)
            self.assertIn('description', compute_resource_admin.fields)
            self.assertIn('creation_date', compute_resource_admin.fields)
            self.assertIn('modification_date', compute_resource_admin.fields)
            self.assertIn('creation_date', compute_resource_admin.readonly_fields)
            self.assertIn('modification_date', compute_resource_admin.readonly_fields)
            change_view_mock.assert_called_with(compute_resource_admin, request_mock, 1,
                                                '', None)

    def test_save_model(self):
        """
        Test whether overriden save_model method creates a new compute resource from the
        fields in the add compute resource page or properly change the modification date
        for a modified existing compute resource.
        """
        compute_resource_admin = pl_admin.ComputeResourceAdmin(pl_admin.ComputeResource,
                                                               pl_admin.admin.site)
        request_mock = mock.Mock()
        obj_mock = mock.Mock()
        obj_mock_creation_date = timezone.now()
        obj_mock.modification_date = obj_mock_creation_date
        form_mock = mock.Mock()
        form_mock.instance = mock.Mock()
        with mock.patch.object(pl_admin.admin.ModelAdmin, 'save_model',
                               return_value=None) as save_model_mock:
            compute_resource_admin.save_model(request_mock, obj_mock, form_mock, False)
            save_model_mock.assert_called_with(request_mock, obj_mock,
                                               form_mock, False)

            compute_resource_admin.save_model(request_mock, obj_mock, form_mock, True)
            save_model_mock.assert_called_with(request_mock, obj_mock, form_mock, True)
            self.assertGreater(obj_mock.modification_date, obj_mock_creation_date)

    def test_delete_model(self):
        """
        Test whether overriden delete_model method sets an error message when the user
        attempts to delete a compute resource that would result in orphan plugins.
        """
        (compute_resource, tf) = pl_admin.ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plg, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plg.compute_resources.set([compute_resource])
        plg.save()
        compute_resource_admin = pl_admin.ComputeResourceAdmin(pl_admin.ComputeResource,
                                                               pl_admin.admin.site)
        request_mock = mock.Mock()
        pl_admin.messages.set_level = mock.Mock(return_value=None)
        pl_admin.messages.error = mock.Mock(return_value=None)
        compute_resource_admin.delete_model(request_mock, compute_resource)
        pl_admin.messages.set_level.assert_called_with(request_mock, pl_admin.messages.ERROR)
        pl_admin.messages.error.assert_called_with(request_mock, ANY)

    def test_delete_queryset(self):
        """
        Test whether overriden delete_queryset method sets an error message when the user
        attempts to delete compute resources that would result in orphan plugins.
        """
        (compute_resource, tf) = pl_admin.ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plg, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plg.compute_resources.set([compute_resource])
        plg.save()
        compute_resource_admin = pl_admin.ComputeResourceAdmin(pl_admin.ComputeResource,
                                                               pl_admin.admin.site)
        request_mock = mock.Mock()
        pl_admin.messages.set_level = mock.Mock(return_value=None)
        pl_admin.messages.error = mock.Mock(return_value=None)
        compute_resource_admin.delete_queryset(request_mock, plg.compute_resources.all())
        pl_admin.messages.set_level.assert_called_with(request_mock, pl_admin.messages.ERROR)
        pl_admin.messages.error.assert_called_with(request_mock, ANY)


class PluginAdminFormTests(TestCase):
    """
    Test PluginAdminForm.
    """

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
        # re-enable logging
        logging.disable(logging.NOTSET)

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        (self.compute_resource, tf) = pl_admin.ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)
        # create a plugin
        self.plugin_name = "simplecopyapp"
        self.plugin_version = "0.1"
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        self.plugin = pl_admin.Plugin()
        self.plugin.meta = pl_meta
        self.plugin.version = self.plugin_version

    def test_clean_validate_name_version_and_save_plugin_descriptors(self):
        """
        Test whether overriden clean method validates the full set of plugin descriptors
        and save the newly created plugin to the DB.
        """
        # mock manager's register_plugin method
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        (plugin, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta,
                                                             version=self.plugin_version)
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin',
                               return_value=plugin) as register_plugin_mock:
            plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
            form = plugin_admin.form
            form.instance = self.plugin
            form.cleaned_data = {'name': self.plugin_name, 'version': self.plugin_version,
                                 'compute_resources': [self.compute_resource]}
            self.assertIsNone(form.instance.pk)
            form.clean(form)
            self.assertEqual(form.instance, plugin)
            self.assertIsNotNone(form.instance.pk)
            register_plugin_mock.assert_called_with(self.plugin_name,
                                                    self.plugin_version,
                                                    'host')

    def test_clean_validate_url_and_save_plugin_descriptors(self):
        """
        Test whether overriden clean method validates the full set of plugin descriptors
        and save the newly created plugin to the DB.
        """
        # mock manager's register_plugin_by_url method
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        (plugin, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta,
                                                             version=self.plugin_version)
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        plugin_store_url = "http://chris-store.local:8010/api/v1/1/"
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin_by_url',
                               return_value=plugin) as register_plugin_by_url_mock:
            plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
            form = plugin_admin.form
            form.instance = self.plugin
            form.cleaned_data = {'url': plugin_store_url,
                                 'compute_resources': plugin.compute_resources.all()}
            self.assertIsNone(form.instance.pk)
            form.clean(form)
            self.assertEqual(form.cleaned_data.get('name'), self.plugin_name)
            self.assertEqual(form.cleaned_data.get('version'), self.plugin_version)
            self.assertNotIn('url', form.cleaned_data)
            self.assertEqual(form.instance, plugin)
            self.assertIsNotNone(form.instance.pk)
            register_plugin_by_url_mock.assert_called_with(plugin_store_url, 'host')

    def test_clean_raises_validation_error_if_cannot_register_plugin(self):
        """
        Test whether overriden clean method raises a ValidationError when there
        is a validation error or it cannot save a new plugin to the DB.
        """
        # mock manager's register_plugin method
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin',
                               side_effect=Exception) as register_plugin_mock:
            plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
            form = plugin_admin.form
            form.instance = self.plugin
            form.cleaned_data = {'name': self.plugin_name, 'version': self.plugin_version,
                                 'compute_resources': [self.compute_resource]}
            self.assertIsNone(form.instance.pk)
            with self.assertRaises(forms.ValidationError):
                form.clean(form)

    def test_clean_raises_validation_error_if_compute_resources_is_NONE(self):
        """
        Test whether overriden clean method raises a ValidationError when compute
        resources is None (the user didn't picked up any in the UI) .
        """
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name='test_plg',
                                                                  type='fs')
        plugin = pl_admin.Plugin()
        plugin.meta = pl_meta
        plugin.version = self.plugin_version
        # mock manager's register_plugin method
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin',
                               return_value=plugin) as register_plugin_mock:
            plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
            form = plugin_admin.form
            form.instance = plugin
            form.cleaned_data = {'name': 'test_plg', 'version': self.plugin_version,
                                 'compute_resources': None}
            with self.assertRaises(forms.ValidationError):
                form.clean(form)


class PluginAdminTests(TestCase):
    """
    Test PluginAdmin.
    """

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_add_view(self):
        """
        Test whether overriden add_view view only shows the required fields in the add
        plugin page and in editable mode.
        """
        plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
        request_mock = mock.Mock()
        with mock.patch.object(pl_admin.admin.ModelAdmin, 'add_view',
                               return_value=None) as add_view_mock:
            plugin_admin.add_view(request_mock)
            self.assertEqual(len(plugin_admin.fieldsets), 3)
            self.assertEqual(len(plugin_admin.readonly_fields), 0)
            add_view_mock.assert_called_with(plugin_admin, request_mock, '', None)

    def test_change_view(self):
        """
        Test whether overriden change_view view shows all plugin fields in readonly
        mode except the 'compute_resources' field that is shown in editable mode.
        """
        plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
        request_mock = mock.Mock()
        with mock.patch.object(pl_admin.admin.ModelAdmin, 'change_view',
                               return_value=None) as change_view_mock:
            plugin_admin.change_view(request_mock, 1)
            self.assertNotIn('compute_resources', plugin_admin.readonly_fields)
            self.assertEqual(len(plugin_admin.fieldsets), 2)
            change_view_mock.assert_called_with(plugin_admin, request_mock, 1, '', None)

    def test_delete_model(self):
        """
        Test whether overriden delete_model method deletes the associated meta if this
        is the last associated plugin.
        """
        (compute_resource, tf) = pl_admin.ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plg, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plg.compute_resources.set([compute_resource])
        plg.save()
        plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
        request_mock = mock.Mock()
        plugin_admin.delete_model(request_mock, plg)
        with self.assertRaises(pl_admin.PluginMeta.DoesNotExist):
            pl_admin.PluginMeta.objects.get(name='pacspull')

    def test_delete_queryset(self):
        """
        Test whether overriden delete_queryset method deletes plugin metas if their last
        associated plugin is deleted.
        """
        (compute_resource, tf) = pl_admin.ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name='pacspull', type='fs')
        (plg, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plg.compute_resources.set([compute_resource])
        plg.save()
        plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
        request_mock = mock.Mock()
        plugin_admin.delete_queryset(request_mock, pl_meta.plugins.all())
        with self.assertRaises(pl_admin.PluginMeta.DoesNotExist):
            pl_admin.PluginMeta.objects.get(name='pacspull')

    def test_add_plugins_from_file_view(self):
        """
        Test whether custom add_plugins_from_file_view view passes a summary dict in
        the context for valid form POST or otherwise an empty form.
        """
        plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
        summary = {'success': [{'plugin_name': 'my_plugin'}], 'error': []}
        plugin_admin.register_plugins_from_file = mock.Mock(return_value=summary)
        request_mock = mock.Mock()
        request_mock.META = {'SCRIPT_NAME': ''}

        with mock.patch.object(pl_admin, 'render',
                               return_value=mock.Mock()) as render_mock:
            request_mock.method = 'GET'
            plugin_admin.add_plugins_from_file_view(request_mock)
            render_mock.assert_called_with(request_mock,
                                           'admin/plugins/plugin/add_plugins_from_file.html',
                                           {'site_title': 'ChRIS Admin',
                                            'site_header': 'ChRIS Administration',
                                            'site_url': ANY,
                                            'has_permission': ANY,
                                            'available_apps': ANY,
                                            'is_popup': ANY,
                                            'opts': ANY,
                                            'file_form': ANY})

            request_mock.method = 'POST'
            request_mock.FILES = {'file': 'f'}
            with mock.patch.object(pl_admin.UploadFileForm, 'is_valid',
                                   return_value=True) as is_valid_mock:
                plugin_admin.add_plugins_from_file_view(request_mock)
                plugin_admin.register_plugins_from_file.assert_called_with(
                    request_mock.FILES['file'])
                render_mock.assert_called_with(request_mock,
                                'admin/plugins/plugin/add_plugins_from_file_result.html',
                                               {'site_title': 'ChRIS Admin',
                                                'site_header': 'ChRIS Administration',
                                                'site_url': ANY,
                                                'has_permission': ANY,
                                                'available_apps': ANY,
                                                'is_popup': ANY,
                                                'opts': ANY,
                                                'summary': summary})

    def test_register_plugins_from_file(self):
        """
        Test whether custom register_plugins_from_file method registers plugins from the
        ChRIS store that have been specified in a text file.
        """
        plugin_admin = pl_admin.PluginAdmin(pl_admin.Plugin, pl_admin.admin.site)
        file_content = b'simplefsapp host\n simpledsapp 1.0.6 host\n http://chris-store.local:8010/api/v1/1/ host\n'

        with mock.patch.object(pl_admin.PluginManager, 'register_plugin',
                               return_value=None) as register_plugin_mock:

            with mock.patch.object(pl_admin.PluginManager, 'register_plugin_by_url',
                                   return_value=None) as register_plugin_by_url_mock:
                summary = {'success': [{'plugin_name': 'simplefsapp'},
                                       {'plugin_name': 'simpledsapp'},
                                       {'plugin_name': 'http://chris-store.local:8010/api/v1/1/'}
                                       ],
                           'error': []}
                with io.BytesIO(file_content) as f:
                    self.assertEqual(plugin_admin.register_plugins_from_file(f), summary)
                    register_plugin_mock.assert_called_with('simpledsapp', '1.0.6', 'host')
                    register_plugin_by_url_mock.assert_called_with('http://chris-store.local:8010/api/v1/1/',
                                                              'host')

            with mock.patch.object(pl_admin.PluginManager, 'register_plugin_by_url',
                                   side_effect=Exception('Error')):
                summary = {'success': [{'plugin_name': 'simplefsapp'},
                                       {'plugin_name': 'simpledsapp'}
                                       ],
                           'error': [{'plugin_name': 'http://chris-store.local:8010/api/v1/1/',
                                      'code': 'Error'}]}
                with io.BytesIO(file_content) as f:
                    self.assertEqual(plugin_admin.register_plugins_from_file(f), summary)


class ComputeResourceAdminListViewTests(PluginAdminFormTests):
    """
    Test the admin-computeresource-list.
    """

    def setUp(self):
        super(ComputeResourceAdminListViewTests, self).setUp()

        self.content_type = 'application/vnd.collection+json'
        self.create_read_url = reverse("admin-computeresource-list")
        self.post = json.dumps(
            {"template": {"data": [{"name": "name",
                                    "value": "moc"},
                                   {"name": "compute_url",
                                    "value": "http://pfcon.local:30005/api/v1/1/"},
                                   {"name": "compute_user",
                                    "value": "mocuser"},
                                   {"name": "compute_password",
                                    "value": "mocuser1234"},
                                   {"name": "description",
                                    "value": "moc compute env"}
                                   ]}})
        self.admin_username = 'admin'
        self.admin_password = 'adminpass'
        self.username = 'foo'
        self.password = 'pass'
        # create admin user
        User.objects.create_superuser(username=self.admin_username,
                                      password=self.admin_password,
                                      email='admin@babymri.org')
        # create normal user
        User.objects.create_user(username=self.username,
                                 password=self.password)

    def test_compute_resource_create_success(self):
        self.client.login(username=self.admin_username, password=self.admin_password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_compute_resource_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_compute_resource_create_failure_access_denied(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_compute_resource_list_success(self):
        self.client.login(username=self.admin_username, password=self.admin_password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, self.compute_resource.name)

    def test_compute_resource_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_compute_resource_list_failure_access_denied(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class PluginAdminSerializerTests(PluginAdminFormTests):

    def test_validate_registers_plugin(self):
        """
        Test whether overriden validate method registers plugin by url.
        """
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        (plugin, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta,
                                                             version=self.plugin_version)
        # mock manager's register_plugin_by_url method
        plugin_store_url = "http://chris-store.local:8010/api/v1/1/"
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin_by_url',
                               return_value=plugin) as register_plugin_by_url_mock:
            plg_admin_serializer = pl_admin.PluginAdminSerializer(plugin)
            data = {'plugin_store_url': plugin_store_url,
                    'compute_name': self.compute_resource.name}
            data = plg_admin_serializer.validate(data)
            self.assertNotIn('plugin_store_url', data)
            self.assertNotIn('compute_name', data)
            register_plugin_by_url_mock.assert_called_with(plugin_store_url,
                                                           self.compute_resource.name)

    def test_validate_raises_validation_error_if_cannot_register_plugin(self):
        """
        Test whether overriden validate method raises a ValidationError when there
        is a validation error or it cannot save a new plugin to the DB.
        """
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        (plugin, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta,
                                                             version=self.plugin_version)
        # mock manager's register_plugin method
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin_by_url',
                               side_effect=Exception) as register_plugin_by_url_mock:
            plg_admin_serializer = pl_admin.PluginAdminSerializer(plugin)
            plugin_store_url = "http://chris-store.local:8010/api/v1/1/"
            data = {'plugin_store_url': plugin_store_url,
                    'compute_name': self.compute_resource.name}
            with self.assertRaises(pl_admin.serializers.ValidationError):
                plg_admin_serializer.validate(data)


class PluginAdminListViewTests(PluginAdminFormTests):
    """
    Test the admin-plugin-list view.
    """

    def setUp(self):
        super(PluginAdminListViewTests, self).setUp()

        self.content_type = 'application/vnd.collection+json'
        self.create_read_url = reverse("admin-plugin-list")
        self.post = json.dumps(
            {"template": {"data": [{"name": "plugin_store_url",
                                    "value": "http://chris-store.local:8010/api/v1/1/"},
                                   {"name": "compute_name",
                                    "value": self.compute_resource.name}
                                   ]}})
        self.admin_username = 'admin'
        self.admin_password = 'adminpass'
        self.username = 'foo'
        self.password = 'pass'
        # create admin user
        User.objects.create_superuser(username=self.admin_username,
                                      password=self.admin_password,
                                      email='admin@babymri.org')
        # create normal user
        User.objects.create_user(username=self.username,
                                 password=self.password)

    def test_plugin_create_success(self):
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        (plugin, tf) = pl_admin.Plugin.objects.get_or_create(meta=pl_meta,
                                                             version=self.plugin_version)
        self.client.login(username=self.admin_username, password=self.admin_password)
        with mock.patch.object(pl_admin.PluginManager, 'register_plugin_by_url',
                               return_value=plugin) as register_plugin_by_url_mock:
            response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_plugin_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_create_failure_access_denied(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_plugin_list_success(self):
        (pl_meta, tf) = pl_admin.PluginMeta.objects.get_or_create(name=self.plugin_name,
                                                                  type='fs')
        pl_admin.Plugin.objects.get_or_create(meta=pl_meta, version=self.plugin_version)
        self.client.login(username=self.admin_username, password=self.admin_password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, self.plugin_name)

    def test_plugin_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_plugin_list_failure_access_denied(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


# chris_backend/plugins/tests/test_manager.py


import logging
from unittest import mock

from django.test import TestCase
from django.conf import settings

from plugins.models import PluginMeta, Plugin
from plugins.models import PluginParameter, DefaultStrParameter
from plugins.models import ComputeResource
from plugins.services import manager


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class PluginManagerTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)
        self.plugin_fs_name = "simplecopyapp"

        plugin_parameters = [{'name': 'dir', 'type': 'string', 'action': 'store',
                              'optional': True, 'flag': '--dir', 'short_flag': '-d',
                              'default': '/', 'help': 'test plugin', 'ui_exposed': True}]

        self.plg_data = {'description': 'A simple chris fs app demo',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-simplecopyapp',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'simplefsapp'}

        self.plg_meta_data = {'name':  self.plugin_fs_name,
                              'title': 'Dir plugin',
                              'license': 'MIT',
                              'type': 'fs',
                              'icon': 'http://github.com/plugin',
                              'category': 'Dir',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters


        self.pl_manager = manager.PluginManager()

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create a plugin
        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        # add plugin's parameters
        parameters = plugin_parameters
        (plg_param, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'],
            short_flag=parameters[0]['short_flag'],
            optional=parameters[0]['optional']
        )
        default = parameters[0]['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param, value=default)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_mananger_can_get_plugin(self):
        """
        Test whether the manager can return a plugin object.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name, version="0.1")
        self.assertEqual(plugin, self.pl_manager.get_plugin(self.plugin_fs_name, "0.1"))

    def test_mananger_can_register_plugin(self):
        """
        Test whether the manager can add a new plugin to the system given its name
        and version.
        """
        self.plugin_repr['name'] = 'testapp'
        # mock manager's get_plugin_representation_from_store static method
        self.pl_manager.get_plugin_representation_from_store = mock.Mock(
            return_value=self.plugin_repr)

        plugin = self.pl_manager.register_plugin('testapp', '0.1', 'host')
        self.assertEqual(Plugin.objects.count(), 2)
        self.assertEqual(plugin.meta.name, 'testapp')
        self.assertTrue(PluginParameter.objects.count() > 1)
        self.pl_manager.get_plugin_representation_from_store.assert_called_with(
            'testapp', '0.1', 30)

        self.pl_manager.register_plugin('testapp', '', 'host')
        self.pl_manager.get_plugin_representation_from_store.assert_called_with(
            'testapp', None, 30)

    def test_mananger_register_plugin_raises_name_error_if_compute_resource_does_not_exist(self):
        """
        Test whether the manager's register_plugin method raises NameError exception
        when the compute respource argument doesn't exist in the DB.
        """
        with self.assertRaises(NameError):
            self.pl_manager.register_plugin('testapp', '0.1', 'dummy')

    def test_mananger_can_register_plugin_by_url(self):
        """
        Test whether the manager can add a new plugin to the system given its url.
        """
        self.plugin_repr['name'] = 'testapp'
        # mock manager's get_plugin_representation_from_store static method
        self.pl_manager.get_plugin_representation_from_store_by_url = mock.Mock(
            return_value=self.plugin_repr)
        plugin = self.pl_manager.register_plugin_by_url(
            'http://127.0.0.1:8010/api/v1/1/', 'host')
        self.assertEqual(Plugin.objects.count(), 2)
        self.assertEqual(plugin.meta.name, 'testapp')
        self.assertTrue(PluginParameter.objects.count() > 1)
        self.pl_manager.get_plugin_representation_from_store_by_url.assert_called_with(
            'http://127.0.0.1:8010/api/v1/1/', 30)

    def test_mananger_register_plugin_by_url_raises_name_error_if_compute_resource_does_not_exist(self):
        """
        Test whether the manager's register_plugin_by_url method raises NameError
        exception when the compute respource argument doesn't exist in the DB.
        """
        with self.assertRaises(NameError):
            self.pl_manager.register_plugin('testapp', '0.1', 'dummy')

    def test_mananger_can_remove_plugin(self):
        """
        Test whether the manager can remove an existing plugin from the system.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name, version="0.1")
        self.pl_manager.remove_plugin(plugin.id)
        self.assertEqual(Plugin.objects.count(), 0)
        self.assertEqual(PluginMeta.objects.count(), 0)
        self.assertEqual(PluginParameter.objects.count(), 0)


# chris_backend/plugins/tests/test_models.py


import logging

from django.test import TestCase
from django.conf import settings

from plugins.models import PluginMeta, PluginMetaFilter, Plugin
from plugins.models import PluginParameter, DefaultStrParameter
from plugins.models import ComputeResource


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ModelTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_fs_name = "simplecopyapp"
        self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True,
                                             'default': "./"}}

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create a plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_fs_name, type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        # add plugins' parameters
        (plg_param, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type=self.plugin_fs_parameters['dir']['type'],
            optional=self.plugin_fs_parameters['dir']['optional']
        )
        default = self.plugin_fs_parameters['dir']['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param, value=default)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class ComputeResourceModelTests(ModelTests):
    """
    Test the ComputeResource model.
    """

    def test_delete(self):
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        # test the failure case bc plugin would be left without a compute resource
        with self.assertRaises(Exception):
            self.compute_resource.delete()
        # test the success case
        (compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="test", compute_url=COMPUTE_RESOURCE_URL)
        plugin_compute_resources = list(plugin.compute_resources.all())
        plugin_compute_resources.append(compute_resource)
        plugin.compute_resources.set(plugin_compute_resources)
        plugin.save()
        n_cr = len(plugin_compute_resources)
        compute_resource.delete()
        self.assertEqual(plugin.compute_resources.all().count(), n_cr - 1)


class PluginMetaFilterTests(ModelTests):

    def setUp(self):
        super(PluginMetaFilterTests, self).setUp()
        self.other_plugin_name = "simplefsapp1"

        # create other plugin
        (meta, tf) = PluginMeta.objects.get_or_create(name=self.other_plugin_name)
        Plugin.objects.get_or_create(meta=meta)

    def test_search_name_authors_category(self):
        """
        Test whether custom method search_name_authors_category returns a filtered
        queryset with all plugins for which name or title or category matches the
        search value.
        """
        meta1 = PluginMeta.objects.get(name=self.plugin_fs_name)
        meta1.category = 'dir'
        meta1.authors = 'author1'
        meta1.save()
        meta2 = PluginMeta.objects.get(name=self.other_plugin_name)
        meta2.category = 'dir'
        meta2.authors = 'author2'
        meta2.save()
        meta_filter = PluginMetaFilter()
        queryset = PluginMeta.objects.all()
        qs = meta_filter.search_name_authors_category(queryset, 'name_authors_category', 'Dir')
        self.assertCountEqual(qs, queryset)


class PluginModelTests(ModelTests):
    """
    Test the Plugin model.
    """

    def test_get_plugin_parameter_names(self):
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        param_names = plugin.get_plugin_parameter_names()
        self.assertEqual(param_names, ['dir'])


class PluginParameterModelTests(ModelTests):
    """
    Test the PluginParameter model.
    """

    def test_get_default(self):
        plugin = Plugin.objects.get(meta__name=self.plugin_fs_name)
        parameter = plugin.parameters.get(name='dir')
        default = parameter.get_default()
        self.assertEqual(default.value, "./")


# chris_backend/plugins/tests/test_serializers.py


import logging
from unittest import mock

from django.test import TestCase
from django.conf import settings
from rest_framework import serializers

from plugins.models import (ComputeResource, PluginMeta, Plugin, PluginParameter,
                            DefaultStrParameter)
from plugins.serializers import (PluginMetaSerializer, PluginSerializer,
                                 PluginParameterSerializer)


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class SerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.plugin_name = "simplecopyapp"
        plugin_parameters = [{'name': 'dir', 'type': 'string', 'action': 'store',
                              'optional': True, 'flag': '--dir', 'short_flag': '-d',
                              'default': '/', 'help': 'test plugin', 'ui_exposed': True}]

        self.plg_data = {'description': 'A simple chris fs app demo',
                         'version': '0.1',
                         'dock_image': 'fnndsc/pl-simplefsapp',
                         'execshell': 'python3',
                         'selfpath': '/usr/local/bin',
                         'selfexec': 'simplefsapp'}

        self.plg_meta_data = {'name': self.plugin_name,
                              'title': 'Dir plugin',
                              'license': 'MIT',
                              'type': 'fs',
                              'icon': 'http://github.com/plugin',
                              'category': 'Dir',
                              'stars': 0,
                              'authors': 'FNNDSC (dev@babyMRI.org)'}

        self.plugin_repr = self.plg_data.copy()
        self.plugin_repr.update(self.plg_meta_data)
        self.plugin_repr['parameters'] = plugin_parameters

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create a plugin
        data = self.plg_meta_data.copy()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(**data)
        data = self.plg_data.copy()
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, **data)
        plugin.compute_resources.set([self.compute_resource])
        plugin.save()

        # add plugin's parameters
        parameters = plugin_parameters
        (plg_param, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin,
            name=parameters[0]['name'],
            type=parameters[0]['type'],
            flag=parameters[0]['flag'],
            optional=parameters[0]['optional']
        )
        default = parameters[0]['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param, value=default)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class PluginMetaSerializerTests(SerializerTests):

    def test_update(self):
        """
        Test whether overriden update method changes modification date.
        """
        meta = PluginMeta.objects.get(name=self.plugin_name)
        initial_mod_date = meta.modification_date
        data = {'name': self.plugin_name, 'public_repo': 'http://github.com/plugin'}
        plg_meta_serializer = PluginMetaSerializer(meta, data)
        plg_meta_serializer.is_valid(raise_exception=True)
        meta = plg_meta_serializer.update(meta, plg_meta_serializer.validated_data)
        self.assertGreater(meta.modification_date, initial_mod_date)


class PluginSerializerTests(SerializerTests):

    def test_validate_version(self):
        """
        Test whether overriden validate_version method raises a ValidationError when
        wrong version type or format has been submitted.
        """
        plg_serializer = PluginSerializer()
        with self.assertRaises(serializers.ValidationError):
            plg_serializer.validate_version(1.2)
        with self.assertRaises(serializers.ValidationError):
            plg_serializer.validate_version('v1.2')

    def test_validate_app_workers_descriptor(self):
        """
        Test whether custom validate_app_workers_descriptor method raises a ValidationError
        when the app worker descriptor cannot be converted to a positive integer.
        """
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'min_number_of_workers', 'value': 'one'}
            PluginSerializer.validate_app_workers_descriptor(descriptor_dict)
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'max_number_of_workers', 'value': 0}
            PluginSerializer.validate_app_workers_descriptor(descriptor_dict)

    def test_validate_app_cpu_descriptor(self):
        """
        Test whether custom validate_app_cpu_descriptor method raises a ValidationError
        when the app cpu descriptor cannot be converted to a fields.CPUInt.
        """
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'min_cpu_limit', 'value': '100me'}
            PluginSerializer.validate_app_cpu_descriptor(descriptor_dict)
            descriptor_dict = {'name': 'max_cpu_limit', 'value': '100m'}
            self.assertEqual(100, PluginSerializer.validate_app_cpu_descriptor(descriptor_dict))

    def test_validate_app_memory_descriptor(self):
        """
        Test whether custom validate_app_memory_descriptor method raises a ValidationError
        when the app memory descriptor cannot be converted to a fields.MemoryInt.
        """
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'min_memory_limit', 'value': '100me'}
            PluginSerializer.validate_app_cpu_descriptor(descriptor_dict)
            descriptor_dict = {'name': 'min_memory_limit', 'value': '100mi'}
            self.assertEqual(100, PluginSerializer.validate_app_cpu_descriptor(descriptor_dict))
            descriptor_dict = {'name': 'max_memory_limit', 'value': '100gi'}
            self.assertEqual(100, PluginSerializer.validate_app_cpu_descriptor(descriptor_dict))

    def test_validate_app_gpu_descriptor(self):
        """
        Test whether custom validate_app_gpu_descriptor method raises a ValidationError
        when the gpu descriptor cannot be converted to a non-negative integer.
        """
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'min_gpu_limit', 'value': 'one'}
            PluginSerializer.validate_app_gpu_descriptor(descriptor_dict)
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'max_gpu_limit', 'value': -1}
            PluginSerializer.validate_app_gpu_descriptor(descriptor_dict)

    def test_validate_app_int_descriptor(self):
        """
        Test whether custom validate_app_int_descriptor method raises a ValidationError
        when the descriptor cannot be converted to a non-negative integer.
        """
        error_msg = "This field must be a non-negative integer."
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'field_name', 'value': 'one'}
            PluginSerializer.validate_app_int_descriptor(descriptor_dict, error_msg)
        with self.assertRaises(serializers.ValidationError):
            descriptor_dict = {'name': 'field_name', 'value': -1}
            PluginSerializer.validate_app_int_descriptor(descriptor_dict, error_msg)

    def test_validate_app_descriptor_limits(self):
        """
        Test whether custom validate_app_descriptor_limit method raises a ValidationError
        when the max limit is smaller than the min limit.
        """
        self.plugin_repr['min_cpu_limit'] = 200
        self.plugin_repr['max_cpu_limit'] = 100
        with self.assertRaises(serializers.ValidationError):
            PluginSerializer.validate_app_descriptor_limits(self.plugin_repr,
                                                            'min_cpu_limit',
                                                            'max_cpu_limit')

    def test_validate_workers_limits(self):
        """
        Test whether custom validate method raises a ValidationError when the
        'max_number_of_workers' is smaller than the 'min_number_of_workers'.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        self.plugin_repr['min_number_of_workers'] = 2
        self.plugin_repr['max_number_of_workers'] = 1
        data = self.plugin_repr.copy()
        del data['parameters']
        with self.assertRaises(serializers.ValidationError):
            plg_serializer.validate(data)

    def test_validate_cpu_limits(self):
        """
        Test whether custom validate method raises a ValidationError when the
        'max_cpu_limit' is smaller than the 'min_cpu_limit'.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        self.plugin_repr['min_cpu_limit'] = 200
        self.plugin_repr['max_cpu_limit'] = 100
        data = self.plugin_repr.copy()
        del data['parameters']
        with self.assertRaises(serializers.ValidationError):
            plg_serializer.validate(data)

    def test_validate_memory_limits(self):
        """
        Test whether custom validate method raises a ValidationError when the
        'max_memory_limit' is smaller than the 'min_memory_limit'.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        self.plugin_repr['min_memory_limit'] = 100000
        self.plugin_repr['max_memory_limit'] = 10000
        data = self.plugin_repr.copy()
        del data['parameters']
        with self.assertRaises(serializers.ValidationError):
            plg_serializer.validate(data)

    def test_validate_gpu_limits(self):
        """
        Test whether custom validate method raises a ValidationError when the
        'max_gpu_limit' is smaller than the 'max_gpu_limit'.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        self.plugin_repr['min_gpu_limit'] = 2
        self.plugin_repr['max_gpu_limit'] = 1
        data = self.plugin_repr.copy()
        del data['parameters']
        with self.assertRaises(serializers.ValidationError):
            plg_serializer.validate(data)

    def test_validate_validates_min_number_of_workers(self):
        """
        Test whether custom validate method validates the 'min_number_of_workers'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['min_number_of_workers'] = 4
        plg_serializer.validate_app_workers_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_workers_descriptor.assert_called_with(
            {'name': 'min_number_of_workers', 'value': 4})

    def test_validate_validates_max_number_of_workers(self):
        """
        Test whether custom validate method validates the 'max_number_of_workers'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['max_number_of_workers'] = 5
        plg_serializer.validate_app_workers_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_workers_descriptor.assert_called_with(
            {'name': 'max_number_of_workers', 'value': 5})

    def test_validate_validates_min_gpu_limit(self):
        """
        Test whether custom validate method validates the 'min_gpu_limit'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['min_gpu_limit'] = 1
        plg_serializer.validate_app_gpu_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_gpu_descriptor.assert_called_with(
            {'name': 'min_gpu_limit', 'value': 1})

    def test_validate_validates_max_gpu_limit(self):
        """
        Test whether custom validate method validates the 'max_gpu_limit'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['max_gpu_limit'] = 2
        plg_serializer.validate_app_gpu_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_gpu_descriptor.assert_called_with(
            {'name': 'max_gpu_limit', 'value': 2})

    def test_validate_validates_min_cpu_limit(self):
        """
        Test whether custom validate method validates the 'min_cpu_limit'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['min_cpu_limit'] = 100
        plg_serializer.validate_app_cpu_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_cpu_descriptor.assert_called_with(
            {'name': 'min_cpu_limit', 'value': 100})

    def test_validate_validates_max_cpu_limit(self):
        """
        Test whether custom validate method validates the 'max_cpu_limit'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['max_cpu_limit'] = 200
        plg_serializer.validate_app_cpu_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_cpu_descriptor.assert_called_with(
            {'name': 'max_cpu_limit', 'value': 200})

    def test_validate_validates_min_memory_limit(self):
        """
        Test whether custom validate method validates the 'min_memory_limit'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['min_memory_limit'] = 10000
        plg_serializer.validate_app_memory_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_memory_descriptor.assert_called_with(
            {'name': 'min_memory_limit', 'value': 10000})

    def test_validate_validates_max_memory_limit(self):
        """
        Test whether custom validate method validates the 'max_memory_limit'
        descriptor.
        """
        plugin = Plugin.objects.get(meta__name=self.plugin_name)
        plg_serializer = PluginSerializer(plugin)
        data = self.plugin_repr.copy()
        del data['parameters']
        data['max_memory_limit'] = 100000
        plg_serializer.validate_app_memory_descriptor = mock.Mock()
        plg_serializer.validate(data)
        plg_serializer.validate_app_memory_descriptor.assert_called_with(
            {'name': 'max_memory_limit', 'value': 100000})


class PluginParameterSerializerTests(SerializerTests):

    def test_validate_validates_parameters_of_path_type_and_optional(self):
        """
        Test whether overriden validate method raises a ValidationError when
        a plugin parameter is optional anf of type 'path' or 'unextpath'.
        """
        with self.assertRaises(serializers.ValidationError):
            plg_param_serializer = PluginParameterSerializer()
            plg_param_serializer.validate({'optional': True, 'type': 'path'})
        with self.assertRaises(serializers.ValidationError):

            plg_param_serializer = PluginParameterSerializer()
            plg_param_serializer.validate({'optional': True, 'type': 'unextpath'})


# chris_backend/plugins/tests/test_views.py


import logging

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import status

from plugins.models import PluginMeta, Plugin
from plugins.models import PluginParameter, DefaultStrParameter
from plugins.models import ComputeResource


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ViewTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.username = 'foo'
        self.password = 'bar'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create API user
        User.objects.create_user(username=self.username,
                                 password=self.password)

        # create an fs plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='simplecopyapp', type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()
        # add plugin's parameters
        (plg_param, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type='string',
            optional=True
        )
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param, value="./")

        # create a ds plugin
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='mri_convert', type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class ComputeResourceListViewTests(ViewTests):
    """
    Test the computeresource-list view.
    """

    def setUp(self):
        super(ComputeResourceListViewTests, self).setUp()
        self.list_url = reverse("computeresource-list")

    def test_compute_resource_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, COMPUTE_RESOURCE_URL)

    def test_compute_resource_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class ComputeResourceListQuerySearchViewTests(ViewTests):
    """
    Test the computeresource-list-query-search view.
    """

    def setUp(self):
        super(ComputeResourceListQuerySearchViewTests, self).setUp()
        self.list_url = reverse("computeresource-list-query-search") + '?name=host'

    def test_compute_resource_list_query_search_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, COMPUTE_RESOURCE_URL)

    def test_compute_resource_list_query_search_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class ComputeResourceDetailViewTests(ViewTests):
    """
    Test the computeresource-detail view.
    """

    def setUp(self):
        super(ComputeResourceDetailViewTests, self).setUp()
        compute_resource = ComputeResource.objects.get(name="host")

        self.read_url = reverse("computeresource-detail", kwargs={"pk": compute_resource.id})

    def test_compute_resource_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, COMPUTE_RESOURCE_URL)

    def test_compute_resource_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginMetaListViewTests(ViewTests):
    """
    Test the pluginmeta-list view.
    """

    def setUp(self):
        super(PluginMetaListViewTests, self).setUp()
        self.list_url = reverse("pluginmeta-list")

    def test_plugin_meta_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, 'simplecopyapp')
        self.assertContains(response, 'mri_convert')

    def test_plugin_meta_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginMetaListQuerySearchViewTests(ViewTests):
    """
    Test the pluginmeta-list-query-search view.
    """

    def setUp(self):
        super(PluginMetaListQuerySearchViewTests, self).setUp()
        self.list_url = reverse("pluginmeta-list-query-search") + '?name=simplecopyapp'

    def test_plugin_meta_list_query_search_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, 'simplecopyapp')

    def test_plugin_meta_list_query_search_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginMetaPluginListViewTests(ViewTests):
    """
    Test the pluginmeta-plugin-list view.
    """

    def setUp(self):
        super(PluginMetaPluginListViewTests, self).setUp()

        pl_meta = PluginMeta.objects.get(name="simplecopyapp")
        self.list_url = reverse("pluginmeta-plugin-list", kwargs={"pk": pl_meta.id})

    def test_plugin_meta_plugin_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "simplecopyapp")
        self.assertNotContains(response, "mri_convert")  # plugin list is plugin meta-specific

    def test_plugin_compute_resource_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginMetaDetailViewTests(ViewTests):
    """
    Test the pluginmeta-detail view.
    """

    def setUp(self):
        super(PluginMetaDetailViewTests, self).setUp()

        meta = PluginMeta.objects.get(name='simplecopyapp')
        self.read_url = reverse("pluginmeta-detail", kwargs={"pk": meta.id})

    def test_plugin_meta_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, 'simplecopyapp')

    def test_plugin_meta_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginListViewTests(ViewTests):
    """
    Test the plugin-list view.
    """

    def setUp(self):
        super(PluginListViewTests, self).setUp()
        self.list_url = reverse("plugin-list")

    def test_plugin_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "simplecopyapp")
        self.assertContains(response, "mri_convert")

    def test_plugin_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginListQuerySearchViewTests(ViewTests):
    """
    Test the plugin-list-query-search view.
    """

    def setUp(self):
        super(PluginListQuerySearchViewTests, self).setUp()
        self.list_url = reverse("plugin-list-query-search") + '?name=simplecopyapp'

    def test_plugin_list_query_search_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "simplecopyapp")
        self.assertNotContains(response, "mri_convert")

    def test_plugin_list_query_search_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginComputeResourceListViewTests(ViewTests):
    """
    Test the plugin-computeresource-list view.
    """

    def setUp(self):
        super(PluginComputeResourceListViewTests, self).setUp()

        plugin = Plugin.objects.get(meta__name="simplecopyapp")
        self.list_url = reverse("plugin-computeresource-list", kwargs={"pk": plugin.id})

    def test_plugin_compute_resource_list_success(self):
        ComputeResource.objects.get_or_create(name="new", compute_url=COMPUTE_RESOURCE_URL)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, COMPUTE_RESOURCE_URL)
        self.assertNotContains(response, "new")  # compute resource list is plugin-specific

    def test_plugin_compute_resource_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginDetailViewTests(ViewTests):
    """
    Test the plugin-detail view.
    """

    def setUp(self):
        super(PluginDetailViewTests, self).setUp()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='simplecopyapp', type='fs')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')

        self.read_url = reverse("plugin-detail", kwargs={"pk": plugin.id})

    def test_plugin_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, "simplecopyapp")

    def test_plugin_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginParameterListViewTests(ViewTests):
    """
    Test the pluginparameter-list view.
    """

    def setUp(self):
        super(PluginParameterListViewTests, self).setUp()
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name='simplecopyapp', type='fs')
        (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')

        # self.corresponding_plugin_url = reverse("plugin-detail", kwargs={"pk": plugin.id})
        self.list_url = reverse("pluginparameter-list", kwargs={"pk": plugin.id})

        # create two plugin parameters
        PluginParameter.objects.get_or_create(plugin=plugin,
                                              name='img_type', type='string')
        PluginParameter.objects.get_or_create(plugin=plugin,
                                              name='prefix', type='string')

    def test_plugin_parameter_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.list_url)
        self.assertContains(response, "img_type")
        self.assertContains(response, "prefix")

    def test_plugin_parameter_list_failure_unauthenticated(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class PluginParameterDetailViewTests(ViewTests):
    """
    Test the pluginparameter-detail view.
    """

    def setUp(self):
        super(PluginParameterDetailViewTests, self).setUp()
        # get the plugin parameter
        param = PluginParameter.objects.get(name='dir')
        self.read_url = reverse("pluginparameter-detail", kwargs={"pk": param.id})

    def test_plugin_parameter_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, "dir")
        self.assertContains(response, "default")
        self.assertContains(response, "./")

    def test_plugin_parameter_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/servicefiles/migrations/0001_initial.py

# Generated by Django 2.1.4 on 2020-04-24 00:00

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Service',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('identifier', models.CharField(max_length=20, unique=True)),
            ],
        ),
        migrations.CreateModel(
            name='ServiceFile',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('fname', models.FileField(max_length=512, unique=True, upload_to='')),
                ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='servicefiles.Service')),
            ],
            options={
                'ordering': ('-fname',),
            },
        ),
    ]


# chris_backend/servicefiles/tests/test_serializers.py


import logging
import time
import io
from unittest import mock

from django.test import TestCase, tag
from django.conf import settings
from rest_framework import serializers

from servicefiles.models import Service, ServiceFile
from servicefiles.serializers import ServiceFileSerializer
from servicefiles.serializers import SwiftManager


class ServiceFileSerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_validate_service_name_failure_registered_service(self):
        """
        Test whether overriden validate_name method validates whether submitted
        unregistered service name is actually the name of a registered service.
        """
        servicefiles_serializer = ServiceFileSerializer()
        with self.assertRaises(serializers.ValidationError):
            servicefiles_serializer.validate_service_name('PACS')

    def test_validate_service_name_success(self):
        """
        Test whether overriden validate_name method successfully returns a valid
        unregistered service name.
        """
        Service.objects.get_or_create(identifier='NewService')
        servicefiles_serializer = ServiceFileSerializer()
        self.assertEqual(servicefiles_serializer.validate_service_name('MyService'),
                         'MyService')
        self.assertEqual(servicefiles_serializer.validate_service_name('NewService'),
                         'NewService')

    def test_validate_updates_validated_data(self):
        """
        Test whether overriden validate method updates validated data with the descriptors
        embedded in the path string.
        """
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        data = {'service_name': 'MyService', 'path': path}
        servicefiles_serializer = ServiceFileSerializer()
        with mock.patch.object(SwiftManager, 'obj_exists',
                               return_value=True) as obj_exists_mock:
            new_data = servicefiles_serializer.validate(data)
            self.assertIn('service', new_data)
            self.assertNotIn('service_name', new_data)
            self.assertEqual(new_data.get('path'), path.strip(' ').strip('/'))
            obj_exists_mock.assert_called_with(new_data.get('path'))

    def test_validate_failure_path_does_not_start_with_SERVICES_PACS(self):
        """
        Test whether overriden validate method validates submitted path must start
        with the 'SERVICES/<service_name>/' string.
        """
        path = 'SERVICES/Other/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        data = {'service_name': 'MyService', 'path': path}
        servicefiles_serializer = ServiceFileSerializer()
        with self.assertRaises(serializers.ValidationError):
            servicefiles_serializer.validate(data)

    def test_validate_failure_path_does_not_exist(self):
        """
        Test whether overriden validate method validates that submitted path exists
        in internal storage.
        """
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        data = {'service_name': 'MyService', 'path': path}
        servicefiles_serializer = ServiceFileSerializer()
        with mock.patch.object(SwiftManager, 'obj_exists',
                               return_value=False) as obj_exists_mock:
            with self.assertRaises(serializers.ValidationError):
                servicefiles_serializer.validate(data)
            obj_exists_mock.assert_called_with(path.strip(' ').strip('/'))

    @tag('integration')
    def test_integration_validate_path_failure_does_not_exist(self):
        """
        Test whether overriden validate method validates that submitted path exists
        in internal storage.
        """
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        data = {'service_name': 'MyService', 'path': path}
        servicefiles_serializer = ServiceFileSerializer()
        with self.assertRaises(serializers.ValidationError):
            servicefiles_serializer.validate(data)

    @tag('integration')
    def test_integration_validate_path_success(self):
        """
        Test whether overriden validate method validates submitted path.
        """
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        data = {'service_name': 'MyService', 'path': path}
        servicefiles_serializer = ServiceFileSerializer()

        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        # upload file to Swift storage
        with io.StringIO("test file") as file1:
            swift_manager.upload_obj(path, file1.read(), content_type='text/plain')
        for _ in range(20):
            if swift_manager.obj_exists(path):
                break
            time.sleep(0.2)
        self.assertEqual(servicefiles_serializer.validate(data).get('path'), path)

        # delete file from Swift storage
        swift_manager.delete_obj(path)

    def test_validate_validates_path_has_not_already_been_registered(self):
        """
        Test whether overriden validate method validates that the submitted path
        has not been already registered.
        """
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        data = {'service_name': 'MyService', 'path': path}
        servicefiles_serializer = ServiceFileSerializer()
        service = Service(identifier='MyService')
        service.save()
        service_file = ServiceFile(service=service)
        service_file.fname.name = path
        service_file.save()
        with self.assertRaises(serializers.ValidationError):
            servicefiles_serializer.validate(data)


# chris_backend/servicefiles/tests/test_views.py


import logging
import json
import io
from unittest import mock

from django.test import TestCase, tag
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse

from rest_framework import status

from core.swiftmanager import SwiftManager
from servicefiles.models import Service, ServiceFile
from servicefiles import views


class ServiceFileViewTests(TestCase):
    """
    Generic servicefile view tests' setup and tearDown.
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'
        self.username = 'test'
        self.password = 'testpass'

        User.objects.create_user(username=self.username, password=self.password)

        service = Service(identifier='MyService')
        service.save()

        # create a service file in the DB "already registered" to the server)
        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        # upload file to Swift storage
        self.path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file1.dcm'
        with io.StringIO("test file") as file1:
            self.swift_manager.upload_obj(self.path, file1.read(),
                                          content_type='text/plain')
        service_file = ServiceFile(service=service)
        service_file.fname.name = self.path
        service_file.save()

    def tearDown(self):
        # delete file from Swift storage
        self.swift_manager.delete_obj(self.path)
        # re-enable logging
        logging.disable(logging.NOTSET)


class ServiceFileListViewTests(ServiceFileViewTests):
    """
    Test the servicefile-list view.
    """

    def setUp(self):
        super(ServiceFileListViewTests, self).setUp()
        self.create_read_url = reverse("servicefile-list")
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file2.dcm'
        self.post = json.dumps(
            {"template": {"data": [{"name": "path", "value": path},
                                   {"name": "service_name", "value": "MyService"}]}})

    def tearDown(self):
        super(ServiceFileListViewTests, self).tearDown()

    @tag('integration')
    def test_integration_servicefile_create_success(self):
        chris_username = 'chris'
        chris_password = 'chris1234'
        User.objects.create_user(username=chris_username, password=chris_password)
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file2.dcm'

        # upload file to Swift storage
        with io.StringIO("test file") as file2:
            self.swift_manager.upload_obj(path, file2.read(), content_type='text/plain')

        # make the POST request using the chris user
        self.client.login(username=chris_username, password=chris_password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # delete file from Swift storage
        self.swift_manager.delete_obj(path)

    def test_servicefile_create_failure_unauthenticated(self):
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_servicefile_create_failure_already_exists(self):
        chris_username = 'chris'
        chris_password = 'chris1234'
        User.objects.create_user(username=chris_username, password=chris_password)
        self.client.login(username=chris_username, password=chris_password)
        path = 'SERVICES/MyService/123456-crazy/brain_crazy_study/brain_crazy_mri/file2.dcm'
        service = Service.objects.get(identifier='MyService')
        service_file = ServiceFile(service=service)
        service_file.fname.name = path
        service_file.save()
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_servicefile_create_failure_permission_denied_not_chris_user(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.post(self.create_read_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_servicefile_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, self.path)

    def test_servicefile_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class ServiceFileDetailViewTests(ServiceFileViewTests):
    """
    Test the servicefile-detail view.
    """

    def setUp(self):
        super(ServiceFileDetailViewTests, self).setUp()
        service = Service.objects.get(identifier='MyService')
        service_file = ServiceFile.objects.get(service=service)
        self.read_url = reverse("servicefile-detail",
                                kwargs={"pk": service_file.id})

    def test_servicefile_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_url)
        self.assertContains(response, self.path)

    def test_servicefile_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class ServiceFileResourceViewTests(ServiceFileViewTests):
    """
    Test the servicefile-resource view.
    """

    def setUp(self):
        super(ServiceFileResourceViewTests, self).setUp()
        service = Service.objects.get(identifier='MyService')
        service_file = ServiceFile.objects.get(service=service)
        self.download_url = reverse("servicefile-resource",
                                    kwargs={"pk": service_file.id}) + 'file1.dcm'

    def tearDown(self):
        super(ServiceFileResourceViewTests, self).tearDown()

    def test_servicefileresource_get(self):
        service = Service.objects.get(identifier='MyService')
        service_file = ServiceFile.objects.get(service=service)
        fileresource_view_inst = mock.Mock()
        fileresource_view_inst.get_object = mock.Mock(return_value=service_file)
        request_mock = mock.Mock()
        with mock.patch('servicefiles.views.FileResponse') as response_mock:
            views.ServiceFileResource.get(fileresource_view_inst, request_mock)
            response_mock.assert_called_with(service_file.fname)

    @tag('integration')
    def test_integration_servicefileresource_download_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, 200)
        content = [c for c in response.streaming_content][0].decode('utf-8')
        self.assertEqual(content, "test file")

    def test_fileresource_download_failure_unauthenticated(self):
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/uploadedfiles/migrations/0001_initial.py

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2017-12-18 20:15
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uploadedfiles.models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='UploadedFile',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('creation_date', models.DateTimeField(auto_now_add=True)),
                ('fname', models.FileField(max_length=512, upload_to=uploadedfiles.models.uploaded_file_path)),
                ('upload_path', models.CharField(max_length=512)),
                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
            ],
        ),
    ]


# chris_backend/uploadedfiles/migrations/0002_auto_20200424_0554.py

# Generated by Django 2.1.4 on 2020-04-24 05:54

from django.db import migrations, models
import uploadedfiles.models


class Migration(migrations.Migration):

    dependencies = [
        ('uploadedfiles', '0001_initial'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='uploadedfile',
            options={'ordering': ('-fname',)},
        ),
        migrations.RemoveField(
            model_name='uploadedfile',
            name='upload_path',
        ),
        migrations.AlterField(
            model_name='uploadedfile',
            name='fname',
            field=models.FileField(max_length=512, unique=True, upload_to=uploadedfiles.models.uploaded_file_path),
        ),
    ]


# chris_backend/uploadedfiles/tests/test_models.py


import logging
from unittest import mock

from django.test import TestCase
from django.contrib.auth.models import User
from uploadedfiles.models import UploadedFile, uploaded_file_path


class UploadedFileModelTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_uploaded_file_path(self):
        uploadedfile_instance = mock.Mock()
        uploadedfile_instance.owner = mock.Mock(spec=User)
        uploadedfile_instance.owner.username = 'foo'
        uploadedfile_instance.owner.upload_path = 'foo/uploads/myuploads'
        filename = 'file1.txt'
        file_path = uploaded_file_path(uploadedfile_instance, filename)
        self.assertEqual(file_path, 'foo/uploads/myuploads')

    def test_str(self):
        uploadedfile_mock = mock.MagicMock(spec=UploadedFile)
        uploadedfile_mock.fname.name = 'foo/uploads/myuploads'
        self.assertEqual(UploadedFile.__str__(uploadedfile_mock), 'foo/uploads/myuploads')

# chris_backend/uploadedfiles/tests/test_serializers.py



import logging
from unittest import mock

from django.test import TestCase, tag
from django.contrib.auth.models import User
from rest_framework import serializers

from uploadedfiles.serializers import UploadedFileSerializer


class UploadedFileSerializerTests(TestCase):

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_validate_path_failure_does_not_start_with_username_uploads(self):
        """
        Test whether overriden validate_path method validates submitted path must start
        with the '<username>/<uploads>/' string.
        """
        uploadedfiles_serializer = UploadedFileSerializer()
        user = mock.Mock(spec=User)
        user.username = 'cube'
        request = mock.Mock()
        request.user = user
        with mock.patch.dict(uploadedfiles_serializer.context,
                             {'request': request}, clear=True):
            with self.assertRaises(serializers.ValidationError):
                uploadedfiles_serializer.validate_upload_path('foo/file1.txt')
            with self.assertRaises(serializers.ValidationError):
                uploadedfiles_serializer.validate_upload_path('cube/file1.txt')
            with self.assertRaises(serializers.ValidationError):
                uploadedfiles_serializer.validate_upload_path('cube/uploads_file1.txt')

    @tag('integration')
    def test_validate_path_success(self):
        """
        Test whether overriden validate_path method validates submitted path.
        """
        uploadedfiles_serializer = UploadedFileSerializer()
        user = mock.Mock(spec=User)
        user.username = 'cube'
        request = mock.Mock()
        request.user = user
        with mock.patch.dict(uploadedfiles_serializer.context,
                             {'request': request}, clear=True):
            upload_path = 'cube/uploads/file1.txt'
            self.assertEqual(uploadedfiles_serializer.validate_upload_path(upload_path),
                             upload_path)

    def test_validate_updates_validated_data(self):
        """
        Test whether overriden validate method updates validated data with a PACS object.
        """
        data = {'upload_path': 'cube/uploads_file1.txt'}
        uploadedfiles_serializer = UploadedFileSerializer()
        user = mock.Mock(spec=User)
        user.username = 'cube'
        request = mock.Mock()
        request.user = user
        with mock.patch.dict(uploadedfiles_serializer.context,
                             {'request': request}, clear=True):
            new_data = uploadedfiles_serializer.validate(data)
            self.assertEqual(new_data['owner'].upload_path, 'cube/uploads_file1.txt')


# chris_backend/uploadedfiles/tests/test_views.py


import logging
import json
import io
from unittest import mock

from django.test import TestCase, tag
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse

from rest_framework import status

from core.swiftmanager import SwiftManager
from uploadedfiles.models import UploadedFile, uploaded_file_path
from uploadedfiles import views


class UploadedFileViewTests(TestCase):
    """
    Generic uploadedfile view tests' setup and tearDown.
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'
        self.chris_username = 'chris'
        self.chris_password = 'chrispass'
        self.username = 'test'
        self.password = 'testpass'
        self.other_username = 'boo'
        self.other_password = 'far'

        # create the chris superuser and two additional users
        User.objects.create_user(username=self.chris_username,
                                 password=self.chris_password)
        User.objects.create_user(username=self.other_username,
                                 password=self.other_password)
        user = User.objects.create_user(username=self.username,
                                 password=self.password)

        # create a file in the DB "already uploaded" to the server)
        self.swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        # upload file to Swift storage
        self.upload_path = f'{self.username}/uploads/file1.txt'
        with io.StringIO("test file") as file1:
            self.swift_manager.upload_obj(self.upload_path, file1.read(),
                                          content_type='text/plain')
            self.uploadedfile = UploadedFile(owner=user)
            self.uploadedfile.fname.name = self.upload_path
            self.uploadedfile.save()

    def tearDown(self):
        # delete file from Swift storage
        self.swift_manager.delete_obj(self.upload_path)
        # re-enable logging
        logging.disable(logging.NOTSET)


class UploadedFileListViewTests(UploadedFileViewTests):
    """
    Test the uploadedfile-list view.
    """

    def setUp(self):
        super(UploadedFileListViewTests, self).setUp()
        self.create_read_url = reverse("uploadedfile-list")

    def tearDown(self):
        super(UploadedFileListViewTests, self).tearDown()

    @tag('integration')
    def test_integration_uploadedfile_create_success(self):

        # POST request using multipart/form-data to be able to upload file
        self.client.login(username=self.username, password=self.password)
        upload_path = f'{self.username}/uploads/file2.txt'

        with io.StringIO("test file") as f:
            post = {"fname": f, "upload_path": upload_path}
            response = self.client.post(self.create_read_url, data=post)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # delete file from Swift storage
        self.swift_manager.delete_obj(upload_path)

    def test_uploadedfile_create_failure_unauthenticated(self):
        upload_path = "{}/uploads/file2.txt".format(self.username)
        response = self.client.post(self.create_read_url,
                                    data={"fname": {}, "upload_path": upload_path})
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_uploadedfile_list_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "file1.txt")

    def test_uploadedfile_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class UploadedFileDetailViewTests(UploadedFileViewTests):
    """
    Test the uploadedfile-detail view.
    """

    def setUp(self):
        super(UploadedFileDetailViewTests, self).setUp()
        self.read_update_delete_url = reverse("uploadedfile-detail",
                                              kwargs={"pk": self.uploadedfile.id})
        upload_path = "{}/uploads/myfolder/myfile1.txt".format(self.username)
        self.put = json.dumps({
            "template": {"data": [{"name": "upload_path", "value": upload_path}]}})

    def test_uploadedfile_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "file1.txt")

    def test_uploadedfile_detail_success_user_chris(self):
        self.client.login(username=self.chris_username, password=self.chris_password)
        response = self.client.get(self.read_update_delete_url)
        self.assertContains(response, "file1.txt")

    def test_uploadedfile_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_uploadedfile_update_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_uploadedfile_update_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_uploadedfile_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        swift_path = self.uploadedfile.fname.name
        mocked_method = 'uploadedfiles.views.SwiftManager.delete_obj'
        with mock.patch(mocked_method) as delete_obj_mock:
            response = self.client.delete(self.read_update_delete_url)
            delete_obj_mock.assert_called_with(swift_path)
            self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
            self.assertEqual(UploadedFile.objects.count(), 0)

    def test_uploadedfile_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_uploadedfile_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_update_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class UploadedFileResourceViewTests(UploadedFileViewTests):
    """
    Test the uploadedfile-resource view.
    """

    def setUp(self):
        super(UploadedFileResourceViewTests, self).setUp()
        self.download_url = reverse("uploadedfile-resource",
                                    kwargs={"pk": self.uploadedfile.id}) + 'file1.txt'

    def tearDown(self):
        super(UploadedFileResourceViewTests, self).tearDown()

    def test_uploadedfileresource_get(self):
        uploadedfile = self.uploadedfile
        fileresource_view_inst = mock.Mock()
        fileresource_view_inst.get_object = mock.Mock(return_value=uploadedfile)
        request_mock = mock.Mock()
        with mock.patch('uploadedfiles.views.FileResponse') as response_mock:
            views.UploadedFileResource.get(fileresource_view_inst, request_mock)
            response_mock.assert_called_with(uploadedfile.fname)

    @tag('integration')
    def test_integration_uploadedfileresource_download_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, 200)
        content = [c for c in response.streaming_content][0].decode('utf-8')
        self.assertEqual(content, "test file")

    def test_fileresource_download_failure_not_related_feed_owner(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_fileresource_download_failure_unauthenticated(self):
        response = self.client.get(self.download_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


# chris_backend/users/tests/test_serializers.py


import logging


from unittest import mock

from django.test import TestCase

from rest_framework import serializers

from uploadedfiles.models import UploadedFile
from users.serializers import UserSerializer, SwiftManager


class UserSerializerTests(TestCase):
    """
    Generic user view tests' setup and tearDown
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.username = 'cube'
        self.password = 'cubepass'
        self.email = 'dev@babymri.org'

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)

    def test_create(self):
        """
        Test whether overriden create method takes care of the password hashing and
        creates a welcome file for the user in its personal storage space.
        """
        user_serializer = UserSerializer()
        validated_data = {'username': self.username, 'password': self.password,
                          'email': self.email}
        with mock.patch.object(SwiftManager, 'upload_obj',
                               return_value=None) as upload_obj_mock:

            user = user_serializer.create(validated_data)

            self.assertEqual(user.username, self.username)
            self.assertEqual(user.email, self.email)
            self.assertNotEqual(user.password, self.password)
            self.assertTrue(user.check_password(self.password))

            welcome_file_path = '%s/uploads/welcome.txt' % self.username
            welcome_file = UploadedFile.objects.get(owner=user)
            self.assertEqual(welcome_file.fname.name, welcome_file_path)
            upload_obj_mock.assert_called_with(welcome_file_path, mock.ANY,
                                               content_type='text/plain')

    def test_validate_username(self):
        """
        Test whether overriden validate_username method raises a
        serializers.ValidationError when the username contains forward slashes or it is
        'chris' or 'SERVICES' special identifiers.
        """
        user_serializer = UserSerializer()
        with self.assertRaises(serializers.ValidationError):
            user_serializer.validate_username('user/')
        with self.assertRaises(serializers.ValidationError):
            user_serializer.validate_username('chris')
        with self.assertRaises(serializers.ValidationError):
            user_serializer.validate_username('SERVICES')
        username = user_serializer.validate_username(self.username)
        self.assertEqual(username, self.username)


# chris_backend/users/tests/test_views.py


import logging
import json
from unittest import mock

from django.test import TestCase, tag
from django.contrib.auth.models import User
from django.urls import reverse
from django.conf import settings

from rest_framework import status

from uploadedfiles.models import UploadedFile
from users.serializers import SwiftManager


class UserViewTests(TestCase):
    """
    Generic user view tests' setup and tearDown
    """

    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'
        self.username = 'cube'
        self.password = 'cubepass'
        self.email = 'dev@babymri.org'

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class UserCreateViewTests(UserViewTests):
    """
    Test the user-create view
    """

    def setUp(self):
        super(UserCreateViewTests, self).setUp()
        self.create_url = reverse("user-create")
        self.post = json.dumps(
            {"template": {"data": [{"name": "username", "value": self.username},
                                   {"name": "password", "value": self.password},
                                   {"name": "email", "value": self.email}]}})

    def test_user_create_success(self):
        with mock.patch.object(SwiftManager, 'upload_obj',
                               return_value=None) as upload_obj_mock:
            response = self.client.post(self.create_url, data=self.post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
            self.assertEqual(response.data["username"], self.username)
            self.assertEqual(response.data["email"], self.email)

            welcome_file_path = '%s/uploads/welcome.txt' % self.username
            upload_obj_mock.assert_called_with(welcome_file_path, mock.ANY,
                                               content_type='text/plain')

    @tag('integration')
    def test_integration_user_create_success(self):
        response = self.client.post(self.create_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data["username"], self.username)
        self.assertEqual(response.data["email"], self.email)

        user = User.objects.get(username=self.username)
        welcome_file_path = '%s/uploads/welcome.txt' % self.username
        welcome_file = UploadedFile.objects.get(owner=user)
        self.assertEqual(welcome_file.fname.name, welcome_file_path)

        # delete welcome file
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        swift_manager.delete_obj(welcome_file_path)

    def test_user_create_failure_already_exists(self):
        User.objects.create_user(username=self.username,
                                 email=self.email,
                                 password=self.password)
        response = self.client.post(self.create_url, data=self.post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_user_create_failure_bad_password(self):
        post = json.dumps(
            {"template": {"data": [{"name": "username", "value": "new_user"},
                                   {"name": "email", "value": self.email},
                                   {"name": "password", "value": "small"}]}})
        response = self.client.post(self.create_url, data=post,
                                    content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)


class UserDetailViewTests(UserViewTests):
    """
    Test the user-detail view
    """

    def setUp(self):
        super(UserDetailViewTests, self).setUp()
        user = User.objects.create_user(username=self.username,
                                        email=self.email,
                                        password=self.password)
        self.read_update_url = reverse("user-detail", kwargs={"pk": user.id})
        self.put = json.dumps({
            "template": {"data": [{"name": "password", "value": "updated_pass"},
                                  {"name": "email", "value": "dev1@babymri.org"}]}})

    def test_user_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_update_url)
        self.assertContains(response, self.username)
        self.assertContains(response, self.email)

    def test_user_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_update_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_user_update_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertContains(response, self.username)
        self.assertContains(response, "dev1@babymri.org")

    def test_user_update_failure_unauthenticated(self):
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_user_update_failure_access_denied(self):
        User.objects.create_user(username="other_username", email="dev2@babymri.org",
                                 password="other_password")
        self.client.login(username="other_username", password="other_password")
        response = self.client.put(self.read_update_url, data=self.put,
                                   content_type=self.content_type)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

# chris_backend/collectionjson/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/collectionjson/apps.py

from django.apps import AppConfig


class CollectionjsonConfig(AppConfig):
    name = 'collectionjson'


# chris_backend/collectionjson/fields.py


from rest_framework.fields import SerializerMethodField


class ItemLinkField(SerializerMethodField):
    def __init__(self, method_name, *args, **kwargs):
        super(ItemLinkField, self).__init__(method_name, *args, **kwargs)


# chris_backend/collectionjson/models.py


from collectionjson.tests.models import Dummy, Idiot, Moron, Simple





# chris_backend/collectionjson/parsers.py

from rest_framework.parsers import JSONParser
from rest_framework.exceptions import ParseError

class CollectionJsonParser(JSONParser):
    media_type = 'application/vnd.collection+json'

    def validate_data(self, stream_data):
        template_valid_str = "Valid format: {template:{data:[{name: ,value: },...]}}"

        if not isinstance(stream_data, dict):
            detail = "Template is not a dictionary. "
            detail += template_valid_str
            raise ParseError(detail=detail)

        json_data = {}
        try:
            for x in stream_data['template']['data']:
                json_data[x['name']] = x['value']
        except KeyError as e:
            detail = "%s field required. " % e 
            detail += template_valid_str  
            raise ParseError(detail=detail)
        except TypeError as e:
            detail = "Invalid data provided. "
            detail += template_valid_str  
            raise ParseError(detail=detail)
        return json_data 

    def parse(self, stream, media_type=None, parser_context=None):
        stream_data = super(CollectionJsonParser, self).parse(stream, media_type,
                                                          parser_context)
        return self.validate_data(stream_data)




# chris_backend/collectionjson/renderers.py


import json

from rest_framework.serializers import (HyperlinkedRelatedField, HyperlinkedIdentityField,
                                        HyperlinkedModelSerializer, ManyRelatedField)
from rest_framework.renderers import JSONRenderer

from .fields import ItemLinkField
from .serializers import NoModelSerializer


class CollectionJsonRenderer(JSONRenderer):
    media_type = 'application/vnd.collection+json'
    format = 'collection+json'

    def _transform_field(self, key, value):
        return {'name': key, 'value': value}

    def _simple_transform_item(self, item):
        data = [self._transform_field(k, v) for (k, v) in item.items()]
        return {'data': data}

    def _get_id_field(self, serializer):
        if isinstance(serializer, HyperlinkedModelSerializer):
            return serializer.url_field_name
        elif isinstance(serializer, NoModelSerializer):
            return serializer.get_url_field_name()
        else:
            return None

    def _get_related_fields(self, fields, id_field):
        return [k for (k, v) in fields
                if k != id_field
                and (isinstance(v, HyperlinkedRelatedField)
                or isinstance(v, HyperlinkedIdentityField)   
                or isinstance(v, ItemLinkField)
                or (isinstance(v, ManyRelatedField)
                    and isinstance(v.child_relation, HyperlinkedRelatedField)))]

    def _make_link(self, rel, href):
        return {'rel': rel, 'href': href}

    def _get_item_field_links(self, field_name, item):
        data = item[field_name]

        if data is None:
            return []
        elif isinstance(data, list):
            return [self._make_link(field_name, x) for x in data]
        else:
            return [self._make_link(field_name, data)]

    def _transform_item(self, serializer, item):
        fields = serializer.fields.items()
        id_field = self._get_id_field(serializer)
        related_fields = self._get_related_fields(fields, id_field)

        data = [self._transform_field(k, item[k])
                for k in item.keys()
                if k != id_field and k not in related_fields]
        result = {'data': data}

        if id_field:
            result['href'] = item[id_field]

        links = []
        for x in related_fields:
            links.extend(self._get_item_field_links(x, item))

        if links:
            result['links'] = links

        return result

    def _transform_items(self, view, data):
        if isinstance(data, dict):
            data = [data]

        if hasattr(view, 'get_serializer'):
            serializer = view.get_serializer()
            return map(lambda x: self._transform_item(serializer, x), data)
        else:
            return map(self._simple_transform_item, data)

    def _is_paginated(self, data):
        pagination_keys = ('next', 'previous', 'results')
        return all(k in data for k in pagination_keys)

    def _get_pagination_links(self, data):
        results = []
        if data.get('next', None):
            results.append(self._make_link('next', data['next']))

        if data.get('previous', None):
            results.append(self._make_link('previous', data['previous']))

        return results

    def _get_items_from_paginated_data(self, data):
        return data.get('results')

    def _get_error(self, data):
        message = data['detail'] if 'detail' in data else json.dumps(data)
        return {
            'error': {
                'message': message
            }
        }

    def _get_items_and_links(self, view, data):
        # This lookup of the Api Root string isn't
        # the right long-term approach. Even if we
        # looked it up properly from the default
        # router, we would still need to handle
        # custom routers. Works okay for now.
        # ------------------------------------------

        links = []
        if 'collection_links' in data:
            l = data.pop('collection_links')
            links = [self._make_link(key, l[key]) for key in l.keys()]
            
        if view.get_view_name() == 'Api Root':
            links = links.extend([self._make_link(key, data[key]) for key in data.keys()])
            items = []
        else:
            if self._is_paginated(data):
                links.extend(self._get_pagination_links(data))
                data = self._get_items_from_paginated_data(data)
            
            items = self._transform_items(view, data)

        return {
            'items': items,
            'links': links,
        }
    
    def get_href(self, request):
        return request.build_absolute_uri()

    def _transform_data(self, request, response, view, data):
        collection = {
            "version": "1.0",
            "href": self.get_href(request),
        }

        if response.exception:
            collection.update(self._get_error(data))
        else:
            collection.update(self._get_items_and_links(view, data))

        if 'queries' in data:
            collection['queries'] = data.pop('queries')

        if 'template' in data:
            collection['template'] = data.pop('template')

        if 'count' in data:
            collection['total'] = data['count']

        return {'collection': collection}

    def render(self, data, media_type=None, renderer_context=None):
        request = renderer_context['request']
        view = renderer_context['view']
        response = renderer_context['response']

        if data:
            data = self._transform_data(request, response, view, data)

        return super(CollectionJsonRenderer, self).render(data, media_type,
                                                          renderer_context)


# chris_backend/collectionjson/serializers.py


from rest_framework import serializers


class NoModelSerializer(serializers.Serializer):
    """
    A serializer that is not associated to a DB model.
    """

    def get_url_field_name(self):
        """
        Custom method to get the name of the url field.
        """
        return 'url'


# chris_backend/collectionjson/services.py


from urllib.parse import urlparse

from django.urls import resolve

from rest_framework.response import Response
from rest_framework import serializers


def get_list_response(list_view_instance, queryset):
    """
    Convenience function to get an HTTP response with a list of objects
    from a list view instance and a queryset
    """
    page = list_view_instance.paginate_queryset(queryset)
    if page is not None:
        serializer = list_view_instance.get_serializer(page, many=True)
        return list_view_instance.get_paginated_response(serializer.data)

    serializer = list_view_instance.get_serializer(queryset, many=True)
    return Response(serializer.data)


def append_collection_links(response, link_dict):
    """
    Convenience function to append document-level links to a response object.
    """
    data = response.data
    if not 'collection_links' in data:
        data['collection_links'] = {}
        
    for (link_relation_name, url) in link_dict.items():
        data['collection_links'][link_relation_name] = url
    return response


def append_collection_template(response, template_data):
    """
    Convenience function to append to a response a collection+json template.
    """
    data = []
    for (k, v) in template_data.items():
        data.append({"name": k, "value": v})
    response.data["template"] = {"data": data}
    return response


def append_collection_querylist(response, query_url_list):
    """
    Convenience function to append to a response a collection+json queries template.
    """
    queries = []
    for query_url in query_url_list:
        relative_url = urlparse(query_url).path
        match = resolve(relative_url)
        filters = match.func.cls.filterset_class.base_filters
        data = []
        for k in filters.keys():
            data.append({"name": k, "value": ""})
        queries.append({'href': query_url, 'rel': 'search', "data": data})
    response.data["queries"] = queries
    return response


def collection_serializer_is_valid(is_valid_method):
    """
    Convenience 'is_valid' method decorator to generate a properly formatted message
    for serializers' validation errors.
    """
    def new_is_valid(*args, **kwargs):
        try:
            valid = is_valid_method(*args, **kwargs)
        except serializers.ValidationError as error:
            raise serializers.ValidationError({'detail': error})
        return valid
    return new_is_valid

# chris_backend/config/urls.py

"""chris_backend URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings

from plugins import admin as plugin_admin_views


urlpatterns = [
    path('chris-admin/', admin.site.urls),

    path('chris-admin/api/v1/',
         plugin_admin_views.PluginAdminList.as_view(),
         name='admin-plugin-list'),

    path('chris-admin/api/v1/computeresources/',
         plugin_admin_views.ComputeResourceAdminList.as_view(),
         name='admin-computeresource-list'),

    path('api/', include('core.api')),
]

if settings.DEBUG:
    import debug_toolbar

    urlpatterns += [
        path('__debug__/', include(debug_toolbar.urls)),
    ]


# chris_backend/config/wsgi.py

"""
WSGI config for chris_backend project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""

import os, sys
sys.path.append(os.path.dirname(os.path.dirname(__file__)))


from django.core.wsgi import get_wsgi_application


os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")

application = get_wsgi_application()


# chris_backend/core/__init__.py


# This will make sure the app is always imported when
# Django starts so that shared_task decorator will use this app
from .celery import app as celery_app

__all__ = ('celery_app',)


# chris_backend/core/admin.py


from django.contrib import admin
from django.contrib.auth.models import Group

from .models import ChrisInstance


class ChrisInstanceAdmin(admin.ModelAdmin):
    readonly_fields = ['creation_date', 'uuid']
    list_display = ('name', 'uuid', 'job_id_prefix', 'creation_date')

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return False


admin.site.site_header = 'ChRIS Administration'
admin.site.site_title = 'ChRIS Admin'
admin.site.unregister(Group)
admin.site.register(ChrisInstance, ChrisInstanceAdmin)


# chris_backend/core/api.py


from django.urls import path, re_path, include
from rest_framework.urlpatterns import format_suffix_patterns
from rest_framework.authtoken.views import obtain_auth_token

from core import views as core_views
from feeds import views as feed_views
from plugins import views as plugin_views
from plugininstances import views as plugininstance_views
from pipelines import views as pipeline_views
from pipelineinstances import views as pipelineinstance_views
from uploadedfiles import views as uploadedfile_views
from pacsfiles import views as pacsfile_views
from servicefiles import views as servicefile_views
from filebrowser import views as filebrowser_views
from users import views as user_views


# API v1 endpoints
urlpatterns = format_suffix_patterns([

    path('v1/auth-token/',
        obtain_auth_token),


    path('v1/users/',
        user_views.UserCreate.as_view(), name='user-create'),

    path('v1/users/<int:pk>/',
        user_views.UserDetail.as_view(), name='user-detail'),


    path('v1/chrisinstance/<int:pk>/',
         core_views.ChrisInstanceDetail.as_view(), name='chrisinstance-detail'),


    path('v1/',
        feed_views.FeedList.as_view(), name='feed-list'),

    path('v1/search/',
        feed_views.FeedListQuerySearch.as_view(), name='feed-list-query-search'),

    path('v1/<int:pk>/',
        feed_views.FeedDetail.as_view(), name='feed-detail'),

    path('v1/note<int:pk>/',
        feed_views.NoteDetail.as_view(), name='note-detail'),

    path('v1/<int:pk>/comments/',
        feed_views.CommentList.as_view(), name='comment-list'),

    path('v1/<int:pk>/comments/search/',
        feed_views.CommentListQuerySearch.as_view(), name='comment-list-query-search'),

    path('v1/comments/<int:pk>/',
        feed_views.CommentDetail.as_view(), name='comment-detail'),

    path('v1/<int:pk>/files/',
        feed_views.FeedFileList.as_view(), name='feedfile-list'),

    path('v1/<int:pk>/plugininstances/',
        feed_views.FeedPluginInstanceList.as_view(), name='feed-plugininstance-list'),

    path('v1/<int:pk>/tags/',
        feed_views.FeedTagList.as_view(), name='feed-tag-list'),

    path('v1/<int:pk>/taggings/',
        feed_views.FeedTaggingList.as_view(), name='feed-tagging-list'),

    path('v1/tags/',
        feed_views.TagList.as_view(), name='tag-list'),

    path('v1/tags/search/',
        feed_views.TagListQuerySearch.as_view(), name='tag-list-query-search'),

    path('v1/tags/<int:pk>/',
        feed_views.TagDetail.as_view(), name='tag-detail'),

    path('v1/tags/<int:pk>/feeds/',
        feed_views.TagFeedList.as_view(), name='tag-feed-list'),

    path('v1/tags/<int:pk>/taggings/',
        feed_views.TagTaggingList.as_view(), name='tag-tagging-list'),

    path('v1/taggings/<int:pk>/',
        feed_views.TaggingDetail.as_view(), name='tagging-detail'),


    path('v1/computeresources/',
         plugin_views.ComputeResourceList.as_view(), name='computeresource-list'),

    path('v1/computeresources/search/',
         plugin_views.ComputeResourceListQuerySearch.as_view(),
         name='computeresource-list-query-search'),

    path('v1/computeresources/<int:pk>/',
         plugin_views.ComputeResourceDetail.as_view(), name='computeresource-detail'),


    path('v1/plugins/metas/',
         plugin_views.PluginMetaList.as_view(),
         name='pluginmeta-list'),

    path('v1/plugins/metas/search/',
         plugin_views.PluginMetaListQuerySearch.as_view(),
         name='pluginmeta-list-query-search'),

    path('v1/plugins/metas/<int:pk>/',
         plugin_views.PluginMetaDetail.as_view(),
         name='pluginmeta-detail'),

    path('v1/plugins/metas/<int:pk>/plugins/',
         plugin_views.PluginMetaPluginList.as_view(),
         name='pluginmeta-plugin-list'),


    path('v1/plugins/',
        plugin_views.PluginList.as_view(), name='plugin-list'),

    path('v1/plugins/search/',
        plugin_views.PluginListQuerySearch.as_view(), name='plugin-list-query-search'),

    path('v1/plugins/<int:pk>/',
        plugin_views.PluginDetail.as_view(), name='plugin-detail'),

    path('v1/plugins/<int:pk>/parameters/',
        plugin_views.PluginParameterList.as_view(), name='pluginparameter-list'),

    path('v1/plugins/parameters/<int:pk>/',
        plugin_views.PluginParameterDetail.as_view(), name='pluginparameter-detail'),

    path('v1/plugins/<int:pk>/computeresources/',
        plugin_views.PluginComputeResourceList.as_view(),
         name='plugin-computeresource-list'),


    path('v1/pipelines/',
        pipeline_views.PipelineList.as_view(),
        name='pipeline-list'),

    path('v1/pipelines/search/',
        pipeline_views.PipelineListQuerySearch.as_view(),
        name='pipeline-list-query-search'),

    path('v1/pipelines/<int:pk>/',
        pipeline_views.PipelineDetail.as_view(),
        name='pipeline-detail'),

    path('v1/pipelines/<int:pk>/plugins/',
        pipeline_views.PipelinePluginList.as_view(), name='pipeline-plugin-list'),

    path('v1/pipelines/<int:pk>/pipings/',
        pipeline_views.PipelinePluginPipingList.as_view(),
        name='pipeline-pluginpiping-list'),

    path('v1/pipelines/<int:pk>/parameters/',
        pipeline_views.PipelineDefaultParameterList.as_view(),
        name='pipeline-defaultparameter-list'),

    path('v1/pipelines/pipings/<int:pk>/',
        pipeline_views.PluginPipingDetail.as_view(),
        name='pluginpiping-detail'),

    path('v1/pipelines/string-parameter/<int:pk>/',
        pipeline_views.DefaultPipingStrParameterDetail.as_view(),
        name='defaultpipingstrparameter-detail'),

    path('v1/pipelines/integer-parameter/<int:pk>/',
        pipeline_views.DefaultPipingIntParameterDetail.as_view(),
        name='defaultpipingintparameter-detail'),

    path('v1/pipelines/float-parameter/<int:pk>/',
        pipeline_views.DefaultPipingFloatParameterDetail.as_view(),
        name='defaultpipingfloatparameter-detail'),

    path('v1/pipelines/boolean-parameter/<int:pk>/',
        pipeline_views.DefaultPipingBoolParameterDetail.as_view(),
        name='defaultpipingboolparameter-detail'),


    path('v1/plugins/<int:pk>/instances/',
        plugininstance_views.PluginInstanceList.as_view(),
        name='plugininstance-list'),

    path('v1/plugins/instances/',
        plugininstance_views.AllPluginInstanceList.as_view(),
        name='allplugininstance-list'),

    path('v1/plugins/instances/search/',
        plugininstance_views.AllPluginInstanceListQuerySearch.as_view(),
        name='allplugininstance-list-query-search'),

    path('v1/plugins/instances/<int:pk>/',
        plugininstance_views.PluginInstanceDetail.as_view(),
        name='plugininstance-detail'),

    path('v1/plugins/instances/<int:pk>/splits/',
         plugininstance_views.PluginInstanceSplitList.as_view(),
         name='plugininstancesplit-list'),

    path('v1/plugins/instances/splits/<int:pk>/',
         plugininstance_views.PluginInstanceSplitDetail.as_view(),
         name='plugininstancesplit-detail'),

    path('v1/plugins/instances/<int:pk>/descendants/',
        plugininstance_views.PluginInstanceDescendantList.as_view(),
        name='plugininstance-descendant-list'),

    path('v1/plugins/instances/<int:pk>/files/',
        plugininstance_views.PluginInstanceFileList.as_view(),
        name='plugininstancefile-list'),

    path('v1/files/',
        plugininstance_views.AllPluginInstanceFileList.as_view(),
        name='allplugininstancefile-list'),

    path('v1/files/search/',
        plugininstance_views.AllPluginInstanceFileListQuerySearch.as_view(),
        name='allplugininstancefile-list-query-search'),

    path('v1/files/<int:pk>/',
        plugininstance_views.PluginInstanceFileDetail.as_view(),
        name='plugininstancefile-detail'),

    re_path(r'^v1/files/(?P<pk>[0-9]+)/.*$',
        plugininstance_views.FileResource.as_view(),
        name='plugininstancefile-resource'),

    path('v1/plugins/instances/<int:pk>/parameters/',
        plugininstance_views.PluginInstanceParameterList.as_view(),
        name='plugininstance-parameter-list'),

    path('v1/plugins/string-parameter/<int:pk>/',
        plugininstance_views.StrParameterDetail.as_view(),
        name='strparameter-detail'),

    path('v1/plugins/integer-parameter/<int:pk>/',
        plugininstance_views.IntParameterDetail.as_view(),
        name='intparameter-detail'),

    path('v1/plugins/float-parameter/<int:pk>/',
        plugininstance_views.FloatParameterDetail.as_view(),
        name='floatparameter-detail'),

    path('v1/plugins/boolean-parameter/<int:pk>/',
        plugininstance_views.BoolParameterDetail.as_view(),
        name='boolparameter-detail'),

    path('v1/plugins/path-parameter/<int:pk>/',
        plugininstance_views.PathParameterDetail.as_view(),
        name='pathparameter-detail'),

    path('v1/plugins/unextpath-parameter/<int:pk>/',
         plugininstance_views.UnextpathParameterDetail.as_view(),
         name='unextpathparameter-detail'),


    path('v1/pipelines/<int:pk>/instances/',
        pipelineinstance_views.PipelineInstanceList.as_view(),
        name='pipelineinstance-list'),

    path('v1/pipelines/instances/',
        pipelineinstance_views.AllPipelineInstanceList.as_view(),
        name='allpipelineinstance-list'),

    path('v1/pipelines/instances/search/',
        pipelineinstance_views.AllPipelineInstanceListQuerySearch.as_view(),
        name='allpipelineinstance-list-query-search'),

    path('v1/pipelines/instances/<int:pk>/',
        pipelineinstance_views.PipelineInstanceDetail.as_view(),
        name='pipelineinstance-detail'),

    path('v1/pipelines/instances/<int:pk>/plugininstances/',
        pipelineinstance_views.PipelineInstancePluginInstanceList.as_view(),
        name='pipelineinstance-plugininstance-list'),


    path('v1/uploadedfiles/',
        uploadedfile_views.UploadedFileList.as_view(),
        name='uploadedfile-list'),

    path('v1/uploadedfiles/search/',
        uploadedfile_views.UploadedFileListQuerySearch.as_view(),
        name='uploadedfile-list-query-search'),

    path('v1/uploadedfiles/<int:pk>/',
        uploadedfile_views.UploadedFileDetail.as_view(),
        name='uploadedfile-detail'),

    re_path(r'^v1/uploadedfiles/(?P<pk>[0-9]+)/.*$',
        uploadedfile_views.UploadedFileResource.as_view(),
        name='uploadedfile-resource'),


    path('v1/pacsfiles/',
        pacsfile_views.PACSFileList.as_view(),
        name='pacsfile-list'),

    path('v1/pacsfiles/search/',
        pacsfile_views.PACSFileListQuerySearch.as_view(),
        name='pacsfile-list-query-search'),

    path('v1/pacsfiles/<int:pk>/',
        pacsfile_views.PACSFileDetail.as_view(),
        name='pacsfile-detail'),

    re_path(r'^v1/pacsfiles/(?P<pk>[0-9]+)/.*$',
        pacsfile_views.PACSFileResource.as_view(),
        name='pacsfile-resource'),


    path('v1/servicefiles/',
        servicefile_views.ServiceFileList.as_view(),
        name='servicefile-list'),

    path('v1/servicefiles/search/',
        servicefile_views.ServiceFileListQuerySearch.as_view(),
        name='servicefile-list-query-search'),

    path('v1/servicefiles/<int:pk>/',
        servicefile_views.ServiceFileDetail.as_view(),
        name='servicefile-detail'),

    re_path(r'^v1/servicefiles/(?P<pk>[0-9]+)/.*$',
        servicefile_views.ServiceFileResource.as_view(),
        name='servicefile-resource'),


    path('v1/filebrowser/',
         filebrowser_views.FileBrowserPathList.as_view(),
         name='filebrowserpath-list'),

    path('v1/filebrowser/search/',
         filebrowser_views.FileBrowserPathListQuerySearch.as_view(),
         name='filebrowserpath-list-query-search'),

    path('v1/filebrowser/<path:path>/',
         filebrowser_views.FileBrowserPath.as_view(),
         name='filebrowserpath'),

    path('v1/filebrowser-files/<path:path>/',
         filebrowser_views.FileBrowserPathFileList.as_view(),
         name='filebrowserpathfile-list'),

])

# Login and logout views for Djangos' browsable API
urlpatterns += [
    path('v1/auth/', include('rest_framework.urls',  namespace='rest_framework')),
]

# chris_backend/core/apps.py

from django.apps import AppConfig


class Core(AppConfig):
    name = 'core'


# chris_backend/core/celery.py


import os
from logging.config import dictConfig
from django.conf import settings

from celery import Celery
from celery.signals import setup_logging

# set the default Django settings module for the 'celery' program
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local')

app = Celery('core')

# using a string here means the worker doesn't have to serialize
# the configuration object to child processes
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix
app.config_from_object('django.conf:settings', namespace='CELERY')

# load task modules from all registered Django app configs.
app.autodiscover_tasks()

# define the the queue for each task
# the default 'celery' queue is exclusively used for the automated tests
task_routes = {
    'plugininstances.tasks.sum': {'queue': 'main1'},
    'plugininstances.tasks.run_plugin_instance': {'queue': 'main1'},
    'plugininstances.tasks.check_plugin_instance_exec_status': {'queue': 'main2'},
    'plugininstances.tasks.cancel_plugin_instance': {'queue': 'main2'},
    'plugininstances.tasks.schedule_waiting_plugin_instances':
        {'queue': 'periodic'},
    'plugininstances.tasks.check_started_plugin_instances_exec_status':
        {'queue': 'periodic'},
    'plugininstances.tasks.cancel_waiting_plugin_instances':
        {'queue': 'periodic'},
}
app.conf.update(task_routes=task_routes)

# setup periodic tasks
app.conf.beat_schedule = {
    'schedule-waiting-plugin-instances-every-45-seconds': {
        'task': 'plugininstances.tasks.schedule_waiting_plugin_instances',
        'schedule': 45.0,
    },
    'check-started-plugin-instances-exec-status-every-30-seconds': {
        'task': 'plugininstances.tasks.check_started_plugin_instances_exec_status',
        'schedule': 30.0,
    },
    'cancel-waiting-plugin-instances-every-30-seconds': {
        'task': 'plugininstances.tasks.cancel_waiting_plugin_instances',
        'schedule': 30.0,
    },
}

# use logging settings in Django settings
@setup_logging.connect
def config_loggers(*args, **kwags):
    dictConfig(settings.LOGGING)

# example task that is passed info about itself
@app.task(bind=True)
def debug_task(self):
    print('Request: {0!r}'.format(self.request))


# chris_backend/core/middleware.py


from django.http import HttpResponse
from rest_framework import status
from rest_framework.renderers import JSONRenderer

from collectionjson.renderers import CollectionJsonRenderer


class RenderedResponse(HttpResponse):
    """
    An HttpResponse that renders its content into Collection+JSON or JSON.
    """
    def __init__(self, data, **kwargs):
        request = data.pop('request')
        mime = request.META.get('HTTP_ACCEPT')
        if mime == 'application/json':
            kwargs['content_type'] = 'application/json'
            data['error'] = data.pop('detail')
            renderer = JSONRenderer()
            content = renderer.render(data)
        else:
            kwargs['content_type'] = 'application/vnd.collection+json'
            self.exception = True
            renderer_context = {'request': request, 'view': None, 'response': self}
            renderer = CollectionJsonRenderer()
            content = renderer.render(data, renderer_context=renderer_context)
        super(RenderedResponse, self).__init__(content, **kwargs)


def api_500(request):
    return RenderedResponse({'detail': 'Internal server error', 'request': request},
                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class ResponseMiddleware(object):

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_exception(self, request, exception):
        print(exception)
        mime = request.META.get('HTTP_ACCEPT')
        if mime != 'text/html':
            return api_500(request)


# chris_backend/core/models.py


import uuid

from django.db import models


class ChrisInstance(models.Model):
    """
    Model class that defines a singleton representing a ChRIS instance.
    """
    creation_date = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=100, default="ChRIS instance")
    uuid = models.UUIDField(default=uuid.uuid4)
    job_id_prefix = models.CharField(max_length=100, blank=True, default='chris-jid-')
    description = models.CharField(max_length=600, blank=True)

    class Meta:
        verbose_name = 'ChRIS instance'
        verbose_name_plural = 'ChRIS instance'

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        count = ChrisInstance.objects.all().count()
        if count > 0:
            self.id = 1
        super().save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        pass

    @classmethod
    def load(cls):
        try:
            obj = cls.objects.get(id=1)
        except cls.DoesNotExist:
            obj = cls()
            obj.save()
        return obj


# chris_backend/core/renderers.py


from rest_framework.renderers import BaseRenderer


class BinaryFileRenderer(BaseRenderer):
    media_type = '*/*'
    charset = None
    render_style = 'binary'

    def render(self, data, media_type=None, renderer_context=None):
        return data    


# chris_backend/core/serializers.py


from rest_framework import serializers

from .models import ChrisInstance


class ChrisInstanceSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = ChrisInstance
        fields = ('url', 'id', 'creation_date', 'name', 'uuid', 'job_id_prefix',
                  'description')


# chris_backend/core/swiftmanager.py

"""
Swift storage manager module.
"""

import logging
import os
import time

from swiftclient import Connection
from swiftclient.exceptions import ClientException


logger = logging.getLogger(__name__)


class SwiftManager(object):

    def __init__(self, container_name, conn_params):
        self.container_name = container_name
        # swift storage connection parameters dictionary
        self.conn_params = conn_params
        # swift storage connection object
        self._conn = None

    def get_connection(self):
        """
        Connect to swift storage and return the connection object.
        """
        if self._conn is not None:
            return self._conn
        for i in range(5):  # 5 retries at most
            try:
                self._conn = Connection(**self.conn_params)
            except ClientException as e:
                logger.error(str(e))
                if i == 4:
                    raise  # give up
                time.sleep(0.4)
            else:
                return self._conn

    def create_container(self):
        """
        Create the storage container.
        """
        conn = self.get_connection()
        try:
            conn.put_container(self.container_name)
        except ClientException as e:
            logger.error(str(e))
            raise

    def ls(self, path, **kwargs):
        """
        Return a list of objects in the swift storage with the provided path
        as a prefix.
        """
        b_full_listing = kwargs.get('full_listing', True)
        l_ls = []  # listing of names to return
        if path:
            conn = self.get_connection()
            for i in range(5):
                try:
                    # get the full list of objects in Swift storage with given prefix
                    ld_obj = conn.get_container(self.container_name,
                                                prefix=path,
                                                full_listing=b_full_listing)[1]
                except ClientException as e:
                    logger.error(str(e))
                    if i == 4:
                        raise
                    time.sleep(0.4)
                else:
                    l_ls = [d_obj['name'] for d_obj in ld_obj]
                    break
        return l_ls

    def path_exists(self, path):
        """
        Return True/False if passed path exists in swift storage.
        """
        return len(self.ls(path, full_listing=False)) > 0

    def obj_exists(self, obj_path):
        """
        Return True/False if passed object exists in swift storage.
        """
        conn = self.get_connection()
        for i in range(5):
            try:
                conn.head_object(self.container_name, obj_path)
            except ClientException as e:
                if e.http_status == 404:
                    return False
                else:
                    logger.error(str(e))
                    if i == 4:
                        raise
                    time.sleep(0.4)
            else:
                return True

    def upload_obj(self, swift_path, contents, **kwargs):
        """
        Upload an object (a file contents) into swift storage.
        """
        conn = self.get_connection()
        for i in range(5):
            try:
                conn.put_object(self.container_name,
                                swift_path,
                                contents=contents,
                                **kwargs)
            except ClientException as e:
                logger.error(str(e))
                if i == 4:
                    raise
                time.sleep(0.4)
            else:
                break

    def download_obj(self, obj_path, **kwargs):
        """
        Download an object from swift storage.
        """
        conn = self.get_connection()
        for i in range(5):
            try:
                resp_headers, obj_contents = conn.get_object(self.container_name,
                                                             obj_path, **kwargs)
            except ClientException as e:
                logger.error(str(e))
                if i == 4:
                    raise
                time.sleep(0.4)
            else:
                return obj_contents

    def copy_obj(self, obj_path, dest_path, **kwargs):
        """
        Copy an object to a new destination in swift storage.
        """
        conn = self.get_connection()
        dest = os.path.join('/' + self.container_name, dest_path.lstrip('/'))
        for i in range(5):
            try:
                conn.copy_object(self.container_name, obj_path, dest, **kwargs)
            except ClientException as e:
                logger.error(str(e))
                if i == 4:
                    raise
                time.sleep(0.4)
            else:
                break

    def delete_obj(self, obj_path):
        """
        Delete an object from swift storage.
        """
        conn = self.get_connection()
        for i in range(5):
            try:
                conn.delete_object(self.container_name, obj_path)
            except ClientException as e:
                logger.error(str(e))
                if i == 4:
                    raise
                time.sleep(0.4)
            else:
                break

    def upload_files(self, local_dir, swift_prefix='', **kwargs):
        """
        Upload all the files within a local directory recursively to swift storage.

        By default, the location in swift storage will map 1:1 to the location of
        files in the local filesytem. This location can be remapped by using the
        <swift_prefix>. For example, assume a local directory /home/user/project/data/
        with the following files:

            '/home/user/project/data/file1',
            '/home/user/project/data/dir1/file_d1',
            '/home/user/project/data/dir2/file_d2'

        and we want to upload everything in that directory to object storage, at location
        '/storage'. In this case, swift_prefix='/storage' results in a new list

            '/storage/file1',
            '/storage/dir1/file_d1',
            '/storage/dir2/file_d2'
        """
        # upload all files down the <local_dir>
        for root, dirs, files in os.walk(local_dir):
            swift_base = root.replace(local_dir, swift_prefix, 1) if swift_prefix else root
            for filename in files:
                swift_path = os.path.join(swift_base, filename)
                if not self.obj_exists(swift_path):
                    local_file_path = os.path.join(root, filename)
                    with open(local_file_path, 'rb') as f:
                        self.upload_obj(swift_path, f.read(), **kwargs)


# chris_backend/core/utils.py


import os
import json
import zlib, base64


def get_file_resource_link(file_serializer, obj):
    """
    Utility function to get the hyperlink to the actual file resource from a
    file serializer.
    """
    fields = file_serializer.fields.items()
    # get the current url
    url_field = [v for (k, v) in fields if k == 'url'][0]
    view = url_field.view_name
    request = file_serializer.context['request']
    format = file_serializer.context['format']
    url = url_field.get_url(obj, view, request, format)
    # return url = current url + file name
    return url + os.path.basename(obj.fname.name)


def json_zip2str(json_data):
    """
    Return a string of compressed JSON data, suitable for transmission
    back to a client.
    """
    return base64.b64encode(
        zlib.compress(
            json.dumps(json_data).encode('utf-8')
        )
    ).decode('ascii')


def filter_files_by_n_slashes(queryset, value):
    """
    Utility function to return the files that have the queried number of slashes in
    their fname property. If the queried number ends in 'u' or 'U' then only one
    file per each last "folder" in the path is returned (useful to efficiently get
    the list of immediate folders under the path).
    """
    value = value.lower()
    try:
        if value.endswith('u'):
            val = int(value[:-1])
        else:
            val = int(value)
    except Exception:
        return queryset
    lookup = r'^[^/]+'
    for i in range(val):
        lookup += '/[^/]+'
    lookup += '$'
    qs = queryset.filter(fname__regex=lookup)
    if value.endswith('u'):
        return unique_files_queryset_by_folder(qs)
    return qs


def unique_files_queryset_by_folder(queryset):
    """
    Utility function to return only one file per each last "folder" in the path
    (useful to efficiently get the list of immediate folders under the path).
    """
    ids = []
    hash_set = set()
    for f in queryset.all():
        path = f.fname.name
        last_slash_ix = path.rindex('/')
        path = path[:last_slash_ix]
        if path not in hash_set:
            ids.append(f.id)
            hash_set.add(path)
    return queryset.filter(pk__in=ids)


# chris_backend/core/views.py


from rest_framework import generics, permissions

from .models import ChrisInstance
from .serializers import ChrisInstanceSerializer


class ChrisInstanceDetail(generics.RetrieveAPIView):
    """
    A compute resource view.
    """
    http_method_names = ['get']
    serializer_class = ChrisInstanceSerializer
    queryset = ChrisInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def get_object(self):
        """
        Overriden to return the ChrisInstance singleton.
        """
        return ChrisInstance.load()


# chris_backend/feeds/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/feeds/apps.py

from django.apps import AppConfig


class FeedsConfig(AppConfig):
    name = 'feeds'


# chris_backend/feeds/models.py


from django.db import models

import django_filters
from django_filters.rest_framework import FilterSet


class Feed(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    modification_date = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=200, blank=True, db_index=True)
    owner = models.ManyToManyField('auth.User', related_name='feed')

    class Meta:
        ordering = ('-creation_date',)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        """
        Overriden to save a new note to the DB the first time the feed is saved.
        """
        super(Feed, self).save(*args, **kwargs)
        if not hasattr(self, 'note'):
            self._save_note()

    def _save_note(self):
        """
        Custom method to create and save a new note to the DB.
        """
        note = Note()
        note.feed = self
        note.save()

    def get_creator(self):
        """
        Custom method to get the user that created the feed.
        """
        plg_inst = self.plugin_instances.filter(plugin__meta__type='fs')[0]
        return plg_inst.owner

    def get_plugin_instances_status_count(self, status):
        """
        Custom method to get the number of associated plugin instances with a given
        execution status.
        """
        return self.plugin_instances.filter(status=status).count()


class FeedFilter(FilterSet):
    min_id = django_filters.NumberFilter(field_name="id", lookup_expr='gte')
    max_id = django_filters.NumberFilter(field_name="id", lookup_expr='lte')
    min_creation_date = django_filters.IsoDateTimeFilter(field_name="creation_date",
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name="creation_date",
                                                         lookup_expr='lte')
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    name_exact = django_filters.CharFilter(field_name='name', lookup_expr='exact')
    name_startswith = django_filters.CharFilter(field_name='name',
                                                lookup_expr='startswith')

    class Meta:
        model = Feed
        fields = ['id', 'name', 'name_exact', 'name_startswith', 'min_id', 'max_id',
                  'min_creation_date', 'max_creation_date']


class Note(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    modification_date = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True)
    content = models.TextField(blank=True)
    feed = models.OneToOneField(Feed, on_delete=models.CASCADE, related_name='note')

    def __str__(self):
        return self.title


class Tag(models.Model):
    name = models.CharField(max_length=100, blank=True)
    color = models.CharField(max_length=20)
    feeds = models.ManyToManyField(Feed, related_name='tags',
                                   through='Tagging')
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    def __str__(self):
        return self.name


class TagFilter(FilterSet):
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    color = django_filters.CharFilter(field_name='color', lookup_expr='icontains')
    owner_username = django_filters.CharFilter(field_name='owner__username',
                                               lookup_expr='exact')

    class Meta:
        model = Tag
        fields = ['id', 'name', 'color', 'owner_username']


class Tagging(models.Model):
    feed = models.ForeignKey(Feed, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)

    class Meta:
        unique_together = ('feed', 'tag',)

    def __str__(self):
        return str(self.id)


class Comment(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True)
    content = models.TextField(blank=True)
    feed = models.ForeignKey(Feed, on_delete=models.CASCADE, related_name='comments')
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    class Meta:
        ordering = ('-creation_date',)

    def __str__(self):
        return self.title


class CommentFilter(FilterSet):

    class Meta:
        model = Comment
        fields = ['id']


# chris_backend/feeds/permissions.py


from rest_framework import permissions


class IsOwnerOrChris(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read and write permissions are only allowed to
        # the owner and superuser 'chris'.
        if hasattr(obj.owner, 'all'):
            return (request.user in obj.owner.all()) or (request.user.username == 'chris')
        return (obj.owner == request.user) or (request.user.username == 'chris')
    

class IsOwnerOrChrisOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it. Read only is allowed to other users.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner and superuser 'chris'.
        if hasattr(obj.owner, 'all'):
            return (request.user in obj.owner.all()) or (request.user.username == 'chris')
        return (obj.owner == request.user) or (request.user.username == 'chris')


class IsRelatedFeedOwnerOrChris(permissions.BasePermission):
    """
    Custom permission to only allow owners of a feed associated to an object or superuser
    'chris' to modify/edit the object (eg. a note).
    """

    def has_object_permission(self, request, view, obj):
        # Read and write permissions are only allowed to
        # the owner and superuser 'chris'.
        if request.user.username == 'chris':
            return True
        return request.user in obj.feed.owner.all()


class IsRelatedTagOwnerOrChris(permissions.BasePermission):
    """
    Custom permission to only allow the owner of a tag associated to an object or
    superuser 'chris' to access the object (eg. a tagging).
    """

    def has_object_permission(self, request, view, obj):
        # Read and write permissions are only allowed to
        # the owner and superuser 'chris'.
        if (request.user.username == 'chris') or (request.user == obj.tag.owner):
            return True
        return False


# chris_backend/feeds/serializers.py


from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import IntegrityError
from rest_framework import serializers

from plugininstances.models import STATUS_CHOICES
from .models import Note, Feed, Tag, Tagging, Comment


class NoteSerializer(serializers.HyperlinkedModelSerializer):
    feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True)

    class Meta:
        model = Note
        fields = ('url', 'id', 'title', 'content', 'feed')


class TagSerializer(serializers.HyperlinkedModelSerializer):
    owner_username = serializers.ReadOnlyField(source='owner.username')
    feeds = serializers.HyperlinkedIdentityField(view_name='tag-feed-list')
    taggings = serializers.HyperlinkedIdentityField(view_name='tag-tagging-list')

    class Meta:
        model = Tag
        fields = ('url', 'id', 'name', 'owner_username', 'color', 'feeds', 'taggings')


class TaggingSerializer(serializers.HyperlinkedModelSerializer):
    owner_username = serializers.ReadOnlyField(source='tag.owner.username')
    tag_id = serializers.ReadOnlyField(source='tag.id')
    feed_id = serializers.ReadOnlyField(source='feed.id')
    feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True)
    tag = serializers.HyperlinkedRelatedField(view_name='tag-detail', read_only=True)

    class Meta:
        model = Tagging
        fields = ('url', 'id', 'owner_username', 'tag_id', 'feed_id', 'tag', 'feed')

    def create(self, validated_data):
        """
        Overriden to handle the error when trying to create an already existing tagging.
        """
        try:
            return super(TaggingSerializer, self).create(validated_data)
        except IntegrityError:
            feed = validated_data['feed']
            tag = validated_data['tag']
            raise serializers.ValidationError(
                {'non_field_errors':
                     ["Tagging for feed_id %s and tag_id %s already exists." %
                      (feed.id, tag.id)]})

    def validate_tag(self, tag_id):
        """
        Custom method to check that a tag id is provided, exists in the DB and
        owned by the user.
        """
        if not tag_id:
            raise serializers.ValidationError({'tag_id': ["A tag id is required."]})
        try:
            pk = int(tag_id)
            tag = Tag.objects.get(pk=pk)
        except (ValueError, ObjectDoesNotExist):
            raise serializers.ValidationError(
                {'tag_id': ["Couldn't find any tag with id %s." % tag_id]})
        user = self.context['request'].user
        if tag.owner != user:
            raise serializers.ValidationError(
                {'tag_id': ["User is not the owner of tag with tag_id %s." % tag_id]})
        return tag

    def validate_feed(self, feed_id):
        """
        Custom method to check that a feed id is provided, exists in the DB and
        owned by the user.
        """
        if not feed_id:
            raise serializers.ValidationError({'feed_id': ["A feed id is required."]})
        try:
            pk = int(feed_id)
            feed = Feed.objects.get(pk=pk)
        except (ValueError, ObjectDoesNotExist):
            raise serializers.ValidationError(
                {'feed_id': ["Couldn't find any feed with id %s." % feed_id]})
        user = self.context['request'].user
        if user not in feed.owner.all():
            raise serializers.ValidationError(
                {'feed_id': ["User is not the owner of feed with feed_id %s." % feed_id]})
        return feed


class FeedSerializer(serializers.HyperlinkedModelSerializer):
    creator_username = serializers.SerializerMethodField()
    created_jobs = serializers.SerializerMethodField()
    waiting_jobs = serializers.SerializerMethodField()
    scheduled_jobs = serializers.SerializerMethodField()
    started_jobs = serializers.SerializerMethodField()
    registering_jobs = serializers.SerializerMethodField()
    finished_jobs = serializers.SerializerMethodField()
    errored_jobs = serializers.SerializerMethodField()
    cancelled_jobs = serializers.SerializerMethodField()
    note = serializers.HyperlinkedRelatedField(view_name='note-detail', read_only=True)
    tags = serializers.HyperlinkedIdentityField(view_name='feed-tag-list')
    taggings = serializers.HyperlinkedIdentityField(view_name='feed-tagging-list')
    comments = serializers.HyperlinkedIdentityField(view_name='comment-list')
    files = serializers.HyperlinkedIdentityField(view_name='feedfile-list')
    plugin_instances = serializers.HyperlinkedIdentityField(
        view_name='feed-plugininstance-list')
    owner = serializers.HyperlinkedRelatedField(many=True, view_name='user-detail',
                                                read_only=True)

    class Meta:
        model = Feed
        fields = ('url', 'id', 'creation_date', 'modification_date', 'name',
                  'creator_username', 'created_jobs', 'waiting_jobs', 'scheduled_jobs',
                  'started_jobs', 'registering_jobs', 'finished_jobs', 'errored_jobs',
                  'cancelled_jobs', 'owner', 'note', 'tags', 'taggings', 'comments',
                  'files', 'plugin_instances')

    def validate_name(self, name):
        """
        Overriden to check that the feed's name does not contain forward slashes and
        is not the special 'uploads' identifier.
        """
        if '/' in name:
            raise serializers.ValidationError(
                ["This field may not contain forward slashes."])
        if name == 'uploads':
            raise serializers.ValidationError(
                ["Forbidden feed name 'uploads'."])
        return name

    def validate_new_owner(self, username):
        """
        Custom method to check whether a new feed owner is a system-registered user.
        """
        try:
            # check if user is a system-registered user
            new_owner = User.objects.get(username=username)
        except ObjectDoesNotExist:
            raise serializers.ValidationError(
                {'owner': ["User %s is not a registered user." % username]})
        return new_owner

    def get_creator_username(self, obj):
        """
        Overriden to get the username of the creator of the feed.
        """
        return obj.get_creator().username

    def get_created_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'created' status.
        """
        if 'created' not in [status[0] for status in STATUS_CHOICES]:
            raise KeyError("Undefined plugin instance execution status: 'created'.")
        return obj.get_plugin_instances_status_count('created')

    def get_waiting_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'waiting' status.
        """
        if 'waiting' not in [status[0] for status in STATUS_CHOICES]:
            msg = "Undefined plugin instance execution status: 'waiting'."
            raise KeyError(msg)
        return obj.get_plugin_instances_status_count('waiting')

    def get_scheduled_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'scheduled' status.
        """
        if 'scheduled' not in [status[0] for status in STATUS_CHOICES]:
            raise KeyError("Undefined plugin instance execution status: 'scheduled'.")
        return obj.get_plugin_instances_status_count('scheduled')

    def get_started_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'started' status.
        """
        if 'started' not in [status[0] for status in STATUS_CHOICES]:
            raise KeyError("Undefined plugin instance execution status: 'started'.")
        return obj.get_plugin_instances_status_count('started')

    def get_registering_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'registeringFiles' status.
        """
        if 'registeringFiles' not in [status[0] for status in STATUS_CHOICES]:
            msg = "Undefined plugin instance execution status: 'registeringFiles'."
            raise KeyError(msg)
        return obj.get_plugin_instances_status_count('registeringFiles')

    def get_finished_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'finishedSuccessfully' status.
        """
        if 'finishedSuccessfully' not in [status[0] for status in STATUS_CHOICES]:
            raise KeyError("Undefined plugin instance execution status: "
                           "'finishedSuccessfully'.")
        return obj.get_plugin_instances_status_count('finishedSuccessfully')

    def get_errored_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'finishedWithError' status.
        """
        if 'finishedWithError' not in [status[0] for status in STATUS_CHOICES]:
            raise KeyError("Undefined plugin instance execution status: "
                           "'finishedWithError'.")
        return obj.get_plugin_instances_status_count('finishedWithError')

    def get_cancelled_jobs(self, obj):
        """
        Overriden to get the number of plugin instances in 'cancelled' status.
        """
        if 'cancelled' not in [status[0] for status in STATUS_CHOICES]:
            raise KeyError("Undefined plugin instance execution status: 'cancelled'.")
        return obj.get_plugin_instances_status_count('cancelled')


class CommentSerializer(serializers.HyperlinkedModelSerializer):
    owner_username = serializers.ReadOnlyField(source='owner.username')
    feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True)

    class Meta:
        model = Comment
        fields = ('url', 'id', 'title', 'owner_username', 'content', 'feed')


# chris_backend/feeds/views.py


from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework.reverse import reverse

from collectionjson import services
from plugininstances.models import PluginInstance, PluginInstanceFile
from plugininstances.serializers import PluginInstanceSerializer
from plugininstances.serializers import PluginInstanceFileSerializer

from .models import Feed, FeedFilter
from .models import Tag, TagFilter
from .models import Comment, CommentFilter
from .models import Note, Tagging
from .serializers import FeedSerializer, NoteSerializer
from .serializers import TagSerializer, TaggingSerializer, CommentSerializer
from .permissions import IsOwnerOrChris, IsOwnerOrChrisOrReadOnly
from .permissions import IsRelatedFeedOwnerOrChris, IsRelatedTagOwnerOrChris


class NoteDetail(generics.RetrieveUpdateAPIView):
    """
    A note view.
    """
    http_method_names = ['get', 'put']
    queryset = Note.objects.all()
    serializer_class = NoteSerializer
    permission_classes = (permissions.IsAuthenticated, IsRelatedFeedOwnerOrChris)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(NoteDetail, self).retrieve(request, *args, **kwargs)
        template_data = {"title": "", "content": ""}
        return services.append_collection_template(response, template_data)


class TagList(generics.ListCreateAPIView):
    """
    A view for the collection of user-specific tags.
    """
    http_method_names = ['get', 'post']
    serializer_class = TagSerializer
    permission_classes = (permissions.IsAuthenticated, )

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the tags 
        owned by the currently authenticated user.
        """
        user = self.request.user
        # if the user is chris then return all the tags in the system
        if user.username == 'chris':
            return Tag.objects.all()
        return Tag.objects.filter(owner=user)

    def perform_create(self, serializer):
        """
        Overriden to associate an owner with the tag before first saving to the DB.
        """
        serializer.save(owner=self.request.user)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a collection+json template.
        """
        response = super(TagList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('tag-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'feeds': reverse('feed-list', request=request)}
        response = services.append_collection_links(response, links)
        # append write template
        template_data = {"name": "", "color": ""}
        return services.append_collection_template(response, template_data)


class TagListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of tags resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = TagSerializer
    queryset = Tag.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = TagFilter


class TagDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    A tag view.
    """
    http_method_names = ['get', 'put', 'delete']
    queryset = Tag.objects.all()
    serializer_class = TagSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(TagDetail, self).retrieve(request, *args, **kwargs)
        template_data = {"name": "", "color": ""}
        return services.append_collection_template(response, template_data)


class FeedTagList(generics.ListAPIView):
    """
    A view for a feed-specific collection of user-specific tags.
    """
    http_method_names = ['get']
    queryset = Feed.objects.all()
    serializer_class = TagSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the tags for the queried feed that are
        owned by the currently authenticated user. Document-level link relations are
        also added to the response.
        """
        queryset = self.get_tags_queryset(request.user)
        response = services.get_list_response(self, queryset)
        feed = self.get_object()
        links = {'feed': reverse('feed-detail', request=request,
                                   kwargs={"pk": feed.id})}
        return services.append_collection_links(response, links)

    def get_tags_queryset(self, user):
        """
        Custom method to get the actual tags queryset for the feed and user.
        """
        feed = self.get_object()
        return feed.tags.filter(owner=user)


class TagFeedList(generics.ListAPIView):
    """
    A view for a tag-specific collection of feeds.
    """
    http_method_names = ['get']
    queryset = Tag.objects.all()
    serializer_class = FeedSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the feeds for the queried tag.
        Document-level link relations are also added to the response.
        """
        queryset = self.get_feeds_queryset()
        response = services.get_list_response(self, queryset)
        tag = self.get_object()
        links = {'tag': reverse('tag-detail', request=request,
                                   kwargs={"pk": tag.id})}
        return services.append_collection_links(response, links)

    def get_feeds_queryset(self):
        """
        Custom method to get the actual feeds queryset for the tag.
        """
        tag = self.get_object()
        return tag.feeds.all()


class FeedTaggingList(generics.ListCreateAPIView):
    """
    A view for the collection of feed-specific taggings.
    """
    http_method_names = ['get', 'post']
    queryset = Feed.objects.all()
    serializer_class = TaggingSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def perform_create(self, serializer):
        """
        Overriden to associate a tag and feed with the tagging before first
        saving to the DB.
        """
        request_data = serializer.context['request'].data
        tag_id = ""
        if 'tag_id' in request_data:
            tag_id = request_data['tag_id']
        tag = serializer.validate_tag(tag_id)
        serializer.save(tag=tag, feed=self.get_object())

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the taggings for the queried feed.
        Document-level link relations and a collection+json template are also
        added to the response.
        """
        queryset = self.get_taggings_queryset(request.user)
        response = services.get_list_response(self, queryset)
        feed = self.get_object()
        links = {'feed': reverse('feed-detail', request=request,
                                   kwargs={"pk": feed.id})}
        response = services.append_collection_links(response, links)
        template_data = {"tag_id": ""}
        return services.append_collection_template(response, template_data)

    def get_taggings_queryset(self, user):
        """
        Custom method to get the actual taggings queryset for the feed.
        """
        feed = self.get_object()
        return Tagging.objects.filter(feed=feed, tag__owner=user)


class TagTaggingList(generics.ListCreateAPIView):
    """
    A view for the collection of tag-specific taggings.
    """
    http_method_names = ['get', 'post']
    queryset = Tag.objects.all()
    serializer_class = TaggingSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def perform_create(self, serializer):
        """
        Overriden to associate a tag and feed with the tagging before first
        saving to the DB.
        """
        request_data = serializer.context['request'].data
        feed_id = ""
        if 'feed_id' in request_data:
            feed_id = request_data['feed_id']
        feed = serializer.validate_feed(feed_id)
        serializer.save(tag=self.get_object(), feed=feed)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the taggings for the queried tag.
        Document-level link relations and a collection+json template are also
        added to the response.
        """
        queryset = self.get_taggings_queryset()
        response = services.get_list_response(self, queryset)
        tag = self.get_object()
        links = {'tag': reverse('tag-detail', request=request,
                                   kwargs={"pk": tag.id})}
        response = services.append_collection_links(response, links)
        template_data = {"feed_id": ""}
        return services.append_collection_template(response, template_data)

    def get_taggings_queryset(self):
        """
        Custom method to get the actual taggings queryset for the tag.
        """
        tag = self.get_object()
        return Tagging.objects.filter(tag=tag)


class TaggingDetail(generics.RetrieveDestroyAPIView):
    """
    A tagging view.
    """
    http_method_names = ['get', 'delete']
    queryset = Tagging.objects.all()
    serializer_class = TaggingSerializer
    permission_classes = (permissions.IsAuthenticated, IsRelatedTagOwnerOrChris)


class FeedList(generics.ListAPIView):
    """
    A view for the collection of feeds.
    """
    http_method_names = ['get']
    serializer_class = FeedSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the feeds 
        owned by the currently authenticated user.
        """
        user = self.request.user
        # if the user is chris then return all the feeds in the system
        if user.username == 'chris':
            return Feed.objects.all()
        return Feed.objects.filter(owner=user)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations.
        """
        response = super(FeedList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('feed-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        user = self.request.user
        links = {'chrisinstance': reverse('chrisinstance-detail', request=request,
                                          kwargs={"pk": 1}),
                 'admin': reverse('admin-plugin-list', request=request),
                 'files': reverse('allplugininstancefile-list', request=request),
                 'compute_resources': reverse('computeresource-list', request=request),
                 'plugin_metas': reverse('pluginmeta-list', request=request),
                 'plugins': reverse('plugin-list', request=request),
                 'plugin_instances': reverse('allplugininstance-list', request=request),
                 'pipelines': reverse('pipeline-list', request=request),
                 'pipeline_instances': reverse('allpipelineinstance-list',
                                               request=request),
                 'tags': reverse('tag-list', request=request),
                 'uploadedfiles': reverse('uploadedfile-list', request=request),
                 'pacsfiles': reverse('pacsfile-list', request=request),
                 'servicefiles': reverse('servicefile-list', request=request),
                 'filebrowser': reverse('filebrowserpath-list', request=request),
                 'user': reverse('user-detail', request=request, kwargs={"pk": user.id})}
        return services.append_collection_links(response, links)


class FeedListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of feeds resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = FeedSerializer
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = FeedFilter

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the feeds 
        owned by the currently authenticated user.
        """
        user = self.request.user
        # if the user is chris then return all the feeds in the system
        if user.username == 'chris':
            return Feed.objects.all()
        return Feed.objects.filter(owner=user)


class FeedDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    A feed view.
    """
    http_method_names = ['get', 'put', 'delete']
    queryset = Feed.objects.all()
    serializer_class = FeedSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris,)

    def perform_update(self, serializer):
        """
        Overriden to update feed's owners if requested by a PUT request.
        """
        if 'owner' in self.request.data:
            self.update_owners(serializer)
        super(FeedDetail, self).perform_update(serializer)
        
    def update_owners(self, serializer):
        """
        Custom method to update the feed's owners.
        """
        feed = self.get_object()
        owners = feed.owner.values('username')
        username = self.request.data.pop('owner')
        if {'username': username} not in owners:
            new_owner = serializer.validate_new_owner(username)
            owners = [o for o in feed.owner.all()]
            owners.append(new_owner)
            serializer.save(owner=owners)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(FeedDetail, self).retrieve(request, *args, **kwargs)
        template_data = {"name": "", "owner": ""}
        return services.append_collection_template(response, template_data)


class CommentList(generics.ListCreateAPIView):
    """
    A view for the collection of comments.
    """
    http_method_names = ['get', 'post']
    queryset = Feed.objects.all()
    serializer_class = CommentSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChrisOrReadOnly)

    def perform_create(self, serializer):
        """
        Overriden to associate an owner and feed with the comment
        before first saving to the DB.
        """
        serializer.save(owner=self.request.user, feed=self.get_object())

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the comments for the queried feed.
        A collection+json write template and document-level link relation are also
        added to the response.
        """
        queryset = self.get_comments_queryset()
        response = services.get_list_response(self, queryset)
        feed = self.get_object()
        # append query list
        query_list = [reverse('comment-list-query-search', request=request,
                              kwargs={"pk": feed.id})]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'feed': reverse('feed-detail', request=request,
                                   kwargs={"pk": feed.id})}
        response = services.append_collection_links(response, links)
        # append write template
        template_data = {"title": "", "content": ""}
        return services.append_collection_template(response, template_data)

    def get_comments_queryset(self):
        """
        Custom method to get the actual comments' queryset.
        """
        feed = self.get_object()
        return self.filter_queryset(feed.comments.all())


class CommentListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of feed-specific comments resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = CommentSerializer
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = CommentFilter

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is comprised by the feed-specific
        comments.
        """
        feed = get_object_or_404(Feed, pk=self.kwargs['pk'])
        return feed.comments.all()


class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    A comment view.
    """
    http_method_names = ['get', 'put', 'delete']
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChrisOrReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(CommentDetail, self).retrieve(request, *args, **kwargs)
        template_data = {"title": "", "content": ""}
        return services.append_collection_template(response, template_data)


class FeedFileList(generics.ListAPIView):
    """
    A view for the collection of all files in a feed.
    """
    http_method_names = ['get']
    queryset = Feed.objects.all()
    serializer_class = PluginInstanceFileSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the files for the queried feed and
        append document-level link relations.
        """
        queryset = self.get_feedfiles_queryset()
        response = services.get_list_response(self, queryset)
        feed = self.get_object()
        links = {'feed': reverse('feed-detail', request=request, kwargs={"pk": feed.id})}
        return services.append_collection_links(response, links)

    def get_feedfiles_queryset(self):
        """
        Custom method to get the actual feed files queryset.
        """
        feed = self.get_object()
        return PluginInstanceFile.objects.filter(plugin_inst__feed=feed)


class FeedPluginInstanceList(generics.ListAPIView):
    """
    A view for the collection of feed-specific plugin instances.
    """
    http_method_names = ['get']
    queryset = Feed.objects.all()
    serializer_class = PluginInstanceSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the plugin instances for the queried feed.
        """
        queryset = self.get_plugin_instances_queryset()
        response = services.get_list_response(self, queryset)
        feed = self.get_object()
        links = {'feed': reverse('feed-detail', request=request, kwargs={"pk": feed.id})}
        return services.append_collection_links(response, links)

    def get_plugin_instances_queryset(self):
        """
        Custom method to get the actual plugin instances queryset.
        """
        feed = self.get_object()
        return self.filter_queryset(feed.plugin_instances.all())


# chris_backend/filebrowser/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/filebrowser/apps.py

from django.apps import AppConfig


class FilebrowserConfig(AppConfig):
    name = 'filebrowser'


# chris_backend/filebrowser/models.py

from django.db import models

# Create your models here.


# chris_backend/filebrowser/serializers.py


from rest_framework import serializers
from rest_framework.reverse import reverse

from collectionjson.serializers import NoModelSerializer
from collectionjson.fields import ItemLinkField
from core.utils import get_file_resource_link


class FileBrowserPathListSerializer(NoModelSerializer):
    path = serializers.CharField(read_only=True)
    subfolders = serializers.CharField(read_only=True)
    url = serializers.SerializerMethodField()

    def get_url(self, obj):
        """
        Overriden to get the url of the path.
        """
        request = self.context['request']
        return reverse('filebrowserpath-list', request=request)


class FileBrowserPathSerializer(NoModelSerializer):
    path = serializers.CharField(read_only=True)
    subfolders = serializers.CharField(read_only=True)
    url = serializers.SerializerMethodField()
    files = ItemLinkField('get_files')

    def get_url(self, obj):
        """
        Overriden to get the url of the path.
        """
        request = self.context['request']
        path = self.context['view'].kwargs.get('path')
        return reverse('filebrowserpath', request=request, kwargs={"path": path})

    def get_files(self, obj):
        """
        Custom method to get the hyperlink to the list of files directly under the path.
        """
        request = self.context['request']
        path = self.context['view'].kwargs.get('path')
        return reverse('filebrowserpathfile-list', request=request, kwargs={"path": path})


class FileBrowserPathFileSerializer(serializers.HyperlinkedModelSerializer):
    fname = serializers.FileField(use_url=False)
    fsize = serializers.ReadOnlyField(source='fname.size')
    file_resource = ItemLinkField('get_file_link')

    class Meta:
        model = None
        fields = ('url', 'creation_date', 'fname', 'fsize', 'file_resource')

    def get_file_link(self, obj):
        """
        Custom method to get the hyperlink to the actual file resource.
        """
        return get_file_resource_link(self, obj)


# chris_backend/filebrowser/services.py


from servicefiles.models import ServiceFile
from servicefiles.serializers import ServiceFileSerializer
from pacsfiles.models import PACSFile
from pacsfiles.serializers import PACSFileSerializer
from uploadedfiles.models import UploadedFile
from uploadedfiles.serializers import UploadedFileSerializer
from plugininstances.models import PluginInstanceFile
from plugininstances.serializers import PluginInstanceFileSerializer


def get_path_file_model_class(path, username):
    """
    Convenience function to get the file model class associated to a path.
    """
    model_class = None
    if path.startswith('SERVICES/PACS'):
        model_class = PACSFile
    elif path.startswith('SERVICES'):
        model_class = ServiceFile
    elif path.startswith(f'{username}/uploads'):
        model_class = UploadedFile
    elif path.startswith(f'{username}'):
        model_class = PluginInstanceFile
    return model_class


def get_path_file_serializer_class(path, username):
    """
    Convenience function to get the file serializer class associated to a path.
    """
    model_class = get_path_file_model_class(path, username)
    if model_class is None:
        return None
    serializers_map = {'ServiceFile': ServiceFileSerializer,
                       'PACSFile': PACSFileSerializer,
                       'UploadedFile': UploadedFileSerializer,
                       'PluginInstanceFile': PluginInstanceFileSerializer
                       }
    return serializers_map[model_class.__name__]


def get_path_file_queryset(path, username):
    """
    Convenience function to get the queryset associated to a path. Raises ValueError
    if the path is not found.
    """
    model_class = get_path_file_model_class(path, username)
    if model_class is None:
        raise ValueError('Path not found.')
    qs = model_class.objects.filter(fname__startswith=path)
    try:
        qs[0]
    except IndexError:
        if path not in ('SERVICES', 'SERVICES/PACS', f'{username}', f'{username}/uploads'):
            raise ValueError('Path not found.')
    return qs


def get_path_folders(path, username):
    """
    Convenience function to get the immediate subfolders under a path.
    """
    qs = get_path_file_queryset(path, username)
    hash_set = set()
    for obj in qs:
        name = obj.fname.name
        folder = name.replace(path + '/', '', 1)
        try:
            first_slash_ix = folder.index('/')
        except ValueError:
            pass  # no folders under this path (only files)
        else:
            folder = folder[:first_slash_ix]
            if folder not in hash_set:
                hash_set.add(folder)
    subfolders = list(hash_set)
    if path == 'SERVICES':
        subfolders.append('PACS')
    if path == f'{username}':
        subfolders.append('uploads')
    return subfolders


# chris_backend/filebrowser/views.py


from django.http import Http404
from rest_framework import generics, permissions
from rest_framework.reverse import reverse
from rest_framework.response import Response
from rest_framework.views import APIView

from core.utils import filter_files_by_n_slashes
from .serializers import (FileBrowserPathListSerializer, FileBrowserPathSerializer,
                          FileBrowserPathFileSerializer)
from .services import get_path_folders, get_path_file_queryset, get_path_file_model_class


class FileBrowserPathList(generics.ListAPIView):
    """
    A view for the initial page of the collection of file browser paths. The returned
    collection only has a single element.
    """
    http_method_names = ['get']
    serializer_class = FileBrowserPathListSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append a query list to the response.
        """
        response = super(FileBrowserPathList, self).list(request, *args, **kwargs)
        # append query list
        query_url = reverse('filebrowserpath-list-query-search', request=request)
        data = [{'name': 'path', 'value': ''}]
        queries = [{'href': query_url, 'rel': 'search', 'data': data}]
        response.data['queries'] = queries
        return response

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the initial
        path (empty path).
        """
        username = self.request.user.username
        objects = [{'path': '', 'subfolders': f'SERVICES,{username}'}]
        return self.filter_queryset(objects)


class FileBrowserPathListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of file browser paths resulting from a query search.
    The returned collection only has at most one element.
    """
    http_method_names = ['get']
    permission_classes = (permissions.IsAuthenticated,)

    def get_queryset(self):
        """
        Overriden to return a custom queryset.
        """
        username = self.request.user.username
        path = self.request.GET.get('path', '')
        if not path:
            objects = [{'path': '', 'subfolders': f'SERVICES,{username}'}]
        else:
            path = path.strip('/')
            try:
                subfolders = get_path_folders(path, username)
            except ValueError:
                objects = []
            else:
                objects = [{'path': path, 'subfolders': ','.join(subfolders)}]
        return self.filter_queryset(objects)

    def get_serializer_class(self, *args, **kwargs):
        """
        Overriden to return the serializer class that should be used for serializing
        output.
        """
        path = self.request.GET.get('path', '')
        if not path:
            return FileBrowserPathListSerializer
        self.kwargs['path'] = path.strip('/')
        return FileBrowserPathSerializer


class FileBrowserPath(APIView):
    """
    A file browser path view.
    """
    http_method_names = ['get']
    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, *args, **kwargs):
        """
        Overriden to be able to make a GET request to an actual file resource.
        """
        username = request.user.username
        path = kwargs.get('path')
        try:
            subfolders = get_path_folders(path, username)
        except ValueError:
            raise Http404('Not found.')
        object = {'path': path, 'subfolders': ','.join(subfolders)}
        serializer = self.get_serializer(object)
        return Response(serializer.data)

    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for serializing output.
        """
        kwargs.setdefault('context', self.get_serializer_context())
        return FileBrowserPathSerializer(*args, **kwargs)

    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {'request': self.request, 'view': self}


class FileBrowserPathFileList(generics.ListAPIView):
    """
    A view for the collection of a file browser path's files.
    """
    http_method_names = ['get']
    permission_classes = (permissions.IsAuthenticated, )

    def get_queryset(self):
        """
        Overriden to return a custom queryset.
        """
        username = self.request.user.username
        path = self.kwargs.get('path')
        try:
            qs = get_path_file_queryset(path, username)
        except ValueError:
            raise Http404('Not found.')
        n_slashes = path.count('/') + 1
        return filter_files_by_n_slashes(qs, str(n_slashes))

    def get_serializer_class(self):
        """
        Overriden to return the serializer class that should be used for serializing
        output.
        """
        username = self.request.user.username
        path = self.kwargs.get('path')
        model_class = get_path_file_model_class(path, username)
        FileBrowserPathFileSerializer.Meta.model = model_class
        return FileBrowserPathFileSerializer


# chris_backend/pacsfiles/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/pacsfiles/apps.py

from django.apps import AppConfig


class PacsfilesConfig(AppConfig):
    name = 'pacsfiles'


# chris_backend/pacsfiles/models.py


from django.db import models
import django_filters
from django_filters.rest_framework import FilterSet

from core.utils import filter_files_by_n_slashes


class PACS(models.Model):
    identifier = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return self.identifier


class PACSFile(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    fname = models.FileField(max_length=512, unique=True)
    PatientID = models.CharField(max_length=100, db_index=True)
    PatientName = models.CharField(max_length=150, blank=True)
    PatientBirthDate = models.DateField(blank=True, null=True)
    PatientAge = models.IntegerField(blank=True, null=True)
    PatientSex = models.CharField(max_length=1, choices=[('M', 'Male'), ('F', 'Female')],
                                  blank=True)
    StudyDate = models.DateField(db_index=True)
    AccessionNumber = models.CharField(max_length=100, blank=True, db_index=True)
    Modality = models.CharField(max_length=15, blank=True)
    ProtocolName = models.CharField(max_length=64, blank=True)
    StudyInstanceUID = models.CharField(max_length=100)
    StudyDescription = models.CharField(max_length=400, blank=True)
    SeriesInstanceUID = models.CharField(max_length=100)
    SeriesDescription = models.CharField(max_length=400, blank=True)
    pacs = models.ForeignKey(PACS, on_delete=models.CASCADE)

    class Meta:
        ordering = ('-fname',)

    def __str__(self):
        return self.fname.name


class PACSFileFilter(FilterSet):
    min_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='lte')
    fname = django_filters.CharFilter(field_name='fname', lookup_expr='startswith')
    fname_exact = django_filters.CharFilter(field_name='fname', lookup_expr='exact')
    fname_icontains = django_filters.CharFilter(field_name='fname',
                                                lookup_expr='icontains')
    fname_nslashes = django_filters.CharFilter(method='filter_by_n_slashes')
    PatientName = django_filters.CharFilter(field_name='PatientName',
                                            lookup_expr='icontains')
    ProtocolName = django_filters.CharFilter(field_name='ProtocolName',
                                             lookup_expr='icontains')
    StudyDescription = django_filters.CharFilter(field_name='StudyDescription',
                                                 lookup_expr='icontains')
    SeriesDescription = django_filters.CharFilter(field_name='SeriesDescription',
                                                 lookup_expr='icontains')
    pacs_identifier = django_filters.CharFilter(field_name='pacs__identifier',
                                                lookup_expr='exact')
    min_PatientAge = django_filters.NumberFilter(field_name='PatientAge',
                                                 lookup_expr='gte')
    max_PatientAge = django_filters.NumberFilter(field_name='PatientAge',
                                                 lookup_expr='lte')

    class Meta:
        model = PACSFile
        fields = ['id', 'min_creation_date', 'max_creation_date', 'fname', 'fname_exact',
                  'fname_icontains', 'fname_nslashes', 'PatientID', 'PatientName',
                  'PatientSex', 'PatientAge', 'min_PatientAge', 'max_PatientAge',
                  'PatientBirthDate', 'StudyDate', 'AccessionNumber', 'ProtocolName',
                  'StudyInstanceUID', 'StudyDescription', 'SeriesInstanceUID',
                  'SeriesDescription', 'pacs_identifier']

    def filter_by_n_slashes(self, queryset, name, value):
        """
        Custom method to return the files that have the queried number of slashes in
        their fname property. If the queried number ends in 'u' or 'U' then only one
        file per each last "folder" in the path is returned (useful to efficiently get
        the list of immediate folders under the path).
        """
        return filter_files_by_n_slashes(queryset, value)


# chris_backend/pacsfiles/permissions.py


from rest_framework import permissions


class IsChrisOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow superuser 'chris' to create it.
    Read only is allowed to other users.
    """

    def has_permission(self, request, view):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the superuser 'chris'.
        return request.user.username == 'chris'


# chris_backend/pacsfiles/serializers.py


import logging

from django.conf import settings
from rest_framework import serializers

from collectionjson.fields import ItemLinkField
from core.utils import get_file_resource_link
from core.swiftmanager import SwiftManager

from .models import PACS, PACSFile


logger = logging.getLogger(__name__)


class PACSSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = PACS
        fields = ('url', 'id', 'identifier')


class PACSFileSerializer(serializers.HyperlinkedModelSerializer):
    file_resource = ItemLinkField('get_file_link')
    path = serializers.CharField(write_only=True)
    fname = serializers.FileField(use_url=False, required=False)
    fsize = serializers.ReadOnlyField(source='fname.size')
    pacs_identifier = serializers.ReadOnlyField(source='pacs.identifier')
    pacs_name = serializers.CharField(write_only=True)

    class Meta:
        model = PACSFile
        fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'path', 'PatientID',
                  'PatientName', 'PatientBirthDate', 'PatientAge', 'PatientSex',
                  'StudyDate', 'AccessionNumber', 'Modality', 'ProtocolName',
                  'StudyInstanceUID', 'StudyDescription', 'SeriesInstanceUID',
                  'SeriesDescription', 'pacs_identifier', 'pacs_name', 'file_resource')

    def get_file_link(self, obj):
        """
        Custom method to get the hyperlink to the actual file resource.
        """
        return get_file_resource_link(self, obj)

    def create(self, validated_data):
        """
        Overriden to associate a Swift storage path with the newly created pacs file.
        """
        # remove path as it is not part of the model and then compute fname
        path = validated_data.pop('path')
        validated_data['fname'] = path
        return super(PACSFileSerializer, self).create(validated_data)

    def validate_pacs_name(self, pacs_name):
        """
        Overriden to check whether the provided PACS name is a valid PACS identifier.
        """
        try:
            PACS.objects.get(identifier=pacs_name)
        except PACS.DoesNotExist:
            # validate new PACS identifier
            pacs_serializer = PACSSerializer(data={'identifier': pacs_name})
            try:
                pacs_serializer.is_valid(raise_exception=True)
            except serializers.ValidationError as e:
                raise serializers.ValidationError(e.detail['identifier'])
        return pacs_name

    def validate_path(self, path):
        """
        Overriden to check whether the provided path is under SERVICES/PACS/ path.
        """
        path = path.strip(' ').strip('/')
        if not path.startswith('SERVICES/PACS/'):
            raise serializers.ValidationError(
                ["File path must start with 'SERVICES/PACS/'."])
        # verify that the file is indeed already in Swift
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        try:
            swift_path_exists = swift_manager.obj_exists(path)
        except Exception as e:
            logger.error('Swift storage error, detail: %s' % str(e))
            raise serializers.ValidationError(["Could not find this path."])
        if not swift_path_exists:
            raise serializers.ValidationError(["Could not find this path."])
        return path

    def validate(self, data):
        """
        Overriden to validate calculated API descriptors from the provided and check
        whether the provided path is already registered.
        """
        # remove pacs_name as it is not part of the model
        pacs_name = data.pop('pacs_name')
        path = data.get('path')
        prefix = 'SERVICES/PACS/%s/' % pacs_name
        if not path.startswith(prefix):
            error_msg = "File path must start with '%s'." % prefix
            raise serializers.ValidationError([error_msg])
        # verify that the file has not already been registered
        try:
            PACSFile.objects.get(fname=path)
        except PACSFile.DoesNotExist:
            pass
        else:
            error_msg = "File has already been registered."
            raise serializers.ValidationError({'path': [error_msg]})
        # update validated data with a pacs object
        (pacs, tf) = PACS.objects.get_or_create(identifier=pacs_name)
        data.update({'pacs': pacs})
        return data


# chris_backend/pacsfiles/views.py


from django.http import FileResponse
from rest_framework import generics, permissions
from rest_framework.reverse import reverse

from collectionjson import services
from core.renderers import BinaryFileRenderer

from .models import PACSFile, PACSFileFilter
from .serializers import PACSFileSerializer
from .permissions import IsChrisOrReadOnly


class PACSFileList(generics.ListCreateAPIView):
    """
    A view for the collection of PACS files.
    """
    http_method_names = ['get', 'post']
    queryset = PACSFile.objects.all()
    serializer_class = PACSFileSerializer
    permission_classes = (permissions.IsAuthenticated, IsChrisOrReadOnly,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a collection+json
        template to the response.
        """
        response = super(PACSFileList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('pacsfile-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append write template
        template_data = {'path': '', 'PatientID': '', 'PatientName': '',
                         'PatientBirthDate': '', 'PatientAge': '', 'PatientSex': '',
                         'StudyDate': '', 'AccessionNumber': '', 'Modality': '',
                         'ProtocolName': '', 'StudyInstanceUID': '',
                         'StudyDescription': '', 'SeriesInstanceUID': '',
                         'SeriesDescription': '', 'pacs_name': ''}
        return services.append_collection_template(response, template_data)

    def create(self, request, *args, **kwargs):
        """
        Overriden to remove computed descriptors from the request if submitted.
        """
        self.request.data.pop('fname', None)
        return super(PACSFileList, self).create(request, *args, **kwargs)


class PACSFileListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of PACS files resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PACSFileSerializer
    queryset = PACSFile.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PACSFileFilter


class PACSFileDetail(generics.RetrieveAPIView):
    """
    A PACS file view.
    """
    http_method_names = ['get']
    queryset = PACSFile.objects.all()
    serializer_class = PACSFileSerializer
    permission_classes = (permissions.IsAuthenticated,)


class PACSFileResource(generics.GenericAPIView):
    """
    A view to enable downloading of a file resource .
    """
    http_method_names = ['get']
    queryset = PACSFile.objects.all()
    renderer_classes = (BinaryFileRenderer,)
    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, *args, **kwargs):
        """
        Overriden to be able to make a GET request to an actual file resource.
        """
        pacs_file = self.get_object()
        return FileResponse(pacs_file.fname)


# chris_backend/pipelineinstances/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/pipelineinstances/apps.py

from django.apps import AppConfig


class Pipelineinstances(AppConfig):
    name = 'pipelineinstances'


# chris_backend/pipelineinstances/models.py


from django.db import models
import django_filters
from django_filters.rest_framework import FilterSet

from pipelines.models import Pipeline
from plugins.fields import CPUField, MemoryField


class PipelineInstance(models.Model):
    title = models.CharField(max_length=100, blank=True)
    description = models.CharField(max_length=800, blank=True)
    pipeline = models.ForeignKey(Pipeline, on_delete=models.CASCADE,
                                 related_name='instances')
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    cpu_limit = CPUField(null=True)
    memory_limit = MemoryField(null=True)
    number_of_workers = models.IntegerField(null=True)
    gpu_limit = models.IntegerField(null=True)

    class Meta:
        ordering = ('pipeline',)

    def __str__(self):
        return self.title


class PipelineInstanceFilter(FilterSet):
    title = django_filters.CharFilter(field_name='title', lookup_expr='icontains')
    description = django_filters.CharFilter(field_name='description',
                                            lookup_expr='icontains')
    pipeline_name = django_filters.CharFilter(field_name='pipeline__name',
                                               lookup_expr='icontains')
    owner_username = django_filters.CharFilter(field_name='owner__username',
                                               lookup_expr='exact')

    class Meta:
        model = PipelineInstance
        fields = ['id', 'title', 'description', 'pipeline_name', 'owner_username']


# chris_backend/pipelineinstances/permissions.py

from rest_framework import permissions


class IsOwnerOrChrisOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it. Read only is allowed to other users.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner and superuser 'chris'.
        return (request.user == obj.owner) or (request.user.username == 'chris')

# chris_backend/pipelineinstances/serializers.py


from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers

from plugininstances.models import PluginInstance

from .models import PipelineInstance


class PipelineInstanceSerializer(serializers.HyperlinkedModelSerializer):
    pipeline_id = serializers.ReadOnlyField(source='pipeline.id')
    pipeline_name = serializers.ReadOnlyField(source='pipeline.name')
    previous_plugin_inst_id = serializers.IntegerField(min_value=1, write_only=True,
                                                       required=False)
    owner_username = serializers.ReadOnlyField(source='owner.username')
    pipeline = serializers.HyperlinkedRelatedField(view_name='pipeline-detail',
                                                   read_only=True)
    plugin_instances = serializers.HyperlinkedIdentityField(
        view_name='pipelineinstance-plugininstance-list')

    class Meta:
        model = PipelineInstance
        fields = ('url', 'id', 'title', 'pipeline_id', 'pipeline_name', 'owner_username',
                  'description', 'previous_plugin_inst_id', 'pipeline',
                  'plugin_instances', 'cpu_limit', 'memory_limit',
                  'number_of_workers', 'gpu_limit')

    def create(self, validated_data):
        """
        Overriden to delete 'previous_plugin_inst_id' from serializer data as it's not
        a model field.
        """
        del validated_data['previous_plugin_inst_id']
        return super(PipelineInstanceSerializer, self).create(validated_data)

    def validate_previous_plugin_inst(self, previous_plugin_inst_id):
        """
        Custom method to check that an integer id is provided for previous instance. Then
        check that the id exists in the DB and that the user can run plugins within the
        corresponding feed.
        """
        if not previous_plugin_inst_id:
            raise serializers.ValidationError(
                {'previous_plugin_inst_id': ["This field is required."]})
        try:
            pk = int(previous_plugin_inst_id)
            previous_plugin_inst = PluginInstance.objects.get(pk=pk)
        except (ValueError, ObjectDoesNotExist):
            raise serializers.ValidationError(
                {'previous_plugin_inst_id':
                     ["Couldn't find any 'previous' plugin instance with id %s."]})
        # check that the user can run plugins within this feed
        user = self.context['request'].user
        if user not in previous_plugin_inst.feed.owner.all():
            raise serializers.ValidationError(
                {'previous_plugin_inst_id':
                     ["User is not an owner of feed for previous instance with id %s." %
                      previous_plugin_inst_id]})
        return previous_plugin_inst

    def parse_parameters(self):
        """
        Custom method to parse pipeline instance parameters in the request data
        dictionary.
        """
        request_data = self.context['request'].data
        # parameters name in the request have the form
        # < plugin.id > _ < piping.id > _ < previous_piping.id > _ < param.name >
        parsed_params_dict = {}
        for param in request_data:
            try:
                param_elements = param.split('_')
                id_list = param_elements[:3]
                piping_id = int(id_list[1])
            except Exception:
                pass
            else:
                param_name = '_'.join(param_elements[3:])
                if piping_id in parsed_params_dict:
                    parsed_params_dict[piping_id][param_name] = request_data[param]
                else:
                    parsed_params_dict[piping_id] = {param_name: request_data[param]}
        return parsed_params_dict


# chris_backend/pipelineinstances/views.py


from rest_framework import generics, permissions
from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.response import Response
from rest_framework.serializers import ValidationError

from collectionjson import services
from pipelines.models import Pipeline
from plugininstances.models import PluginInstance
from plugininstances.serializers import PluginInstanceSerializer
from plugininstances.serializers import PARAMETER_SERIALIZERS
from plugininstances.tasks import cancel_plugin_instance
from plugins.fields import MemoryInt, CPUInt

from .models import PipelineInstance, PipelineInstanceFilter
from .serializers import PipelineInstanceSerializer
from .permissions import IsOwnerOrChrisOrReadOnly


class PipelineInstanceList(generics.ListCreateAPIView):
    """
    A view for the collection of pipeline-specific instances.
    """
    http_method_names = ['get', 'post']
    serializer_class = PipelineInstanceSerializer
    queryset = Pipeline.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def perform_create(self, serializer):
        """
        Overriden to associate a pipeline with the newly created pipeline instance before
        first saving to the DB. All the pipeline instance's parameters in the request are
        parsed and properly saved to the DB with the corresponding plugin instances.
        """
        # get previous plugin instance
        request_data = serializer.context['request'].data
        previous_plugin_inst_id = request_data['previous_plugin_inst_id'] \
            if 'previous_plugin_inst_id' in request_data else ""
        previous_plugin_inst = serializer.validate_previous_plugin_inst(
            previous_plugin_inst_id)
        pipeline = self.get_object()
        # parse and transform plugin parameter names in the request
        self.parsed_parameters = serializer.parse_parameters()
        # create a plugin instance for each piping in the pipeline in the same
        # tree order as the pipings
        pipings_tree = pipeline.get_pipings_tree()
        tree = pipings_tree['tree']
        root_id = pipings_tree['root_id']
        root_pip = tree[root_id]['piping']
        plugin_inst_dict = self.create_plugin_inst(root_pip, previous_plugin_inst)
        plugin_instances = [plugin_inst_dict]
        # breath-first traversal
        plugin_inst_queue = [plugin_inst_dict]
        pip_id_queue = [root_id]
        while len(pip_id_queue):
            curr_id = pip_id_queue.pop(0)
            curr_plugin_inst_dict = plugin_inst_queue.pop(0)
            child_ids = tree[curr_id]['child_ids']
            for id in child_ids:
                pip = tree[id]['piping']
                plugin_inst_dict = self.create_plugin_inst(
                    pip, curr_plugin_inst_dict['plugin_inst'])
                plugin_instances.append(plugin_inst_dict)
                pip_id_queue.append(id)
                plugin_inst_queue.append(plugin_inst_dict)
        # if no validation errors at this point then save to the DB
        self.pipeline_inst = serializer.save(owner=self.request.user, pipeline=pipeline)
        for plg_inst_dict in plugin_instances:
            self.save_plugin_inst(plg_inst_dict)

        # run the plugin's app
        # PipelineInstanceManager.run_pipeline_instance(pipeline_inst,
        #                                 parameters_dict,
        #                                 service='pfcon',
        #                                 inputDirOverride='/share/incoming',
        #                                 outputDirOverride='/share/outgoing')

    def list(self, request, *args, **kwargs):
        """
        Overriden to return the list of pipeline instances for the queried pipeline.
        A document-level link relation and a collection+json template are also added to
        the response.
        """
        queryset = self.get_pipeline_instances_queryset()
        response = services.get_list_response(self, queryset)
        pipeline = self.get_object()
        # append document-level link relations
        links = {'pipeline': reverse('pipeline-detail', request=request,
                                   kwargs={"pk": pipeline.id})}
        response = services.append_collection_links(response, links)
        # append write template
        template_data = {'previous_plugin_inst_id': "", 'title': "", 'description': "",
                         'cpu_limit': "", 'memory_limit': "", 'number_of_workers': "",
                         'gpu_limit': ""}
        param_names = pipeline.get_pipings_parameters_names()
        for name in param_names:
            template_data[name] = ""
        return services.append_collection_template(response, template_data)

    def get_pipeline_instances_queryset(self):
        """
        Custom method to get the actual pipeline instances' queryset.
        """
        pipeline = self.get_object()
        return self.filter_queryset(pipeline.instances.all())

    def create_plugin_inst(self, piping, previous_inst):
        """
        Custom method to create a plugin instance and validate its parameters.
        """
        owner = self.request.user
        plugin_inst = PluginInstance()
        plugin_inst.owner = owner
        plugin_inst.plugin = piping.plugin
        plugin_inst.previous = previous_inst
        plugin_inst.compute_resource = piping.plugin.compute_resources.all()[0]
        # collect and validate parameters from the request
        parsed_parameters = self.parsed_parameters
        parameter_serializers = []
        parameters = piping.plugin.parameters.all()
        for parameter in parameters:
            if (piping.id in parsed_parameters) and (parameter.name in
                                                     parsed_parameters[piping.id]):
                request_value = parsed_parameters[piping.id][parameter.name]
                data = {'value': request_value}
            else:
                default = getattr(piping, parameter.type + '_param').filter(
                    plugin_param=parameter)[0]
                data = {'value': default.value}
            parameter_serializer = PARAMETER_SERIALIZERS[parameter.type](data=data)
            try:
                parameter_serializer.is_valid(raise_exception=True)
            except ValidationError:
                raise ValidationError({'detail': 'A valid %s is required for %s'
                                                 % (parameter.type, parameter.name)})
            parameter_serializers.append((parameter, parameter_serializer))
        return {'plugin_inst': plugin_inst,
                'parameter_serializers': parameter_serializers}

    def save_plugin_inst(self, plg_inst_dict):
        """
        Custom method to save a plugin instance and its parameters to the DB.
        """
        plg_inst = plg_inst_dict['plugin_inst']
        plg = plg_inst.plugin
        pipeline_inst = self.pipeline_inst
        plg_inst.pipeline_inst = pipeline_inst
        plg_inst.title = pipeline_inst.title
        # set cpu limit
        if pipeline_inst.cpu_limit:
            plg_inst.cpu_limit = pipeline_inst.cpu_limit
            if plg_inst.cpu_limit < CPUInt(plg.min_cpu_limit):
                plg_inst.cpu_limit = CPUInt(plg.min_cpu_limit)
            if plg_inst.cpu_limit > CPUInt(plg.max_cpu_limit):
                plg_inst.cpu_limit = CPUInt(plg.max_cpu_limit)
        # set memory limit
        if pipeline_inst.memory_limit:
            plg_inst.memory_limit = pipeline_inst.memory_limit
            if plg_inst.memory_limit < MemoryInt(plg.min_memory_limit):
                plg_inst.memory_limit = MemoryInt(plg.min_memory_limit)
            if plg_inst.memory_limit > MemoryInt(plg.max_memory_limit):
                plg_inst.memory_limit = MemoryInt(plg.max_memory_limit)
        # set number of workers
        if pipeline_inst.number_of_workers:
            plg_inst.number_of_workers = pipeline_inst.number_of_workers
            if plg_inst.number_of_workers < plg.number_of_workers:
                plg_inst.number_of_workers = plg.min_number_of_workers
            if plg_inst.number_of_workers > plg.max_number_of_workers:
                plg_inst.number_of_workers = plg.max_number_of_workers
        # set gpu limit
        if pipeline_inst.gpu_limit:
            plg_inst.gpu_limit = pipeline_inst.gpu_limit
            if plg_inst.gpu_limit < plg.min_gpu_limit:
                plg_inst.gpu_limit = plg.min_gpu_limit
            if plg_inst.gpu_limit > plg.max_gpu_limit:
                plg_inst.gpu_limit = plg.max_gpu_limit
        # save plugin instance
        previous = plg_inst.previous
        plg_inst.save()
        plg_inst.previous = previous
        plg_inst.save()
        # save plugin instance's parameters
        for param, param_serializer in plg_inst_dict['parameter_serializers']:
            param_serializer.save(plugin_inst=plg_inst, plugin_param=param)


class AllPipelineInstanceList(generics.ListAPIView):
    """
    A view for the collection of all pipeline instances.
    """
    http_method_names = ['get']
    serializer_class = PipelineInstanceSerializer
    queryset = PipelineInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to add a query list and document-level link relation to the response.
        """
        response = super(AllPipelineInstanceList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('allpipelineinstance-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'pipelines': reverse('pipeline-list', request=request)}
        return services.append_collection_links(response, links)


class AllPipelineInstanceListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of pipeline instances resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PipelineInstanceSerializer
    queryset = PipelineInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PipelineInstanceFilter


class PipelineInstanceDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    A pipeline instance view.
    """
    http_method_names = ['get', 'put', 'delete']
    serializer_class = PipelineInstanceSerializer
    queryset = PipelineInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChrisOrReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template to the response.
        """
        response = super(PipelineInstanceDetail, self).retrieve(request, *args, **kwargs)
        template_data = {'title': '', 'description': ''}
        return services.append_collection_template(response, template_data)

    def update(self, request, *args, **kwargs):
        """
        Overriden to remove descriptors that are not allowed to be updated before
        serializer validation.
        """
        data = self.request.data
        data.pop('gpu_limit', None)
        data.pop('number_of_workers', None)
        data.pop('cpu_limit', None)
        data.pop('memory_limit', None)
        return super(PipelineInstanceDetail, self).update(request, *args, **kwargs)

    def destroy(self, request, *args, **kwargs):
        """
        Overriden to cancel all the associated plugin instances before attempting to
        delete the pipeline instance.
        """
        pipeline_inst = self.get_object()
        for plg_inst in pipeline_inst.plugin_instances.all():
            if plg_inst.status == 'started':
                cancel_plugin_instance.delay(plg_inst.id)  # call async task
            plg_inst.status = 'cancelled'
            plg_inst.save()
        return super(PipelineInstanceDetail, self).destroy(request, *args, **kwargs)


class PipelineInstancePluginInstanceList(generics.ListAPIView):
    """
    A view for the collection of plugin instances that compose the pipeline instance.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceSerializer
    queryset = PipelineInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list with all the plugin instances that are part of
        the queried pipeline instance.
        """
        queryset = self.get_plugin_instances_queryset()
        return services.get_list_response(self, queryset)

    def get_plugin_instances_queryset(self):
        """
        Custom method to get a queryset with all the plugin instances that are part of
        the queried pipeline instance.
        """
        pipeline_inst = self.get_object()
        return self.filter_queryset(pipeline_inst.plugin_instances.all())


# chris_backend/pipelines/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/pipelines/apps.py

from django.apps import AppConfig


class PipelinesConfig(AppConfig):
    name = 'pipelines'


# chris_backend/pipelines/models.py


from django.db import models
from django.core.exceptions import ObjectDoesNotExist

import django_filters
from django_filters.rest_framework import FilterSet

from plugins.models import Plugin, PluginParameter


class Pipeline(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    modification_date = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=100, unique=True)
    locked = models.BooleanField(default=True)
    authors = models.CharField(max_length=200, blank=True)
    category = models.CharField(max_length=100, blank=True)
    description = models.CharField(max_length=800, blank=True)
    owner = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL)
    plugins = models.ManyToManyField(Plugin, related_name='pipelines',
                                     through='PluginPiping')

    class Meta:
        ordering = ('category',)

    def __str__(self):
        return self.name

    def get_pipings_parameters_names(self):
        """
        Custom method to get the list of all the plugin parameter names for all the
        associated plugin pipings. The name of the parameters is transformed to have the
        plugin id, piping id and previous piping id as a prefix.
        """
        pipings = self.plugin_pipings.all()
        param_names = []
        for pip in pipings:
            plg = pip.plugin
            prev_pip_id = pip.previous.id if pip.previous else 'null'
            # param name becomes <plugin.id>_<piping.id>_<previous_piping.id>_<param.name>
            param_names.extend(['%s_%s_%s_%s' % (plg.id, pip.id, prev_pip_id, param.name)
                                for param in plg.parameters.all()])
        return param_names

    def get_pipings_tree(self):
        """
        Custom method to return a dictionary containing a dictionary representing a tree
        of pipings and the id of the piping that is the root of the tree. The keys of the
        dictionary tree are the pipings' ids and the values are dictionaries containing
        the piping and the list of child pipings' ids.
        """
        pipings = list(self.plugin_pipings.all())
        root_pip = [pip for pip in pipings if not pip.previous][0]
        root_id = root_pip.id
        tree = {}
        tree[root_id] = {'piping': root_pip, 'child_ids':[]}
        for pip in pipings:
            if pip.id not in tree:
                tree[pip.id] = {'piping': pip, 'child_ids': []}
                prev_id = pip.previous.id
                if prev_id in tree:
                    tree[prev_id]['child_ids'].append(pip.id)
                else:
                    tree[prev_id] = {'piping': pip.previous, 'child_ids': [pip.id]}
        return {'root_id': root_id, 'tree': tree}

    def check_parameter_defaults(self):
        """
        Custom method to raise an exception if any of the plugin parameters associated to
        any of the pipings in the pipeline doesn't have a default value.
        """
        for piping in self.plugin_pipings.all():
            piping.check_parameter_defaults()

    @staticmethod
    def get_accesible_pipelines(user):
        """
        Custom method to get a filtered queryset with all the pipelines that are
        accessible to a given user (not locked or otherwise own by the user).
        """
        queryset = Pipeline.objects.all()
        # if the user is chris then return all the pipelines in the queryset
        if user.username == 'chris':
            return queryset
        # construct the full lookup expression.
        lookup = models.Q(locked=False) | models.Q(owner=user)
        return queryset.filter(lookup)


class PipelineFilter(FilterSet):
    min_creation_date = django_filters.IsoDateTimeFilter(field_name="creation_date",
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name="creation_date",
                                                         lookup_expr='lte')
    owner_username = django_filters.CharFilter(field_name='owner__username',
                                               lookup_expr='exact')
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    category = django_filters.CharFilter(field_name='category', lookup_expr='icontains')
    description = django_filters.CharFilter(field_name='description',
                                            lookup_expr='icontains')
    authors = django_filters.CharFilter(field_name='authors', lookup_expr='icontains')

    class Meta:
        model = Pipeline
        fields = ['id', 'owner_username', 'name', 'category', 'description',
                  'authors', 'min_creation_date', 'max_creation_date']


class PluginPiping(models.Model):
    plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE)
    pipeline = models.ForeignKey(Pipeline, on_delete=models.CASCADE,
                                 related_name='plugin_pipings')
    previous = models.ForeignKey("self", on_delete=models.CASCADE, null=True,
                                 related_name='next')

    class Meta:
        ordering = ('pipeline',)

    def __str__(self):
        return str(self.id)

    def save(self, *args, **kwargs):
        """
        Overriden to save the default plugin parameters' values associated with this
        piping.
        """
        param_defaults = []
        if 'parameter_defaults' in kwargs:
            param_defaults = kwargs['parameter_defaults']
            del kwargs['parameter_defaults']
        super(PluginPiping, self).save(*args, **kwargs)
        plugin = self.plugin
        parameters = plugin.parameters.all()
        for parameter in parameters:
            param = [d for d in param_defaults if d['name'] == parameter.name]
            default_model_class = DEFAULT_PIPING_PARAMETER_MODELS[parameter.type]
            try:
                default_piping_param = default_model_class.objects.get(
                    plugin_piping=self, plugin_param=parameter)
            except ObjectDoesNotExist:
                default_piping_param = default_model_class()
                default_piping_param.plugin_piping = self
                default_piping_param.plugin_param = parameter
                if param:
                    default_piping_param.value = param[0]['default']
                else:
                    # use plugin parameter's default for piping's default
                    default = parameter.get_default()
                    default_piping_param.value = default.value if default else None
                default_piping_param.save()
            else:
                if param:
                    default_piping_param.value = param[0]['default']
                    default_piping_param.save()

    def check_parameter_defaults(self):
        """
        Custom method to raise an exception if any of the plugin parameters associated to
        the piping doesn't have a default value.
        """
        for type in DEFAULT_PIPING_PARAMETER_MODELS:
            typed_parameters = getattr(self, type + '_param')
            for parameter in typed_parameters.all():
                if parameter.value is None:
                    raise ValueError('A default is required for parameter %s'
                                     % parameter.plugin_param.name)


class DefaultPipingStrParameter(models.Model):
    value = models.CharField(max_length=600, null=True)
    plugin_piping = models.ForeignKey(PluginPiping, on_delete=models.CASCADE,
                                    related_name='string_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='string_piping_default')

    class Meta:
        unique_together = ('plugin_piping', 'plugin_param',)

    def __str__(self):
        return self.value


class DefaultPipingIntParameter(models.Model):
    value = models.IntegerField(null=True)
    plugin_piping = models.ForeignKey(PluginPiping, on_delete=models.CASCADE,
                                    related_name='integer_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='integer_piping_default')

    class Meta:
        unique_together = ('plugin_piping', 'plugin_param',)

    def __str__(self):
        return str(self.value)


class DefaultPipingFloatParameter(models.Model):
    value = models.FloatField(null=True)
    plugin_piping = models.ForeignKey(PluginPiping, on_delete=models.CASCADE,
                                    related_name='float_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='float_piping_default')

    class Meta:
        unique_together = ('plugin_piping', 'plugin_param',)

    def __str__(self):
        return str(self.value)


class DefaultPipingBoolParameter(models.Model):
    value = models.BooleanField(null=True)
    plugin_piping = models.ForeignKey(PluginPiping, on_delete=models.CASCADE,
                                    related_name='boolean_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='boolean_piping_default')

    class Meta:
        unique_together = ('plugin_piping', 'plugin_param',)

    def __str__(self):
        return str(self.value)


DEFAULT_PIPING_PARAMETER_MODELS = {'string': DefaultPipingStrParameter,
                                   'integer': DefaultPipingIntParameter,
                                   'float': DefaultPipingFloatParameter,
                                   'boolean': DefaultPipingBoolParameter}


# chris_backend/pipelines/permissions.py


from rest_framework import permissions


class IsChrisOrOwnerOrNotLockedReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it. Read only is allowed to other users only
    when object is not locked.
    """

    def has_object_permission(self, request, view, obj):
        if (request.user.username == 'chris') or (obj.owner == request.user):
            # superuser 'chris' and owner always have read/write access
            return True

        # Read permissions are allowed to other users if object is not locked,
        return (request.method in permissions.SAFE_METHODS) and not obj.locked


class IsChrisOrOwnerAndLockedOrNotLockedReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow read/write access to the Superuser Chris or the
    object's owner when corresponding pipeline is locked. Read only access is granted to
    any user when corresponding pipeline is not locked.
    """

    def has_object_permission(self, request, view, obj):
        pipeline = obj.plugin_piping.pipeline

        if request.user.username == 'chris':
            # superuser 'chris' always has read/write access
            return True

        if pipeline.locked:
            # owner has read/write access
            return pipeline.owner == request.user
        else:
            # only allow read access (GET, HEAD or OPTIONS requests.)
            return request.method in permissions.SAFE_METHODS


class IsChrisOrOwnerOrNotLocked(permissions.BasePermission):
    """
    Custom permission to only allow access to the Superuser Chris or the object's owner.
    Access is granted to any other user when corresponding pipeline is not locked.
    """

    def has_object_permission(self, request, view, obj):
        pipeline = obj.pipeline
        if (request.user.username == 'chris') or (pipeline.owner == request.user):
            # superuser 'chris' and owner always have read/write access
            return True
        return not pipeline.locked

# chris_backend/pipelines/serializers.py


import json

from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from rest_framework import serializers
from rest_framework.reverse import reverse

from collectionjson.fields import ItemLinkField
from plugins.models import Plugin, TYPES
from plugins.serializers import DEFAULT_PARAMETER_SERIALIZERS
from plugininstances.models import PluginInstance

from .models import Pipeline, PluginPiping
from .models import DefaultPipingFloatParameter, DefaultPipingIntParameter
from .models import DefaultPipingBoolParameter, DefaultPipingStrParameter


class PipelineSerializer(serializers.HyperlinkedModelSerializer):
    plugin_tree = serializers.JSONField(write_only=True, required=False)
    plugin_inst_id = serializers.IntegerField(min_value=1, write_only=True,
                                              required=False)
    owner_username = serializers.ReadOnlyField(source='owner.username')
    plugins = serializers.HyperlinkedIdentityField(view_name='pipeline-plugin-list')
    plugin_pipings = serializers.HyperlinkedIdentityField(
        view_name='pipeline-pluginpiping-list')
    default_parameters = serializers.HyperlinkedIdentityField(
        view_name='pipeline-defaultparameter-list')
    instances = serializers.HyperlinkedIdentityField(view_name='pipelineinstance-list')

    class Meta:
        model = Pipeline
        fields = ('url', 'id', 'name', 'locked', 'authors', 'category', 'description',
                  'plugin_tree', 'plugin_inst_id', 'owner_username', 'creation_date',
                  'modification_date', 'plugins', 'plugin_pipings', 'default_parameters',
                  'instances')

    def create(self, validated_data):
        """
        Overriden to create the pipeline and associate to it a tree of plugins computed
        either from an existing plugin instance or from a passed tree.
        """
        tree_dict = validated_data.pop('plugin_tree', None)
        root_plg_inst = validated_data.pop('plugin_inst_id', None)
        pipeline = super(PipelineSerializer, self).create(validated_data)

        if not tree_dict:  # generate tree_dict from passed plugin instance
            tree_dict = {'root_index': 0, 'tree': []}
            curr_ix = 0
            queue = [root_plg_inst]
            while len(queue) > 0:
                visited_instance = queue.pop()
                plg_id = visited_instance.plugin.id
                child_instances = list(visited_instance.next.all())
                queue.extend(child_instances)
                lower_ix = curr_ix + 1
                upper_ix = lower_ix + len(child_instances)
                child_indices = list(range(lower_ix, upper_ix))
                parameter_instances = visited_instance.get_parameter_instances()
                # parameter defaults assigned from the plugin instance's parameter values
                defaults = [{'name': p_inst.plugin_param.name, 'default': p_inst.value}
                            for p_inst in parameter_instances]
                tree_dict['tree'].append({'plugin_id': plg_id,
                                          'plugin_parameter_defaults': defaults,
                                          'child_indices': child_indices})
                curr_ix = upper_ix - 1
        PipelineSerializer._add_plugin_tree_to_pipeline(pipeline, tree_dict)
        return pipeline

    def update(self, instance, validated_data):
        """
        Overriden to remove parameters that are not allowed to be used on update and
        to add modification date.
        """
        validated_data.pop('plugin_tree', None)
        validated_data.pop('plugin_inst_id', None)
        validated_data.update({'modification_date': timezone.now()})
        return super(PipelineSerializer, self).update(instance, validated_data)

    def validate(self, data):
        """
        Overriden to validate that at least one of two fields are in data
        when creating a new pipeline.
        """
        if not self.instance:  # this validation only happens on create and not on update
            if 'plugin_tree' not in data and 'plugin_inst_id' not in data:
                raise serializers.ValidationError(
                    {'non_field_errors': ["At least one of the fields 'plugin_tree' "
                                          "or 'plugin_inst_id' must be provided."]})
            if 'plugin_tree' in data  and 'locked' in data and not data['locked']:
                # if user wants to unlock pipeline right away at creation time then check
                # that defaults for all plugin parameters can be defined
                tree = data['plugin_tree']['tree']
                for node in tree:
                    plg = Plugin.objects.get(pk=node['plugin_id'])
                    parameters = plg.parameters.all()
                    for parameter in parameters:
                        default = parameter.get_default()
                        parameter_default = default.value if default else None
                        if parameter_default is None:  # no default provided by the plugin
                            plg_param_defaults = node['plugin_parameter_defaults']
                            param_default = [d for d in plg_param_defaults if
                                                  d['name'] == parameter.name]
                            if not param_default:  # no default provided by the user
                                error_msg = 'Pipeline can not be unlocked until all ' \
                                            'plugin parameters have default values.'
                                raise serializers.ValidationError(
                                    {'non_field_errors': [error_msg]})
        return data

    def validate_plugin_inst_id(self, plugin_inst_id):
        """
        Overriden to validate the plugin instance id.
        """
        try:
            plg_inst = PluginInstance.objects.get(pk=plugin_inst_id)
        except ObjectDoesNotExist:
            raise serializers.ValidationError(
                ["Couldn't find any plugin instance with id %s." % plugin_inst_id])
        plg = plg_inst.plugin
        if plg.meta.type == 'fs':
            raise serializers.ValidationError(
                ["Plugin instance of %s which is of type 'fs' and therefore can not "
                 "be used as the root of a new pipeline." % plg.meta.name])
        return plg_inst

    def validate_plugin_tree(self, plugin_tree):
        """
        Overriden to validate the tree of plugin ids. It should be a list of dictionaries.
        Each dictionary is a tree node containing the index of the previous node in the
        list and either a plugin id or a plugin name and a plugin version.
        """
        try:
            plugin_list = list(json.loads(plugin_tree))
        except json.decoder.JSONDecodeError:
            # overriden validation methods automatically add the field name to the msg
            msg = ["Invalid JSON string %s." % plugin_tree]
            raise serializers.ValidationError(msg)
        except Exception:
            msg = ["Invalid tree list in %s" % plugin_tree]
            raise serializers.ValidationError(msg)
        if len(plugin_list) == 0:
            msg = ["Invalid empty list in %s" % plugin_tree]
            raise serializers.ValidationError(msg)

        for d in plugin_list:
            try:
                prev_ix = d['previous_index']
                if 'plugin_id' not in d:
                    plg_name = d['plugin_name']
                    plg_version = d['plugin_version']
                    plg = Plugin.objects.get(meta__name=plg_name, version=plg_version)
                    d['plugin_id'] = plg.id
                else:
                    plg_id = d['plugin_id']
                    plg = Plugin.objects.get(pk=plg_id)
            except ObjectDoesNotExist:
                if 'plugin_id' not in d:
                    msg = ["Couldn't find any plugin with name %s and version %s." %
                     (plg_name, plg_version)]
                else:
                    msg = ["Couldn't find any plugin with id %s." % plg_id]
                raise serializers.ValidationError(msg)
            except Exception:
                msg = ["Object %s must be a JSON object with 'previous_index' and "
                       "either 'plugin_id' or 'plugin_name' and 'plugin_version' "
                       "properties." % d]
                raise serializers.ValidationError(msg)
            if plg.meta.type == 'fs':
                msg = ["Plugin %s is of type 'fs' and therefore can not be used to "
                       "create a pipeline." % plg]
                raise serializers.ValidationError(msg)
            if 'plugin_parameter_defaults' in d:
                param_defaults = d['plugin_parameter_defaults']
                PipelineSerializer.validate_plugin_parameter_defaults(plg, param_defaults)
            else:
                d['plugin_parameter_defaults'] = []
        try:
            tree_dict = PipelineSerializer.get_tree(plugin_list)
            PipelineSerializer.validate_tree(tree_dict)
        except (ValueError, Exception) as e:
            raise serializers.ValidationError([str(e)])
        return tree_dict

    def validate_locked(self, locked):
        """
        Overriden to raise a validation error when the locked value is false and there
        are plugin parameters in the pipeline without default values.
        """
        error_msg = 'Pipeline can not be unlocked until all plugin parameters have ' \
                    'default values.'
        if not locked and self.instance:  # this validation only happens on update
            try:
                self.instance.check_parameter_defaults()
            except ValueError:
                raise serializers.ValidationError([error_msg])
        return locked

    @staticmethod
    def validate_plugin_parameter_defaults(plugin, parameter_defaults):
        """
        Custom method to validate the parameter names and their default values given
        for a plugin in the plugin tree.
        """
        parameters = plugin.parameters.all()
        for d in parameter_defaults:
            try:
                name = d['name']
                default = d['default']
            except KeyError:
                error_msg = "Invalid parameter default object %s. Each default object " \
                            "must have 'name' and 'default' properties." % d
                raise serializers.ValidationError({'plugin_tree': [error_msg]})
            param = [param for param in parameters if param.name == name]
            if not param:
                error_msg = "Could not find any parameter with name %s for plugin %s." % \
                            (name, plugin.meta.name)
                raise serializers.ValidationError({'plugin_tree': [error_msg]})
            default_param_serializer = DEFAULT_PARAMETER_SERIALIZERS[param[0].type](
                data={'value': default})
            if not default_param_serializer.is_valid():
                error_msg = "Invalid default value %s for parameter %s for plugin %s." % \
                            (default, name, plugin.meta.name)
                raise serializers.ValidationError({'plugin_tree': [error_msg]})

    @staticmethod
    def get_tree(tree_list):
        """
        Custom method to return a dictionary containing a list of nodes representing a
        tree of plugins and the index of the root of the tree. Each node is a dictionary
        containing the plugin id, its parameter defaults and the list of child indices.
        """
        try:
            root_ix = [ix for ix,d in enumerate(tree_list)
                       if d['previous_index'] is None][0]
        except IndexError:
            raise ValueError("Couldn't find the root of the tree in %s" % tree_list)
        tree = [None] * len(tree_list)
        plugin_id = tree_list[root_ix]['plugin_id']
        defaults = tree_list[root_ix]['plugin_parameter_defaults']
        tree[root_ix] = {'plugin_id': plugin_id,
                         'plugin_parameter_defaults': defaults,
                         'child_indices': []}
        for ix, d in enumerate(tree_list):
            if ix != root_ix:
                if not tree[ix]:
                    plugin_id = d['plugin_id']
                    defaults = d['plugin_parameter_defaults']
                    tree[ix] = {'plugin_id': plugin_id,
                                'plugin_parameter_defaults': defaults,
                                'child_indices': []}
                prev_ix = d['previous_index']
                try:
                    if tree[prev_ix]:
                        tree[prev_ix]['child_indices'].append(ix)
                    else:
                        plugin_id = tree_list[prev_ix]['plugin_id']
                        defaults = tree_list[prev_ix]['plugin_parameter_defaults']
                        tree[prev_ix] = {'plugin_id': plugin_id,
                                         'plugin_parameter_defaults': defaults,
                                         'child_indices': [ix]}
                except (IndexError, TypeError):
                    raise ValueError("Invalid 'previous_index' for node %s" % d)
        return {'root_index': root_ix, 'tree': tree}

    @staticmethod
    def validate_tree(tree_dict):
        """
        Custom method to validate whether the represented tree in tree_dict dictionary
        is a single connected component.
        """
        root_ix = tree_dict['root_index']
        tree = tree_dict['tree']
        num_nodes = len(tree)
        # breath-first traversal
        nodes = []
        queue = [root_ix]
        while len(queue):
            curr_ix = queue.pop(0)
            nodes.append(curr_ix)
            queue.extend(tree[curr_ix]['child_indices'])
        if len(nodes) < num_nodes:
            raise ValueError("Tree is not connected!")

    @staticmethod
    def _add_plugin_tree_to_pipeline(pipeline, tree_dict):
        """
        Internal custom method to associate a tree of plugins to a pipeline in the DB.
        """
        # here a piping precedes another piping if its corresponding plugin precedes
        # the other piping's plugin in the pipeline
        root_ix = tree_dict['root_index']
        tree = tree_dict['tree']
        root_plg = Plugin.objects.get(pk=tree[root_ix]['plugin_id'])
        root_plg_piping = PluginPiping.objects.create(pipeline=pipeline, plugin=root_plg)
        defaults = tree[root_ix]['plugin_parameter_defaults']
        root_plg_piping.save(parameter_defaults=defaults)
        # breath-first traversal
        piping_queue = [root_plg_piping]
        ix_queue = [root_ix]
        while len(piping_queue):
            curr_ix = ix_queue.pop(0)
            curr_piping = piping_queue.pop(0)
            for ix in tree[curr_ix]['child_indices']:
                plg = Plugin.objects.get(pk=tree[ix]['plugin_id'])
                plg_piping = PluginPiping.objects.create(pipeline=pipeline, plugin=plg,
                                                         previous=curr_piping)
                defaults = tree[ix]['plugin_parameter_defaults']
                plg_piping.save(parameter_defaults=defaults)
                ix_queue.append(ix)
                piping_queue.append(plg_piping)


class PluginPipingSerializer(serializers.HyperlinkedModelSerializer):
    plugin_id = serializers.ReadOnlyField(source='plugin.id')
    pipeline_id = serializers.ReadOnlyField(source='pipeline.id')
    previous_id = serializers.ReadOnlyField(source='previous.id')
    previous = serializers.HyperlinkedRelatedField(view_name='pluginpiping-detail',
                                                 read_only=True)
    plugin = serializers.HyperlinkedRelatedField(view_name='plugin-detail',
                                                 read_only=True)
    pipeline = serializers.HyperlinkedRelatedField(view_name='pipeline-detail',
                                                   read_only=True)

    class Meta:
        model = PluginPiping
        fields = ('url', 'id', 'plugin_id', 'pipeline_id', 'previous_id', 'previous',
                  'plugin', 'pipeline')


class DefaultPipingStrParameterSerializer(serializers.HyperlinkedModelSerializer):
    previous_plugin_piping_id = serializers.ReadOnlyField(
        source='plugin_piping.previous_id')
    plugin_piping_id = serializers.ReadOnlyField(source='plugin_piping.id')
    plugin_id = serializers.ReadOnlyField(source='plugin_piping.plugin_id')
    plugin_name = serializers.ReadOnlyField(source='plugin_param.plugin.meta.name')
    plugin_version = serializers.ReadOnlyField(source='plugin_param.plugin.version')
    param_id = serializers.ReadOnlyField(source='plugin_param.id')
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_piping = serializers.HyperlinkedRelatedField(view_name='pluginpiping-detail',
                                                   read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultPipingStrParameter
        fields = ('url', 'id', 'value', 'type', 'plugin_piping_id',
                  'previous_plugin_piping_id', 'param_name', 'param_id', 'plugin_piping',
                  'plugin_name', 'plugin_version', 'plugin_id', 'plugin_param')


class DefaultPipingIntParameterSerializer(serializers.HyperlinkedModelSerializer):
    previous_plugin_piping_id = serializers.ReadOnlyField(
        source='plugin_piping.previous_id')
    plugin_piping_id = serializers.ReadOnlyField(source='plugin_piping.id')
    plugin_id = serializers.ReadOnlyField(source='plugin_piping.plugin_id')
    plugin_name = serializers.ReadOnlyField(source='plugin_param.plugin.meta.name')
    plugin_version = serializers.ReadOnlyField(source='plugin_param.plugin.version')
    param_id = serializers.ReadOnlyField(source='plugin_param.id')
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_piping = serializers.HyperlinkedRelatedField(view_name='pluginpiping-detail',
                                                   read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultPipingIntParameter
        fields = ('url', 'id', 'value', 'type', 'plugin_piping_id',
                  'previous_plugin_piping_id', 'param_name', 'param_id', 'plugin_piping',
                  'plugin_name', 'plugin_version', 'plugin_id', 'plugin_param')


class DefaultPipingFloatParameterSerializer(serializers.HyperlinkedModelSerializer):
    previous_plugin_piping_id = serializers.ReadOnlyField(
        source='plugin_piping.previous_id')
    plugin_piping_id = serializers.ReadOnlyField(source='plugin_piping.id')
    plugin_id = serializers.ReadOnlyField(source='plugin_piping.plugin_id')
    plugin_name = serializers.ReadOnlyField(source='plugin_param.plugin.meta.name')
    plugin_version = serializers.ReadOnlyField(source='plugin_param.plugin.version')
    param_id = serializers.ReadOnlyField(source='plugin_param.id')
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_piping = serializers.HyperlinkedRelatedField(view_name='pluginpiping-detail',
                                                   read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultPipingFloatParameter
        fields = ('url', 'id', 'value', 'type', 'plugin_piping_id',
                  'previous_plugin_piping_id', 'param_name', 'param_id', 'plugin_piping',
                  'plugin_name', 'plugin_version', 'plugin_id', 'plugin_param')


class DefaultPipingBoolParameterSerializer(serializers.HyperlinkedModelSerializer):
    previous_plugin_piping_id = serializers.ReadOnlyField(
        source='plugin_piping.previous_id')
    plugin_piping_id = serializers.ReadOnlyField(source='plugin_piping.id')
    plugin_id = serializers.ReadOnlyField(source='plugin_piping.plugin_id')
    plugin_name = serializers.ReadOnlyField(source='plugin_param.plugin.meta.name')
    plugin_version = serializers.ReadOnlyField(source='plugin_param.plugin.version')
    param_id = serializers.ReadOnlyField(source='plugin_param.id')
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_piping = serializers.HyperlinkedRelatedField(view_name='pluginpiping-detail',
                                                   read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultPipingBoolParameter
        fields = ('url', 'id', 'value', 'type', 'plugin_piping_id',
                  'previous_plugin_piping_id', 'param_name', 'param_id', 'plugin_piping',
                  'plugin_name', 'plugin_version', 'plugin_id', 'plugin_param')


class GenericDefaultPipingParameterSerializer(serializers.HyperlinkedModelSerializer):
    previous_plugin_piping_id = serializers.ReadOnlyField(
        source='plugin_piping.previous_id')
    plugin_piping_id = serializers.ReadOnlyField(source='plugin_piping.id')
    plugin_id = serializers.ReadOnlyField(source='plugin_piping.plugin_id')
    plugin_name = serializers.ReadOnlyField(source='plugin_param.plugin.meta.name')
    plugin_version = serializers.ReadOnlyField(source='plugin_param.plugin.version')
    param_id = serializers.ReadOnlyField(source='plugin_param.id')
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    value = serializers.SerializerMethodField()
    url = ItemLinkField('_get_url')
    plugin_piping = serializers.HyperlinkedRelatedField(view_name='pluginpiping-detail',
                                                   read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultPipingStrParameter
        fields = ('url', 'id', 'value', 'type', 'plugin_piping_id',
                  'previous_plugin_piping_id', 'param_name', 'param_id', 'plugin_piping',
                  'plugin_name', 'plugin_version', 'plugin_id', 'plugin_param')

    def _get_url(self, obj):
        """
        Custom method to get the correct url for the serialized object regardless of
        its type.
        """
        request = self.context['request']
        # here default piping parameter detail view names are assumed to
        # follow a convention
        view_name = 'defaultpiping' + TYPES[obj.plugin_param.type] + 'parameter-detail'
        return reverse(view_name, request=request, kwargs={"pk": obj.id})

    def get_value(self, obj):
        """
        Overriden to get the default parameter value regardless of its type.
        """
        return obj.value


DEFAULT_PIPING_PARAMETER_SERIALIZERS = {'string': DefaultPipingStrParameterSerializer,
                                        'integer': DefaultPipingIntParameterSerializer,
                                        'float': DefaultPipingFloatParameterSerializer,
                                        'boolean': DefaultPipingBoolParameterSerializer}


# chris_backend/pipelines/views.py


from rest_framework import generics, permissions
from rest_framework import status
from rest_framework.response import Response
from rest_framework.reverse import reverse

from collectionjson import services
from plugins.serializers import PluginSerializer

from .models import Pipeline, PipelineFilter, PluginPiping
from .models import DefaultPipingBoolParameter, DefaultPipingStrParameter
from .models import DefaultPipingIntParameter, DefaultPipingFloatParameter
from .serializers import PipelineSerializer, PluginPipingSerializer
from .serializers import DEFAULT_PIPING_PARAMETER_SERIALIZERS
from .serializers import GenericDefaultPipingParameterSerializer
from .permissions import IsChrisOrOwnerOrNotLockedReadOnly, IsChrisOrOwnerOrNotLocked
from .permissions import IsChrisOrOwnerAndLockedOrNotLockedReadOnly


class PipelineList(generics.ListCreateAPIView):
    """
    A view for the collection of pipelines.
    """
    http_method_names = ['get', 'post']
    serializer_class = PipelineSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the pipelines
        that are accessible to the currently authenticated user.
        """
        return Pipeline.get_accesible_pipelines(self.request.user)

    def perform_create(self, serializer):
        """
        Overriden to associate an owner with the pipeline before first saving to the DB.
        """
        serializer.save(owner=self.request.user)

    def list(self, request, *args, **kwargs):
        """
        Overriden to add document-level link relations, query list and a collection+json
        template to the response.
        """
        response = super(PipelineList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('pipeline-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'plugins': reverse('plugin-list', request=request)}
        response = services.append_collection_links(response, links)
        # append write template
        template_data = {'name': "", 'authors': "", 'category': "", 'description': "",
                         'locked': "", 'plugin_tree': "", 'plugin_inst_id': ""}
        return services.append_collection_template(response, template_data)


class PipelineListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of pipelines resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PipelineSerializer
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PipelineFilter

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the pipelines
        that are accessible to the currently authenticated user.
        """
        return Pipeline.get_accesible_pipelines(self.request.user)


class PipelineDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    A pipeline view.
    """
    http_method_names = ['get', 'put', 'delete']
    queryset = Pipeline.objects.all()
    serializer_class = PipelineSerializer
    permission_classes = (permissions.IsAuthenticated, IsChrisOrOwnerOrNotLockedReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(PipelineDetail, self).retrieve(request, *args, **kwargs)
        template_data = {'name': "", 'authors': "", 'category': "", 'description': ""}
        pipeline = self.get_object()
        if pipeline.locked:
            template_data['locked'] = ""
        return services.append_collection_template(response, template_data)

    def update(self, request, *args, **kwargs):
        """
        Overriden to include required parameters if not in the request and delete
        'locked' parameter if the pipeline is not locked.
        """
        pipeline = self.get_object()
        if not pipeline.locked and 'locked' in request.data:
            # this pipeline was made available to the public so it cannot be locked
            del request.data['locked']
        if 'name' not in request.data:
            request.data['name'] = pipeline.name  # name is required in the serializer
        return super(PipelineDetail, self).update(request, *args, **kwargs)

    def destroy(self, request, *args, **kwargs):
        """
        Overriden to check that the pipeline is locked before attempting to delete it.
        """
        pipeline = self.get_object()
        if not pipeline.locked:
            return Response(status=status.HTTP_304_NOT_MODIFIED)
        return super(PipelineDetail, self).destroy(request, *args, **kwargs)


class PipelinePluginList(generics.ListAPIView):
    """
    A view for a pipeline-specific collection of plugins.
    """
    http_method_names = ['get']
    queryset = Pipeline.objects.all()
    serializer_class = PluginSerializer
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerOrNotLockedReadOnly,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the plugins for the queried pipeline.
        Document-level link relations are also added to the response.
        """
        queryset = self.get_plugins_queryset()
        response = services.get_list_response(self, queryset)
        pipeline = self.get_object()
        links = {'pipeline': reverse('pipeline-detail', request=request,
                                   kwargs={"pk": pipeline.id})}
        return services.append_collection_links(response, links)

    def get_plugins_queryset(self):
        """
        Custom method to get the actual plugins queryset for the queried pipeline.
        """
        pipeline = self.get_object()
        return pipeline.plugins.all()


class PipelinePluginPipingList(generics.ListAPIView):
    """
    A view for the collection of pipeline-specific plugin pipings.
    """
    http_method_names = ['get']
    queryset = Pipeline.objects.all()
    serializer_class = PluginPipingSerializer
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerOrNotLockedReadOnly,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the plugin pipings for the queried pipeline.
        Document-level link relations are also added to the response.
        """
        queryset = self.get_plugin_pipings_queryset()
        response = services.get_list_response(self, queryset)
        pipeline = self.get_object()
        links = {'pipeline': reverse('pipeline-detail', request=request,
                                   kwargs={"pk": pipeline.id})}
        return services.append_collection_links(response, links)

    def get_plugin_pipings_queryset(self,):
        """
        Custom method to get the actual plugin pipings queryset for the pipeline.
        """
        pipeline = self.get_object()
        return pipeline.plugin_pipings.all()


class PipelineDefaultParameterList(generics.ListAPIView):
    """
    A view for the collection of pipeline-specific plugin parameters' defaults.
    """
    http_method_names = ['get']
    queryset = Pipeline.objects.all()
    serializer_class = GenericDefaultPipingParameterSerializer
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerOrNotLockedReadOnly)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list with all the default parameter values used by the
        queried pipeline.
        """
        queryset = self.get_default_parameters_queryset()
        response = services.get_list_response(self, queryset)
        return response

    def get_default_parameters_queryset(self):
        """
        Custom method to get a queryset with all the default parameters regardless their
        type.
        """
        pipeline = self.get_object()
        queryset = []
        queryset.extend(list(DefaultPipingStrParameter.objects.filter(
            plugin_piping__pipeline=pipeline)))
        queryset.extend(list(DefaultPipingIntParameter.objects.filter(
            plugin_piping__pipeline=pipeline)))
        queryset.extend(list(DefaultPipingFloatParameter.objects.filter(
            plugin_piping__pipeline=pipeline)))
        queryset.extend(list(DefaultPipingBoolParameter.objects.filter(
            plugin_piping__pipeline=pipeline)))
        return self.filter_queryset(queryset)


class PluginPipingDetail(generics.RetrieveAPIView):
    """
    A plugin piping view.
    """
    http_method_names = ['get']
    queryset = PluginPiping.objects.all()
    serializer_class = PluginPipingSerializer
    permission_classes = (permissions.IsAuthenticated, IsChrisOrOwnerOrNotLocked,)


class DefaultPipingStrParameterDetail(generics.RetrieveUpdateAPIView):
    """
    A view for a string default value for a plugin parameter in a pipeline's
    plugin piping.
    """
    http_method_names = ['get', 'put']
    serializer_class = DEFAULT_PIPING_PARAMETER_SERIALIZERS['string']
    queryset = DefaultPipingStrParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerAndLockedOrNotLockedReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(DefaultPipingStrParameterDetail, self).retrieve(
            request, *args, **kwargs)
        template_data = {"value": ""}
        return services.append_collection_template(response, template_data)


class DefaultPipingIntParameterDetail(generics.RetrieveUpdateAPIView):
    """
    A view for an integer default value for a plugin parameter in a pipeline's
    plugin piping.
    """
    http_method_names = ['get', 'put']
    serializer_class = DEFAULT_PIPING_PARAMETER_SERIALIZERS['integer']
    queryset = DefaultPipingIntParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerAndLockedOrNotLockedReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(DefaultPipingIntParameterDetail, self).retrieve(
            request, *args, **kwargs)
        template_data = {"value": ""}
        return services.append_collection_template(response, template_data)


class DefaultPipingFloatParameterDetail(generics.RetrieveUpdateAPIView):
    """
    A view for a float default value for a plugin parameter in a pipeline's
    plugin piping.
    """
    http_method_names = ['get', 'put']
    serializer_class = DEFAULT_PIPING_PARAMETER_SERIALIZERS['float']
    queryset = DefaultPipingFloatParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerAndLockedOrNotLockedReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(DefaultPipingFloatParameterDetail, self).retrieve(
            request, *args, **kwargs)
        template_data = {"value": ""}
        return services.append_collection_template(response, template_data)


class DefaultPipingBoolParameterDetail(generics.RetrieveUpdateAPIView):
    """
    A view for a boolean default value for a plugin parameter in a pipeline's
    plugin piping.
    """
    http_method_names = ['get', 'put']
    serializer_class = DEFAULT_PIPING_PARAMETER_SERIALIZERS['boolean']
    queryset = DefaultPipingBoolParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,
                          IsChrisOrOwnerAndLockedOrNotLockedReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(DefaultPipingBoolParameterDetail, self).retrieve(
            request, *args, **kwargs)
        template_data = {"value": ""}
        return services.append_collection_template(response, template_data)


# chris_backend/plugininstances/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/plugininstances/apps.py

from django.apps import AppConfig


class PlugininstancesConfig(AppConfig):
    name = 'plugininstances'


# chris_backend/plugininstances/models.py


import logging

from django.db import models
from django.conf import settings

import django_filters
from django_filters.rest_framework import FilterSet

from core.utils import filter_files_by_n_slashes
from feeds.models import Feed
from plugins.models import ComputeResource, Plugin, PluginParameter
from plugins.fields import CPUField, MemoryField
from plugins.fields import MemoryInt, CPUInt
from pipelineinstances.models import PipelineInstance

if settings.DEBUG:
    import pdb, pudb


logger = logging.getLogger(__name__)


STATUS_CHOICES = [("created",               "Default initial"),
                  ("waiting",               "Waiting to be scheduled"),
                  ("scheduled",             "Scheduled on worker"),
                  ("started",               "Started on compute env"),
                  ("registeringFiles",      "Registering output files"),
                  ("finishedSuccessfully",  "Finished successfully"),
                  ("finishedWithError",     "Finished with error"),
                  ("cancelled",             "Cancelled")]


class PluginInstance(models.Model):
    title = models.CharField(max_length=100, blank=True)
    start_date = models.DateTimeField(auto_now_add=True)
    end_date = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=30, choices=STATUS_CHOICES, default='created')
    summary = models.CharField(max_length=4000, blank=True)
    raw = models.TextField(blank=True)
    size = models.BigIntegerField(default=0)
    error_code = models.CharField(max_length=7, blank=True)
    previous = models.ForeignKey("self", on_delete=models.CASCADE, null=True,
                                 related_name='next')
    plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE, related_name='instances')
    feed = models.ForeignKey(Feed, on_delete=models.CASCADE,
                             related_name='plugin_instances')
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    compute_resource = models.ForeignKey(ComputeResource, null=True,
                                         on_delete=models.SET_NULL,
                                         related_name='plugin_instances')
    pipeline_inst = models.ForeignKey(PipelineInstance, null=True,
                                      on_delete=models.SET_NULL,
                                      related_name='plugin_instances')
    cpu_limit = CPUField(null=True)
    memory_limit = MemoryField(null=True)
    number_of_workers = models.IntegerField(null=True)
    gpu_limit = models.IntegerField(null=True)

    class Meta:
        ordering = ('-start_date',)

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        """
        Overriden to save a new feed to the DB the first time 'fs' instances are saved.
        For 'ds' and 'ts' instances the feed of the previous instance is assigned.
        """
        if not hasattr(self, 'feed'):
            plugin_type = self.plugin.meta.type
            if plugin_type == 'fs':
                self.feed = self._save_feed()
            elif plugin_type in ('ds', 'ts'):
                self.feed = self.previous.feed
        self._set_compute_defaults()
        super(PluginInstance, self).save(*args, **kwargs)

    def _save_feed(self):
        """
        Custom internal method to create and save a new feed to the DB.
        """
        feed = Feed()
        feed.name = self.title or self.plugin.meta.name
        feed.save()
        feed.owner.set([self.owner])
        feed.save()
        return feed

    def _set_compute_defaults(self):
        """
        Custom internal method to set compute-related defaults.
        """
        if not self.cpu_limit:
            self.cpu_limit = CPUInt(self.plugin.min_cpu_limit)
        if not self.memory_limit:
            self.memory_limit = MemoryInt(self.plugin.min_memory_limit)
        if not self.number_of_workers:
            self.number_of_workers = self.plugin.min_number_of_workers
        if not self.gpu_limit:
            self.gpu_limit = self.plugin.min_gpu_limit

    def get_root_instance(self):
        """
        Custom method to return the root plugin instance for this plugin instance.
        """
        current = self
        while not current.plugin.meta.type == 'fs':
            current = current.previous
        return current

    def get_descendant_instances(self):
        """
        Custom method to return all the plugin instances that are a descendant of this
        plugin instance.
        """
        descendant_instances = []
        queue = [self]
        while len(queue) > 0:
            visited = queue.pop()
            queue.extend(list(visited.next.all()))
            descendant_instances.append(visited)
        return descendant_instances

    def get_output_path(self):
        """
        Custom method to get the output directory for files generated by
        the plugin instance object.
        """
        # 'fs' plugins will output files to:
        # SWIFT_CONTAINER_NAME/<username>/feed_<id>/plugin_name_plugin_inst_<id>/data
        # 'ds' and 'ts' plugins will output files to:
        # SWIFT_CONTAINER_NAME/<username>/feed_<id>/...
        #/previous_plugin_name_plugin_inst_<id>/plugin_name_plugin_inst_<id>/data
        current = self
        path = '/{0}_{1}/data'.format(current.plugin.meta.name, current.id)
        while not current.plugin.meta.type == 'fs':
            current = current.previous
            path = '/{0}_{1}'.format(current.plugin.meta.name, current.id) + path
        username = self.owner.username
        output_path = '{0}/feed_{1}'.format(username, current.feed.id) + path
        return output_path

    def get_parameter_instances(self):
        """
        Custom method to get all the parameter instances associated with this plugin
        instance regardless of their type.
        """
        parameter_instances = []
        parameter_instances.extend(list(self.unextpath_param.all()))
        parameter_instances.extend(list(self.path_param.all()))
        parameter_instances.extend(list(self.string_param.all()))
        parameter_instances.extend(list(self.integer_param.all()))
        parameter_instances.extend(list(self.float_param.all()))
        parameter_instances.extend(list(self.boolean_param.all()))
        return parameter_instances


class PluginInstanceFilter(FilterSet):
    min_start_date = django_filters.IsoDateTimeFilter(field_name='start_date',
                                                      lookup_expr='gte')
    max_start_date = django_filters.IsoDateTimeFilter(field_name='start_date',
                                                      lookup_expr='lte')
    min_end_date = django_filters.IsoDateTimeFilter(field_name='end_date',
                                                    lookup_expr='gte')
    max_end_date = django_filters.IsoDateTimeFilter(field_name='end_date',
                                                    lookup_expr='lte')
    title = django_filters.CharFilter(field_name='title', lookup_expr='icontains')
    owner_username = django_filters.CharFilter(field_name='owner__username',
                                               lookup_expr='exact')
    feed_id = django_filters.CharFilter(field_name='feed_id', lookup_expr='exact')
    root_id = django_filters.CharFilter(method='filter_by_root_id')
    plugin_id = django_filters.CharFilter(field_name='plugin_id', lookup_expr='exact')
    pipeline_inst_id = django_filters.CharFilter(field_name='pipeline_inst_id',
                                                 lookup_expr='exact')
    plugin_name = django_filters.CharFilter(field_name='plugin__meta__name',
                                            lookup_expr='icontains')
    plugin_name_exact = django_filters.CharFilter(field_name='plugin__meta__name',
                                                  lookup_expr='exact')
    plugin_version = django_filters.CharFilter(field_name='plugin__version',
                                               lookup_expr='exact')

    class Meta:
        model = PluginInstance
        fields = ['id', 'min_start_date', 'max_start_date', 'min_end_date',
                  'max_end_date', 'root_id', 'title', 'status', 'owner_username',
                  'feed_id', 'plugin_id', 'plugin_name', 'plugin_name_exact',
                  'plugin_version', 'pipeline_inst_id']

    def filter_by_root_id(self, queryset, name, value):
        """
        Custom method to return the plugin instances in a queryset with a common root
        plugin instance.
        """
        filtered_queryset = []
        root_queryset = queryset.filter(pk=value)
        # check whether the root id value is in the DB
        if not root_queryset.exists():
            return root_queryset
        queue = [root_queryset[0]]
        while len(queue) > 0:
            visited = queue.pop()
            queue.extend(list(visited.next.all()))
            filtered_queryset.append(visited)
        return filtered_queryset


class PluginInstanceLock(models.Model):
    plugin_inst = models.OneToOneField(PluginInstance, on_delete=models.CASCADE,
                                       related_name='lock')

    def __str__(self):
        return self.plugin_inst.id


class PluginInstanceSplit(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    filter = models.CharField(max_length=600, blank=True)
    created_plugin_inst_ids = models.CharField(max_length=600)
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='splits')

    class Meta:
        ordering = ('plugin_inst', '-creation_date',)

    def __str__(self):
        return self.created_plugin_inst_ids


class PluginInstanceFile(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    fname = models.FileField(max_length=1024, unique=True)
    plugin_inst = models.ForeignKey(PluginInstance, db_index=True,
                                    on_delete=models.CASCADE, related_name='files')

    class Meta:
        ordering = ('-fname',)

    def __str__(self):
        return self.fname.name


class PluginInstanceFileFilter(FilterSet):
    min_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='lte')
    plugin_inst_id = django_filters.CharFilter(field_name='plugin_inst_id',
                                               lookup_expr='exact')
    feed_id = django_filters.CharFilter(field_name='plugin_inst__feed_id',
                                               lookup_expr='exact')
    fname = django_filters.CharFilter(field_name='fname', lookup_expr='startswith')
    fname_exact = django_filters.CharFilter(field_name='fname', lookup_expr='exact')
    fname_icontains = django_filters.CharFilter(field_name='fname',
                                                lookup_expr='icontains')
    fname_nslashes = django_filters.CharFilter(method='filter_by_n_slashes')

    class Meta:
        model = PluginInstanceFile
        fields = ['id', 'min_creation_date', 'max_creation_date', 'plugin_inst_id',
                  'feed_id', 'fname', 'fname_exact', 'fname_icontains', 'fname_nslashes']

    def filter_by_n_slashes(self, queryset, name, value):
        """
        Custom method to return the files that have the queried number of slashes in
        their fname property. If the queried number ends in 'u' or 'U' then only one
        file per each last "folder" in the path is returned (useful to efficiently get
        the list of immediate folders under the path).
        """
        return filter_files_by_n_slashes(queryset, value)


class StrParameter(models.Model):
    value = models.CharField(max_length=600, blank=True)
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='string_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='string_inst')

    class Meta:
        unique_together = ('plugin_inst', 'plugin_param',)

    def __str__(self):
        return self.value


class IntParameter(models.Model):
    value = models.IntegerField()
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='integer_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='integer_inst')

    class Meta:
        unique_together = ('plugin_inst', 'plugin_param',)

    def __str__(self):
        return str(self.value)


class FloatParameter(models.Model):
    value = models.FloatField()
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='float_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='float_inst')

    class Meta:
        unique_together = ('plugin_inst', 'plugin_param',)

    def __str__(self):
        return str(self.value)


class BoolParameter(models.Model):
    value = models.BooleanField()
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='boolean_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='boolean_inst')

    class Meta:
        unique_together = ('plugin_inst', 'plugin_param',)

    def __str__(self):
        return str(self.value)


class PathParameter(models.Model):
    value = models.CharField(max_length=16000)  # this string can be a list of long paths
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='path_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='path_inst')

    class Meta:
        unique_together = ('plugin_inst', 'plugin_param',)

    def __str__(self):
        return self.value


class UnextpathParameter(models.Model):
    value = models.CharField(max_length=16000)  # this string can be a list of long paths
    plugin_inst = models.ForeignKey(PluginInstance, on_delete=models.CASCADE,
                                    related_name='unextpath_param')
    plugin_param = models.ForeignKey(PluginParameter, on_delete=models.CASCADE,
                                     related_name='unextpath_inst')

    class Meta:
        unique_together = ('plugin_inst', 'plugin_param',)

    def __str__(self):
        return self.value


# chris_backend/plugininstances/permissions.py

from rest_framework import permissions


class IsRelatedFeedOwnerOrChris(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to access a feed-related object (eg. a plugin instance).
    """

    def has_object_permission(self, request, view, obj):
        # Access is only allowed to related feed owners and superuser 'chris'.
        #import pdb; pdb.set_trace()
        if request.user.username == 'chris':
            return True
        if hasattr(obj, 'plugin_inst'):
            related_feed = obj.plugin_inst.feed
        else:
            related_feed = obj.feed
        if request.user in related_feed.owner.all():
            return True
        return False


class IsOwnerOrChrisOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it. Read only is allowed to other users.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner and superuser 'chris'.
        return (request.user == obj.owner) or (request.user.username == 'chris')


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object modify/edit it.
    Read only is allowed to other users.
    """

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner and superuser 'chris'.
        return request.user == obj.owner


# chris_backend/plugininstances/serializers.py


import logging
import pathlib

from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from rest_framework import serializers
from rest_framework.reverse import reverse

from collectionjson.fields import ItemLinkField
from core.utils import get_file_resource_link
from core.swiftmanager import SwiftManager
from plugins.models import TYPES, Plugin
from feeds.models import Feed

from .models import PluginInstance, PluginInstanceSplit, PluginInstanceFile
from .models import FloatParameter, IntParameter, BoolParameter
from .models import PathParameter, UnextpathParameter, StrParameter


logger = logging.getLogger(__name__)


class PluginInstanceSerializer(serializers.HyperlinkedModelSerializer):
    compute_resource_name = serializers.CharField(max_length=100, required=False,
                                                  source='compute_resource.name')
    previous_id = serializers.ReadOnlyField(source='previous.id')
    plugin_id = serializers.ReadOnlyField(source='plugin.id')
    plugin_name = serializers.ReadOnlyField(source='plugin.meta.name')
    plugin_version = serializers.ReadOnlyField(source='plugin.version')
    plugin_type = serializers.ReadOnlyField(source='plugin.meta.type')
    pipeline_id = serializers.ReadOnlyField(source='pipeline_inst.pipeline.id')
    pipeline_name = serializers.ReadOnlyField(source='pipeline_inst.pipeline.name')
    pipeline_inst_id = serializers.ReadOnlyField(source='pipeline_inst.id')
    feed_id = serializers.ReadOnlyField(source='feed.id')
    output_path = serializers.SerializerMethodField()
    summary = serializers.ReadOnlyField()
    raw = serializers.ReadOnlyField()
    owner_username = serializers.ReadOnlyField(source='owner.username')
    size = serializers.ReadOnlyField()
    error_code = serializers.ReadOnlyField()
    previous = serializers.HyperlinkedRelatedField(
        view_name='plugininstance-detail', read_only=True
    )
    descendants = serializers.HyperlinkedIdentityField(
        view_name='plugininstance-descendant-list'
    )
    parameters = serializers.HyperlinkedIdentityField(
        view_name='plugininstance-parameter-list'
    )
    files = serializers.HyperlinkedIdentityField(view_name='plugininstancefile-list')
    plugin = serializers.HyperlinkedRelatedField(
        view_name='plugin-detail', read_only=True
    )
    pipeline_inst = serializers.HyperlinkedRelatedField(
        view_name='pipelineinstance-detail', read_only=True
    )
    feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True)
    compute_resource = serializers.HyperlinkedRelatedField(
        view_name='computeresource-detail', read_only=True
    )
    splits = serializers.HyperlinkedIdentityField(view_name='plugininstancesplit-list')

    class Meta:
        model = PluginInstance
        fields = ('url', 'id', 'title', 'previous_id', 'compute_resource_name',
                  'plugin_id', 'plugin_name', 'plugin_version', 'plugin_type',
                  'pipeline_id', 'pipeline_name', 'pipeline_inst_id', 'pipeline_inst',
                  'feed_id', 'start_date', 'end_date', 'output_path', 'status',
                  'summary', 'raw', 'owner_username', 'cpu_limit', 'memory_limit',
                  'number_of_workers', 'gpu_limit', 'size', 'error_code', 'previous',
                  'feed', 'plugin', 'descendants', 'files', 'parameters',
                  'compute_resource', 'splits')

    def get_output_path(self, obj):
        """
        Overriden to get the plugin instance's output path.
        """
        return obj.get_output_path()

    def validate_previous(self, previous_id):
        """
        Custom method to check that an id is provided for previous instance when
        corresponding plugin is of type 'ds' or 'ts'. Then check that the provided id
        exists in the DB and that the user can run plugins within this feed.
        """
        # using self.context['view'] in validators prevents calling is_valid when creating
        # a new serializer instance outside the Django view framework. But here is fine
        # as plugin instances are always created through the API
        plugin = self.context['view'].get_object()
        previous = None
        if plugin.meta.type in ('ds', 'ts'):
            if not previous_id:
                raise serializers.ValidationError(
                    {'previous_id': ["This field is required."]})
            try:
                pk = int(previous_id)
                previous = PluginInstance.objects.get(pk=pk)
            except (ValueError, ObjectDoesNotExist):
                err_str = "Couldn't find any 'previous' plugin instance with id %s."
                raise serializers.ValidationError(
                    {'previous_id': [err_str % previous_id]})
            # check that the user can run plugins within this feed
            user = self.context['request'].user
            if user not in previous.feed.owner.all():
                err_str = "User is not an owner of feed for previous instance with id %s."
                raise serializers.ValidationError(
                    {'previous_id': [err_str % previous_id]})
        return previous

    def validate_compute_resource_name(self, compute_resource_name):
        """
        Overriden to check the provided compute resource name is registered with the
        corresponding plugin.
        """
        plg = self.context['view'].get_object()
        if plg.compute_resources.filter(name=compute_resource_name).count() == 0:
            msg = "Plugin '%s' with version '%s' has not been registered with compute " \
                  "resource '%s'." % (plg.meta.name, plg.version, compute_resource_name)
            raise serializers.ValidationError([msg])
        return compute_resource_name

    def validate_status(self, status):
        """
        Overriden to validate a change of status.
        """
        instance = self.instance
        if instance and (status != 'cancelled' or
                         instance.status in ('finishedSuccessfully',
                                             'finishedWithError')):
            msg = "Can not change status from '%s' to '%s'."
            raise serializers.ValidationError([msg % (instance.status, status)])
        return status

    def validate_gpu_limit(self, gpu_limit):
        """
        Overriden to validate gpu_limit is within the proper limits.
        """
        plugin = self.context['view'].get_object()
        self.validate_value_within_interval(gpu_limit,
                                            plugin.min_gpu_limit,
                                            plugin.max_gpu_limit)
        return gpu_limit

    def validate_number_of_workers(self, number_of_workers):
        """
        Overriden to validate number_of_workers is within the proper limits.
        """
        plugin = self.context['view'].get_object()
        self.validate_value_within_interval(number_of_workers,
                                            plugin.min_number_of_workers,
                                            plugin.max_number_of_workers)
        return number_of_workers

    def validate_cpu_limit(self, cpu_limit):
        """
        Overriden to validate cpu_limit is within the proper limits.
        """
        plugin = self.context['view'].get_object()
        self.validate_value_within_interval(cpu_limit,
                                            plugin.min_cpu_limit,
                                            plugin.max_cpu_limit)
        return cpu_limit

    def validate_memory_limit(self, memory_limit):
        """
        Overriden to validate memory_limit is within the proper limits.
        """
        plugin = self.context['view'].get_object()
        self.validate_value_within_interval(memory_limit,
                                            plugin.min_memory_limit,
                                            plugin.max_memory_limit)
        return memory_limit

    @staticmethod
    def validate_value_within_interval(val, min_val, max_val):
        if val < min_val or val > max_val:
            raise serializers.ValidationError(["This field value is out of range."])


class PluginInstanceSplitSerializer(serializers.HyperlinkedModelSerializer):
    compute_resource_name = serializers.CharField(max_length=100, write_only=True,
                                                  required=False)
    created_plugin_inst_ids = serializers.ReadOnlyField()
    plugin_inst_id = serializers.ReadOnlyField(source='plugin_inst.id')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)

    class Meta:
        model = PluginInstanceSplit
        fields = ('url', 'id', 'creation_date', 'filter', 'plugin_inst_id',
                  'created_plugin_inst_ids', 'compute_resource_name', 'plugin_inst')

    def validate_filter(self, filter_value):
        """
        Overriden to check that the provided filter is a string of regular expressions
        separated by commas).
        """
        if filter_value:
            regexs = [r.strip() for r in filter_value.split(',')]
            filter_value = ','.join(regexs)
        return filter_value

    def validate_compute_resource_name(self, compute_resource_name):
        """
        Overriden to check the provided compute resource name is registered with
        pl-topologicalplugin.
        """
        plg_topologcopy = Plugin.objects.filter(meta__name='pl-topologicalcopy').first()
        if not plg_topologcopy:
            raise serializers.ValidationError([f"Could not find plugin "
                                               f"'pl-topologicalcopy'. Please contact "
                                               f"your ChRIS admin."])
        if plg_topologcopy.compute_resources.filter(
                name=compute_resource_name).count() == 0:
            raise serializers.ValidationError([f"Plugin 'pl-topologicalcopy' with version"
                                               f" '{plg_topologcopy.version}' has not"
                                               f" been registered with compute resource"
                                               f" '{compute_resource_name}'."])
        return compute_resource_name


class PluginInstanceFileSerializer(serializers.HyperlinkedModelSerializer):
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    file_resource = ItemLinkField('get_file_link')
    fname = serializers.FileField(use_url=False)
    fsize = serializers.ReadOnlyField(source='fname.size')
    feed_id = serializers.ReadOnlyField(source='plugin_inst.feed.id')
    plugin_inst_id = serializers.ReadOnlyField(source='plugin_inst.id')

    class Meta:
        model = PluginInstanceFile
        fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'feed_id',
                  'plugin_inst_id', 'file_resource', 'plugin_inst')

    def get_file_link(self, obj):
        """
        Custom method to get the hyperlink to the actual file resource.
        """
        return get_file_resource_link(self, obj)


class StrParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = StrParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')

    def __init__(self, *args, **kwargs):
        """
        Overriden to get the plugin parameter as a keyword argument at object creation.
        """
        self.param_name = kwargs.pop('param_name', None)
        self.plugin_type = kwargs.pop('plugin_type', None)
        self.previous = kwargs.pop('previous', None)
        super(StrParameterSerializer, self).__init__(*args, **kwargs)

    def validate_value(self, value):
        """
        Overriden to check that all the provided plugin instance ids exist in the DB and
        belong to the same feed for a 'plugininstances' parameter in a 'ts' plugin
        (value should be a string of plugin instance ids separated by commas).
        """
        if value and self.param_name and self.plugin_type and self.previous:
            if (self.param_name == 'plugininstances') and (self.plugin_type == 'ts'):
                plg_inst_ids = [inst_id.strip() for inst_id in value.split(',')]
                if str(self.previous.id) not in plg_inst_ids:
                    raise serializers.ValidationError(
                        [f"Previous instance id '{self.previous.id}' must be included in "
                         f"the 'plugininstances' list for a non-empty list."])
                for inst_id in plg_inst_ids:
                    try:
                        plg_inst = PluginInstance.objects.get(pk=int(inst_id))
                    except (ValueError, ObjectDoesNotExist):
                        raise serializers.ValidationError(
                            [f"Couldn't find any plugin instance with id '{inst_id}'."])
                    if plg_inst.feed.id != self.previous.feed.id:
                        raise serializers.ValidationError(
                            [f"Plugin instance with id '{inst_id}' is not in this feed."])
                value = ','.join(plg_inst_ids)
            elif self.param_name == 'filter' and self.plugin_type == 'ts':
                regexs = [r.strip() for r in value.split(',')]
                value = ','.join(regexs)
        return value


class IntParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = IntParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')


class FloatParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = FloatParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')


class BoolParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = BoolParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')


def validate_paths(user, string):
    """
    Custom function to check that a user is allowed to access the provided object storage
    paths.
    """
    swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                 settings.SWIFT_CONNECTION_PARAMS)
    path_list = [s.strip() for s in string.split(',')]
    for path in path_list:
        path_parts = pathlib.Path(path).parts
        if len(path_parts) == 0:
            # trying to access the root of the storage
            raise serializers.ValidationError(
                ["You do not have permission to access this path."])
        if path_parts[0] != user.username and path_parts[0] != 'SERVICES':
            if len(path_parts) == 1 or path_parts[1] == 'uploads':
                # trying to access another user's root or personal space
                raise serializers.ValidationError(
                    ["You do not have permission to access this path."])
            try:
                # file paths should be of the form <username>/feed_<id>/..
                feed_id = path_parts[1].split('_')[-1]
                feed = Feed.objects.get(pk=feed_id)
            except (ValueError, Feed.DoesNotExist):
                raise serializers.ValidationError(
                    ["This field may not be an invalid path."])
            if user not in feed.owner.all():
                raise serializers.ValidationError(
                    ["You do not have permission to access this path."])
        else:
            # check whether path exists in swift
            try:
                path_exists = swift_manager.path_exists(path)
            except Exception as e:
                logger.error('Swift storage error, detail: %s' % str(e))
                raise serializers.ValidationError(
                    ["Could not validate this path."])
            if not path_exists:
                raise serializers.ValidationError(
                    ["This field may not be an invalid path."])
    return ','.join(path_list)


class PathParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = PathParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')

    def __init__(self, *args, **kwargs):
        """
        Overriden to get the request user as a keyword argument at object creation.
        """
        self.user = kwargs.pop('user')
        super(PathParameterSerializer, self).__init__(*args, **kwargs)

    def validate_value(self, value):
        """
        Overriden to check that the user making the request is allowed to access
        the provided object storage paths (value should be a string of paths separated
        by commas).
        """
        return validate_paths(self.user, value)


class UnextpathParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = UnextpathParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')

    def __init__(self, *args, **kwargs):
        """
        Overriden to get the request user as a keyword argument at object creation.
        """
        self.user = kwargs.pop('user')
        super(UnextpathParameterSerializer, self).__init__(*args, **kwargs)

    def validate_value(self, value):
        """
        Overriden to check that the user making the request is allowed to access
        the provided object storage paths (value should be a string of paths separated
        by commas).
        """
        return validate_paths(self.user, value)


class GenericParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    value = serializers.SerializerMethodField()
    url = ItemLinkField('_get_url')
    plugin_inst = serializers.HyperlinkedRelatedField(view_name='plugininstance-detail',
                                                      read_only=True)
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = StrParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_inst',
                  'plugin_param')

    def _get_url(self, obj):
        """
        Custom method to get the correct url for the serialized object regardless of
        its type.
        """
        request = self.context['request']
        # here parameter detail view names are assumed to follow a convention
        view_name = TYPES[obj.plugin_param.type] + 'parameter-detail'
        return reverse(view_name, request=request, kwargs={"pk": obj.id})

    def get_value(self, obj):
        """
        Overriden to get the default parameter value regardless of its type.
        """
        return obj.value


PARAMETER_SERIALIZERS = {'string': StrParameterSerializer,
                         'integer': IntParameterSerializer,
                         'float': FloatParameterSerializer,
                         'boolean': BoolParameterSerializer,
                         'path': PathParameterSerializer,
                         'unextpath': UnextpathParameterSerializer}


# chris_backend/plugininstances/tasks.py


import logging
from functools import wraps

from django.db.models import Q

from celery import shared_task

from .models import PluginInstance
from .services.manager import PluginInstanceManager


logger = logging.getLogger(__name__)


def skip_if_running(f):
    """
    This decorator is supposed to ensure that a task is only running once across all
    workers.
    https://stackoverflow.com/questions/20894771/celery-beat-limit-to-single-task-instance-at-a-time
    """
    task_name = f'{f.__module__}.{f.__name__}'
    @wraps(f)
    def wrapped(self, *args, **kwargs):
        workers = self.app.control.inspect().active()
        if workers is None:
            logger.info('could not find worker for task %s (%s, %s), skipping',
                        task_name, args, kwargs)
            return None
        for worker, tasks in workers.items():
            for task in tasks:
                if (task_name == task['name'] and tuple(args) == tuple(task['args']) and
                        kwargs == task['kwargs'] and self.request.id != task['id']):
                    logger.info('task %s (%s, %s) is running on %s, skipping',
                                task_name, args, kwargs, worker)
                    return None
        return f(self, *args, **kwargs)
    return wrapped


@shared_task
def run_plugin_instance(plg_inst_id):
    """
    Run the app corresponding to this plugin instance.
    """
    #from celery.contrib import rdb;rdb.set_trace()
    plugin_inst = PluginInstance.objects.get(pk=plg_inst_id)
    plg_inst_manager = PluginInstanceManager(plugin_inst)
    plg_inst_manager.run_plugin_instance_app()


@shared_task
def check_plugin_instance_exec_status(plg_inst_id):
    """
    Check the execution status of the app corresponding to this plugin instance.
    """
    plugin_inst = PluginInstance.objects.get(pk=plg_inst_id)
    plg_inst_manager = PluginInstanceManager(plugin_inst)
    plg_inst_manager.check_plugin_instance_app_exec_status()


@shared_task
def cancel_plugin_instance(plg_inst_id):
    """
    Cancel the execution of the app corresponding to this plugin instance.
    """
    plugin_inst = PluginInstance.objects.get(pk=plg_inst_id)
    plg_inst_manager = PluginInstanceManager(plugin_inst)
    plg_inst_manager.cancel_plugin_instance_app_exec()


@shared_task(bind=True)
@skip_if_running
def schedule_waiting_plugin_instances(self):  # task is passed info about itself
    """
    Schedule the apps corresponding to all plugin instances in 'waiting' DB status
    and whom previous plugin instance is in 'finishedSuccessfully' DB status.
    """
    instances = PluginInstance.objects.filter(status='waiting',
                                              previous__status='finishedSuccessfully')
    for plg_inst in instances:
        plg_inst.status = 'scheduled'
        plg_inst.save()
        run_plugin_instance.delay(plg_inst.id)  # call async task


@shared_task
def check_started_plugin_instances_exec_status():
    """
    Check the execution status of the apps corresponding to all the plugin instances
    with 'started' DB status.
    """
    instances = PluginInstance.objects.filter(status='started')
    for plg_inst in instances:
        check_plugin_instance_exec_status.delay(plg_inst.id)  # call async task


@shared_task
def cancel_waiting_plugin_instances():
    """
    Cancel the apps corresponding to all plugin instances in 'waiting' DB
    status when their previous plugin instance is in either 'finishedWithError' or
    'cancelled' DB status.
    """
    lookup = Q(previous__status='finishedWithError') | Q(previous__status='cancelled')
    PluginInstance.objects.filter(
        status='waiting'
    ).filter(lookup).update(status='cancelled')


@shared_task  # toy task for testing celery stuff
def sum(x, y):
    return x + y


# chris_backend/plugininstances/views.py


from django.http import FileResponse
from rest_framework import generics
from rest_framework import permissions
from rest_framework.reverse import reverse
from rest_framework.serializers import ValidationError

from collectionjson import services
from core.renderers import BinaryFileRenderer
from plugins.models import Plugin

from .models import PluginInstance, PluginInstanceFilter, PluginInstanceSplit
from .models import PluginInstanceFile, PluginInstanceFileFilter
from .models import StrParameter, FloatParameter, IntParameter
from .models import BoolParameter, PathParameter, UnextpathParameter
from .serializers import PARAMETER_SERIALIZERS
from .serializers import GenericParameterSerializer, PluginInstanceSplitSerializer
from .serializers import PluginInstanceSerializer, PluginInstanceFileSerializer
from .permissions import (IsRelatedFeedOwnerOrChris, IsOwnerOrChrisOrReadOnly,
                          IsOwnerOrReadOnly)
from .tasks import run_plugin_instance, cancel_plugin_instance


class PluginInstanceList(generics.ListCreateAPIView):
    """
    A view for the collection of plugin instances.
    """
    http_method_names = ['get', 'post']
    serializer_class = PluginInstanceSerializer
    queryset = Plugin.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def create(self, request, *args, **kwargs):
        """
        Overriden to remove descriptors from the request that must take their default
        value on creation.
        """
        self.request.data.pop('status', None)
        return super(PluginInstanceList, self).create(request, *args, **kwargs)

    def perform_create(self, serializer):
        """
        Overriden to associate an owner, a plugin and a previous plugin instance with 
        the newly created plugin instance before first saving to the DB. All the plugin 
        instance's parameters in the request are also properly saved to the DB. Finally
        the plugin's app is run with the provided plugin instance's parameters.
        """
        user = self.request.user
        # get previous plugin instance and create the new plugin instance
        request_data = serializer.context['request'].data
        previous_id = request_data.get('previous_id')
        previous = serializer.validate_previous(previous_id)
        plugin = self.get_object()

        # collect and validate parameters from the request
        parameter_serializers = []
        for parameter in plugin.parameters.all():
            if parameter.name in request_data:
                data = {'value': request_data[parameter.name]}
                param_type = parameter.type
                if param_type in ('path', 'unextpath'):
                    # these serializers need the user to be passed
                    parameter_serializer = PARAMETER_SERIALIZERS[param_type](
                        data=data, user=user)
                elif param_type == 'string' and plugin.meta.type == 'ts':
                    # these serializers need the param's name, plugin type and previous
                    parameter_serializer = PARAMETER_SERIALIZERS[param_type](
                        data=data, param_name=parameter.name, plugin_type='ts',
                        previous=previous)
                else:
                    parameter_serializer = PARAMETER_SERIALIZERS[param_type](data=data)
                parameter_serializer.is_valid(raise_exception=True)
                parameter_serializers.append((parameter, parameter_serializer))
            elif not parameter.optional:
                raise ValidationError({parameter.name: ["This field is required."]})

        # if no validation errors at this point then save to the DB
        cr_data = serializer.validated_data.get('compute_resource')
        if cr_data:
            compute_resource = plugin.compute_resources.get(name=cr_data['name'])
        else:
            compute_resource = plugin.compute_resources.first()
        plg_inst = serializer.save(owner=user, plugin=plugin, previous=previous,
                                   compute_resource=compute_resource)
        for param, param_serializer in parameter_serializers:
            param_serializer.save(plugin_inst=plg_inst, plugin_param=param)

        if previous is None or previous.status == 'finishedSuccessfully':
            # schedule the plugin's app to run
            plg_inst.status = 'scheduled'   # status changes to 'scheduled' right away
            plg_inst.save()
            run_plugin_instance.delay(plg_inst.id)  # call async task
        elif previous.status in ('created', 'waiting', 'scheduled',
                                 'registeringFiles', 'started'):
            plg_inst.status = 'waiting'
            plg_inst.save()
        elif previous.status in ('finishedWithError', 'cancelled'):
            plg_inst.status = 'cancelled'
            plg_inst.save()

    def list(self, request, *args, **kwargs):
        """
        Overriden to return the list of instances for the queried plugin. A document-level
        link relation and a collection+json template are also added to the response.
        """
        queryset = self.get_plugin_instances_queryset()
        response = services.get_list_response(self, queryset)
        plugin = self.get_object()
        # append document-level link relations
        links = {'plugin': reverse('plugin-detail', request=request,
                                   kwargs={"pk": plugin.id}),
                 'compute_resources': reverse('plugin-computeresource-list',
                                              request=request, kwargs={"pk": plugin.id})
                 }
        response = services.append_collection_links(response, links)
        # append write template
        param_names = plugin.get_plugin_parameter_names()
        template_data = {'title': '', 'compute_resource_name': '', 'previous_id': '',
                         'cpu_limit': '', 'memory_limit': '', 'number_of_workers': '',
                         'gpu_limit': ''}
        for name in param_names:
            template_data[name] = ''
        return services.append_collection_template(response, template_data)

    def get_plugin_instances_queryset(self):
        """
        Custom method to get the actual plugin instances' queryset.
        """
        plugin = self.get_object()
        return self.filter_queryset(plugin.instances.all())


class AllPluginInstanceList(generics.ListAPIView):
    """
    A view for the collection of all plugin instances.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to add a query list and document-level link relation to the response.
        """
        response = super(AllPluginInstanceList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('allplugininstance-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'plugins': reverse('plugin-list', request=request)}
        return services.append_collection_links(response, links)


class AllPluginInstanceListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of plugin instances resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PluginInstanceFilter

        
class PluginInstanceDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    A plugin instance view.
    """
    http_method_names = ['get', 'put', 'delete']
    serializer_class = PluginInstanceSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChrisOrReadOnly,)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to add a collection+json template to the response.
        """
        response = super(PluginInstanceDetail, self).retrieve(request, *args, **kwargs)
        template_data = {'title': '', 'status': ''}
        return services.append_collection_template(response, template_data)

    def update(self, request, *args, **kwargs):
        """
        Overriden to remove descriptors that are not allowed to be updated before
        serializer validation.
        """
        data = self.request.data
        data.pop('gpu_limit', None)
        data.pop('number_of_workers', None)
        data.pop('cpu_limit', None)
        data.pop('memory_limit', None)
        return super(PluginInstanceDetail, self).update(request, *args, **kwargs)

    def perform_update(self, serializer):
        """
        Overriden to cancel this plugin instance and all its descendants' execution.
        """
        if 'status' in self.request.data:
            instance = self.get_object()
            if instance.status != 'cancelled':
                descendants = instance.get_descendant_instances()
                if instance.status == 'started':
                    cancel_plugin_instance.delay(instance.id)  # call async task
                for plg_inst in descendants:
                    plg_inst.status = 'cancelled'
                    plg_inst.save()

        super(PluginInstanceDetail, self).perform_update(serializer)

    def destroy(self, request, *args, **kwargs):
        """
        Overriden to cancel the plugin instance execution before deleting it. All the
        descendant instances are also cancelled before they are deleted by the DB CASCADE.
        """
        instance = self.get_object()
        descendants = instance.get_descendant_instances()
        if instance.status == 'started':
            cancel_plugin_instance(instance.id)
        for plg_inst in descendants:
            if plg_inst.status not in ('finishedSuccessfully', 'finishedWithError',
                                       'cancelled'):
                plg_inst.status = 'cancelled'
                plg_inst.save()
        return super(PluginInstanceDetail, self).destroy(request, *args, **kwargs)


class PluginInstanceDescendantList(generics.ListAPIView):
    """
    A view for the collection of plugin instances that are a descendant of this plugin
    instance.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the plugin instance descendants.
        """
        queryset = self.get_descendants_queryset()
        return services.get_list_response(self, queryset)

    def get_descendants_queryset(self):
        """
        Custom method to get the actual descendants queryset.
        """
        instance = self.get_object()
        return self.filter_queryset(instance.get_descendant_instances())


class PluginInstanceSplitList(generics.ListCreateAPIView):
    """
    A view for the collection of splits for a plugin instance.
    """
    http_method_names = ['get', 'post']
    serializer_class = PluginInstanceSplitSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrReadOnly,)

    def perform_create(self, serializer):
        """
        Overriden to associate a plugin instance and a list of newly created
        'pl-topologicalcopy' plugin instance ids with the newly created split before
        first saving to the DB.
        """
        user = self.request.user
        instance = self.get_object()
        plg_topologcopy = Plugin.objects.filter(meta__name='pl-topologicalcopy').first()

        cr_name = serializer.validated_data.pop('compute_resource_name', '')
        if cr_name:
            compute_resource = plg_topologcopy.compute_resources.get(name=cr_name)
        else:
            compute_resource = plg_topologcopy.compute_resources.first()

        plg_filter_param = plg_topologcopy.parameters.get(name='filter')
        plg_plugininstances_param = plg_topologcopy.parameters.get(name='plugininstances')

        created_plg_inst_ids = []
        filter_list = serializer.validated_data.get('filter', '').split(',')
        for f in filter_list:
            plg_inst = PluginInstance.objects.create(
                plugin=plg_topologcopy, owner=user, previous=instance,
                compute_resource=compute_resource
            )
            StrParameter.objects.create(plugin_inst=plg_inst,
                                        plugin_param=plg_plugininstances_param,
                                        value=str(instance.id))
            if f:
                StrParameter.objects.create(plugin_inst=plg_inst,
                                            plugin_param=plg_filter_param,
                                            value=f)
            if instance.status == 'finishedSuccessfully':
                # schedule the plugin's app to run
                plg_inst.status = 'scheduled'  # status changes to 'scheduled' right away
                plg_inst.save()
                run_plugin_instance.delay(plg_inst.id)  # call async task
            elif instance.status in ('created', 'waiting', 'scheduled',
                                     'registeringFiles', 'started'):
                plg_inst.status = 'waiting'
                plg_inst.save()
            elif instance.status in ('finishedWithError', 'cancelled'):
                plg_inst.status = 'cancelled'
                plg_inst.save()
            created_plg_inst_ids.append(str(plg_inst.id))

        serializer.save(
            plugin_inst=instance, created_plugin_inst_ids=','.join(created_plg_inst_ids)
        )

    def list(self, request, *args, **kwargs):
        """
        Overriden to return the list of splits for the queried plugin instance.
        A document-level link relation and a collection+json template are also added
        to the response.
        """
        queryset = self.get_splits_queryset()
        response = services.get_list_response(self, queryset)
        instance = self.get_object()
        # append document-level link relations
        links = {'plugin_inst': reverse('plugininstance-detail', request=request,
                                        kwargs={"pk": instance.id})}
        response = services.append_collection_links(response, links)
        # append write template
        template_data = {'filter': '', 'compute_resource_name': ''}
        return services.append_collection_template(response, template_data)

    def get_splits_queryset(self):
        """
        Custom method to get the actual splits queryset.
        """
        instance = self.get_object()
        return self.filter_queryset(instance.splits.all())


class PluginInstanceSplitDetail(generics.RetrieveAPIView):
    """
    A view for a plugin instance split.
    """
    http_method_names = ['get']
    queryset = PluginInstanceSplit.objects.all()
    serializer_class = PluginInstanceSplitSerializer
    permission_classes = (permissions.IsAuthenticated,)


class PluginInstanceFileList(generics.ListAPIView):
    """
    A view for the collection of files written by a plugin instance.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceFileSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated, IsRelatedFeedOwnerOrChris,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the files created by the queried plugin instance.
        Document-level link relations are also added to the response.
        """
        queryset = self.get_files_queryset()
        response = services.get_list_response(self, queryset)
        instance = self.get_object()
        feed = instance.feed
        links = {'feed': reverse('feed-detail', request=request,
                             kwargs={"pk": feed.id}),
                 'plugin_inst': reverse('plugininstance-detail', request=request,
                                                 kwargs={"pk": instance.id})}
        return services.append_collection_links(response, links)

    def get_files_queryset(self):
        """
        Custom method to get the actual files queryset.
        """
        instance = self.get_object()
        return self.filter_queryset(instance.files.all())


class AllPluginInstanceFileList(generics.ListAPIView):
    """
    A view for the collection of all plugin instance files written to all the feeds the
    the authenticated user owns.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceFileSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to add a query list to the response.
        """
        response = super(AllPluginInstanceFileList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('allplugininstancefile-list-query-search', request=request)]
        return services.append_collection_querylist(response, query_list)

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the feed files
        written to feeds owned by the currently authenticated user.
        """
        user = self.request.user
        # if the user is chris then return all the files in the system
        if user.username == 'chris':
            return PluginInstanceFile.objects.all()
        return PluginInstanceFile.objects.filter(plugin_inst__feed__owner=user)


class AllPluginInstanceFileListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of plugin instance files written to all the feeds the
    the authenticated user owns and resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PluginInstanceFileSerializer
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PluginInstanceFileFilter

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the feed files
        written to feeds owned by the currently authenticated user.
        """
        user = self.request.user
        # if the user is chris then return all the files in the system
        if user.username == 'chris':
            return PluginInstanceFile.objects.all()
        return PluginInstanceFile.objects.filter(plugin_inst__feed__owner=user)


class PluginInstanceFileDetail(generics.RetrieveAPIView):
    """
    A view for a file written by a plugin instance.
    """
    http_method_names = ['get']
    queryset = PluginInstanceFile.objects.all()
    serializer_class = PluginInstanceFileSerializer
    permission_classes = (permissions.IsAuthenticated, IsRelatedFeedOwnerOrChris,)


class FileResource(generics.GenericAPIView):
    """
    A view to enable downloading of a file resource.
    """
    http_method_names = ['get']
    queryset = PluginInstanceFile.objects.all()
    renderer_classes = (BinaryFileRenderer,)
    permission_classes = (permissions.IsAuthenticated, IsRelatedFeedOwnerOrChris,)

    def get(self, request, *args, **kwargs):
        """
        Overriden to be able to make a GET request to an actual file resource.
        """
        plg_inst_file = self.get_object()
        return FileResponse(plg_inst_file.fname)


class PluginInstanceParameterList(generics.ListAPIView):
    """
    A view for the collection of parameters that the plugin instance was run with.
    """
    http_method_names = ['get']
    serializer_class = GenericParameterSerializer
    queryset = PluginInstance.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list with all the parameter values used by the queried
        plugin instance.
        """
        queryset = self.get_parameters_queryset()
        return services.get_list_response(self, queryset)

    def get_parameters_queryset(self):
        """
        Custom method to get a queryset with all the parameters regardless of their type.
        """
        instance = self.get_object()
        return self.filter_queryset(instance.get_parameter_instances())


class StrParameterDetail(generics.RetrieveAPIView):
    """
    A string parameter view.
    """
    http_method_names = ['get']
    serializer_class = PARAMETER_SERIALIZERS['string']
    queryset = StrParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    

class IntParameterDetail(generics.RetrieveAPIView):
    """
    An integer parameter view.
    """
    http_method_names = ['get']
    serializer_class = PARAMETER_SERIALIZERS['integer']
    queryset = IntParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


class FloatParameterDetail(generics.RetrieveAPIView):
    """
    A float parameter view.
    """
    http_method_names = ['get']
    serializer_class = PARAMETER_SERIALIZERS['float']
    queryset = FloatParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    

class BoolParameterDetail(generics.RetrieveAPIView):
    """
    A boolean parameter view.
    """
    http_method_names = ['get']
    serializer_class = PARAMETER_SERIALIZERS['boolean']
    queryset = BoolParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


class PathParameterDetail(generics.RetrieveAPIView):
    """
    A path parameter view.
    """
    http_method_names = ['get']
    serializer_class = PARAMETER_SERIALIZERS['path']
    queryset = PathParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


class UnextpathParameterDetail(generics.RetrieveAPIView):
    """
    A unextpath parameter view.
    """
    http_method_names = ['get']
    serializer_class = PARAMETER_SERIALIZERS['unextpath']
    queryset = UnextpathParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


# chris_backend/plugins/admin.py


from django.contrib import admin
from django.utils import timezone
from django.urls import path
from django.shortcuts import render
from django import forms
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from django.contrib import messages
from rest_framework import generics, permissions, serializers
from rest_framework.reverse import reverse

from collectionjson import services

from .models import PluginMeta, Plugin, ComputeResource
from .serializers import ComputeResourceSerializer, PluginSerializer
from .services.manager import PluginManager


plugin_readonly_fields = [fld.name for fld in Plugin._meta.fields if
                          fld.name != 'compute_resources']


class ComputeResourceAdmin(admin.ModelAdmin):
    readonly_fields = ['creation_date', 'modification_date']
    list_display = ('name', 'compute_url', 'description', 'id')
    list_filter = ['name', 'creation_date', 'modification_date']
    search_fields = ['name', 'description']

    def add_view(self, request, form_url='', extra_context=None):
        """
        Overriden to only show the read/write fields in the add compute resource page.
        """
        self.fields = ['name', 'compute_url', 'compute_auth_url', 'compute_user',
                       'compute_password', 'compute_auth_token', 'description',
                       'max_job_exec_seconds']
        return admin.ModelAdmin.add_view(self, request, form_url, extra_context)

    def change_view(self, request, object_id, form_url='', extra_context=None):
        """
        Overriden to show all compute resources's fields in the compute resource page.
        """
        self.fields = ['name', 'compute_url', 'compute_auth_url', 'compute_user',
                       'compute_password', 'compute_auth_token', 'description',
                       'max_job_exec_seconds', 'creation_date', 'modification_date']
        return admin.ModelAdmin.change_view(self, request, object_id, form_url,
                                            extra_context)

    def save_model(self, request, obj, form, change):
        """
        Overriden to set the modification date.
        """
        if change:
            obj.modification_date = timezone.now()
        super().save_model(request, obj, form, change)

    def delete_model(self, request, obj):
        """
        Overriden to prevent deleting compute resources that would leave orphan
        plugins after the operation.
        """
        try:
            super().delete_model(request, obj)
        except ValidationError as e:
            messages.set_level(request, messages.ERROR)
            messages.error(request, e.message)

    def delete_queryset(self, request, queryset):
        """
        Overriden to prevent deleting compute resources that would leave orphan
        plugins after the operation. This customizes the deletion process for the
        “delete selected objects” action.
        """
        for compute_resource in queryset:
            plg_ids = compute_resource.get_plugins_with_self_as_single_compute_resource()
            if plg_ids:
                messages.set_level(request, messages.ERROR)
                msg = "Can not delete compute resource '%s'. Please do not check it " \
                      "for the delete or otherwise first register its associated " \
                      "plugins with IDs %s with another compute resource."
                messages.error(request, msg % (compute_resource, plg_ids))
                return None
        super().delete_queryset(request, queryset)


class PluginMetaAdmin(admin.ModelAdmin):
    readonly_fields = ['name', 'title', 'stars', 'public_repo', 'license', 'type', 'icon',
                       'category', 'authors', 'documentation', 'creation_date',
                       'modification_date']
    list_display = ('name', 'type', 'id')
    list_filter = ['type', 'creation_date', 'modification_date', 'category']
    search_fields = ['name']

    def has_change_permission(self, request, obj=None):
        """
        Overriden to disable the editing of fields in the view plugin meta page.
        """
        return False

    def has_add_permission(self, request):
        """
        Overriden to remove the add plugin meta button.
        """
        return False


class UploadFileForm(forms.Form):
    file = forms.FileField()


class PluginAdminForm(forms.ModelForm):
    name = forms.CharField(max_length=100, required=False)
    version = forms.CharField(max_length=10, required=False)
    url = forms.URLField(max_length=300, required=False)

    def clean(self):
        """
        Overriden to validate the full set of plugin descriptors and save the newly
        created plugin to the DB.
        """
        if self.instance.pk is None:  # create plugin operation
            pl_manager = PluginManager()
            compute_resources = self.cleaned_data.get('compute_resources')
            if compute_resources is None:
                raise forms.ValidationError('Please choose a compute resource')
            url = self.cleaned_data.pop('url', None)
            cr = compute_resources[0]
            if url:
                try:
                    self.instance = pl_manager.register_plugin_by_url(url, cr.name)
                except Exception as e:
                    raise forms.ValidationError(e)
            else:
                name = self.cleaned_data.get('name')
                if not name:
                    raise forms.ValidationError("A plugin's name or url is required")
                # get user-provided version (can be blank)
                version = self.cleaned_data.get('version')
                try:
                    self.instance = pl_manager.register_plugin(name, version, cr.name)
                except Exception as e:
                    raise forms.ValidationError(e)
            # reset form validated data
            self.cleaned_data['name'] = self.instance.meta.name
            self.cleaned_data['version'] = self.instance.version
            current_compute_resources = list(self.instance.compute_resources.all())
            compute_resources = list(compute_resources) + current_compute_resources
            self.instance.compute_resources.set(compute_resources)
            self.cleaned_data['compute_resources'] = self.instance.compute_resources.all()


class PluginAdmin(admin.ModelAdmin):
    form = PluginAdminForm
    list_display = ('meta', 'version', 'id')
    search_fields = ['meta__name', 'version']
    list_filter = ['meta__name', 'creation_date']
    change_form_template = 'admin/plugins/plugin/change_form.html'
    change_list_template = 'admin/plugins/plugin/change_list.html'

    def add_view(self, request, form_url='', extra_context=None):
        """
        Overriden to only show the required fields in the add plugin page.
        """
        self.readonly_fields = []
        self.fieldsets = [
            ('Compute resources', {'fields': ['compute_resources']}),
            ('Identify plugin by name and version', {'fields': [('name', 'version')]}),
            ('Or identify plugin by url', {'fields': ['url']}),
        ]
        return admin.ModelAdmin.add_view(self, request, form_url, extra_context)

    def change_view(self, request, object_id, form_url='', extra_context=None):
        """
        Overriden to show all plugin's fields in the view plugin page.
        """
        self.readonly_fields = [fl for fl in plugin_readonly_fields]
        self.readonly_fields.append('get_registered_compute_resources')
        self.fieldsets = [
            ('Compute resources', {'fields': ['compute_resources',
                                              'get_registered_compute_resources']}),
            ('Plugin properties', {'fields': plugin_readonly_fields}),
        ]
        return admin.ModelAdmin.change_view(self, request, object_id, form_url,
                                            extra_context)

    # def save_model(self, request, obj, form, change):
    #     """
    #     Overriden to set the modification date..
    #     """
    #     if change:
    #         obj.modification_date = timezone.now()
    #     super().save_model(request, obj, form, change)

    def get_urls(self):
        urls = super(PluginAdmin, self).get_urls()
        custom_urls = [
            path('add_plugins/',
                 self.admin_site.admin_view(self.add_plugins_from_file_view),
                 name="add_plugins"),
        ]
        return custom_urls + urls

    def delete_model(self, request, obj):
        """
        Overriden to delete the associated meta if this is the last associated plugin.
        """
        meta = obj.meta
        super().delete_model(request, obj)
        if meta.plugins.count() == 0:
            meta.delete()  # delete the meta if this is the last associated plugin

    def delete_queryset(self, request, queryset):
        """
        Overriden to delete plugin metas if their last associated plugin is deleted.
        This customizes the deletion process for the “delete selected objects” action.
        """
        pl_metas = []
        for plugin in queryset:
            if plugin.meta.plugins.count() == 1:
                pl_metas.append(plugin.meta)
        super().delete_queryset(request, queryset)
        for meta in pl_metas:
            meta.delete()

    def add_plugins_from_file_view(self, request):
        """
        Custom view to handle the add plugins from file page.
        """
        # custom view should return an HttpResponse
        context = dict(
            # Include common variables for rendering the admin template.
            self.admin_site.each_context(request),
            # Anything else you want in the context...
            opts=self.opts,
        )
        if request.method == 'POST':
            file_form = UploadFileForm(request.POST, request.FILES)
            if file_form.is_valid():
                # handle uploaded file (request.FILES['file'])
                summary = self.register_plugins_from_file(request.FILES['file'])
                context['summary'] = summary
                return render(request,
                              'admin/plugins/plugin/add_plugins_from_file_result.html',
                              context)
        else:
            file_form = UploadFileForm()
        context['file_form'] = file_form
        return render(request,
                      'admin/plugins/plugin/add_plugins_from_file.html',
                      context)

    def register_plugins_from_file(self, f):
        """
        Custom method to register plugins from a text file f. The first string of each
        line of the file is the plugin's name or url, the second string is the version
        (can be empty) and the third string is the associated compute environment. Any
        plugin in the file must already be previously uploaded to the ChRIS store.
        """
        summary = {'success': [], 'error': []}
        val = URLValidator()
        pl_manager = PluginManager()
        for line in f:
            try:  # check file must be a text file
                strings = line.decode().strip().split()
            except UnicodeDecodeError:
                summary = {'success': [], 'error': []}
                break
            if len(strings) == 1:
                summary['error'].append({'plugin_name': strings[0],
                                         'code': 'Missing compute resource identifier.'})
            elif len(strings) > 1:
                try:
                    val(strings[0])  # check whether the first string is a url
                except ValidationError:
                    # register by name
                    plg_name = strings[0]
                    if len(strings) == 2:
                        plg_version = None
                        compute_resource = strings[1]
                    else:
                        plg_version = strings[1]
                        compute_resource = strings[2]
                    try:
                        pl_manager.register_plugin(plg_name, plg_version,
                                                   compute_resource)
                    except Exception as e:
                        summary['error'].append({'plugin_name': plg_name, 'code': str(e)})
                    else:
                        summary['success'].append({'plugin_name': plg_name})
                else:
                    # register by url
                    plg_url = strings[0]
                    compute_resource = strings[1]
                    try:
                        pl_manager.register_plugin_by_url(plg_url, compute_resource)
                    except Exception as e:
                        summary['error'].append({'plugin_name': plg_url, 'code': str(e)})
                    else:
                        summary['success'].append({'plugin_name': plg_url})
        return summary


class ComputeResourceAdminList(generics.ListCreateAPIView):
    """
    A JSON view for the collection of compute resources that can be used by ChRIS admins
    to add a new compute resource through a REST API (alternative to the HTML-based admin
    site).
    """
    http_method_names = ['get', 'post']
    serializer_class = ComputeResourceSerializer
    queryset = ComputeResource.objects.all()
    permission_classes = (permissions.IsAdminUser,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template to the response.
        """
        response = super(ComputeResourceAdminList, self).list(request, *args, **kwargs)
        # append write template
        template_data = {'name': '', 'compute_url': '', 'compute_auth_url': '',
                         'compute_user': '', 'compute_password': '',
                         'compute_auth_token': '', 'description': '',
                         'max_job_exec_seconds': ''}
        return services.append_collection_template(response, template_data)


class PluginAdminSerializer(PluginSerializer):
    """
    A Plugin serializer for the PluginAdminList JSON view.
    """
    plugin_store_url = serializers.URLField(write_only=True)
    compute_name = serializers.CharField(max_length=100, write_only=True)
    version = serializers.CharField(required=False)
    dock_image = serializers.CharField(required=False)
    execshell = serializers.CharField(required=False)
    selfpath = serializers.CharField(required=False)
    selfexec = serializers.CharField(required=False)

    class Meta(PluginSerializer.Meta):
        fields = PluginSerializer.Meta.fields + ('plugin_store_url', 'compute_name')

    def validate(self, data):
        """
        Overriden to validate and register a plugin from a ChRIS store with a compute
        resource associated with this ChRIS instance.
        """
        # remove write_only fields that not part of the Plugin model
        cr_name = data.pop('compute_name')
        plugin_store_url = data.pop('plugin_store_url')
        pl_manager = PluginManager()
        try:
            self.instance = pl_manager.register_plugin_by_url(plugin_store_url, cr_name)
        except Exception as e:
            raise serializers.ValidationError({'non_field_errors': [str(e)]})
        return data


class PluginAdminList(generics.ListCreateAPIView):
    """
    A JSON view for the collection of plugins that can be used by ChRIS admins to
    register plugins through a REST API (alternative to the HTML-based admin site).
    """
    http_method_names = ['get', 'post']
    serializer_class = PluginAdminSerializer
    queryset = Plugin.objects.all()
    permission_classes = (permissions.IsAdminUser,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a collection+json template
        to the response.
        """
        response = super(PluginAdminList, self).list(request, *args, **kwargs)
        # append document-level link relations
        links = {'compute_resources': reverse('admin-computeresource-list',
                                              request=request)}
        response = services.append_collection_links(response, links)
        # append write template
        template_data = {'plugin_store_url': '', 'compute_name': ''}
        return services.append_collection_template(response, template_data)


admin.site.register(ComputeResource, ComputeResourceAdmin)
admin.site.register(PluginMeta, PluginMetaAdmin)
admin.site.register(Plugin, PluginAdmin)


# chris_backend/plugins/apps.py

from django.apps import AppConfig


class PluginsConfig(AppConfig):
    name = 'plugins'


# chris_backend/plugins/fields.py


from django.db import models
from django.core.exceptions import ValidationError


class MemoryInt(int):
    def __new__(cls, memory_str, *args, **kwargs):
        """
        Convert memory_str in format of "xMi" or "xGi" to integer in MiB.
        Example: memory_str_to_int("128Mi") -> 128
                 memory_str_to_int("1Gi")   -> 1024
        Throws ValueError if input string is not formatted correctly.
        """
        if isinstance(memory_str, int):
            return super(MemoryInt, cls).__new__(cls, memory_str)
        try:
            suffix = memory_str[-2:]
            if suffix == 'Gi':
                memory_int = int(memory_str[:-2]) * 1024
                assert memory_int > 0
            elif suffix == 'Mi':
                memory_int = int(memory_str[:-2])
                assert memory_int > 0
            else:
                raise ValueError
        except (IndexError, ValueError, AssertionError):
            raise ValueError("Memory format incorrect. Format is xMi or xGi where x is an integer.")
        return super(MemoryInt, cls).__new__(cls, memory_int)


class CPUInt(int):
    def __new__(cls, cpu_str, *args, **kwargs):
        """
        Convert cpu_str in format of "xm" to integer in millicores.
        Example: cpu_str_to_int("2000m") -> 2000
        Throws ValueError if input string is not formatted correctly.
        """
        if isinstance(cpu_str, int):
            return super(CPUInt, cls).__new__(cls, cpu_str)
        try:
            cpu_int = int(cpu_str[:-1])
            assert cpu_str[-1] == 'm'
            assert cpu_int > 0
        except (IndexError, ValueError, AssertionError):
            raise ValueError("CPU format incorrect. Format is xm where x is an integer in millicores.")
        return super(CPUInt, cls).__new__(cls, cpu_int)


class MemoryField(models.Field):
    """Stores memory quantity as integer."""
    error_message = "Memory format incorrect. Format is xMi or xGi where x is an integer."

    def get_internal_type(self):
        return "IntegerField"

    def get_prep_value(self, value):
        """Python object --> Query Value."""
        if value is None:
            return None
        return int(value)

    def to_python(self, value):
        """Query value --> Python object."""
        if value is None:
            return None
        try:
            return MemoryInt(value)
        except ValueError:
            raise ValidationError(self.error_message)


class CPUField(models.Field):
    """Stores CPU quantity as integer in millicores."""
    error_message = "CPU format incorrect. Format is xm where x is an integer in millicores."

    def get_internal_type(self):
        return "IntegerField"

    def get_prep_value(self, value):
        """Python object --> Query Value."""
        if value is None:
            return None
        return int(value)

    def to_python(self, value):
        """Query value --> Python object."""
        if value is None:
            return None
        try:
            return CPUInt(value)
        except ValueError:
            raise ValidationError(self.error_message)


# chris_backend/plugins/models.py


from django.db import models

import django_filters
from django_filters.rest_framework import FilterSet
from django.core.exceptions import ValidationError

from .fields import CPUField, MemoryField


# front-end API types
TYPE_CHOICES = [("string", "String values"), ("float", "Float values"),
                ("boolean", "Boolean values"), ("integer", "Integer values"),
                ("path", "Path values"), ("unextpath", "Unextracted path values")]

# table of equivalence between front-end API types and back-end types
TYPES = {'string': 'str', 'integer': 'int', 'float': 'float', 'boolean': 'bool',
         'path': 'path', 'unextpath': 'unextpath'}

PLUGIN_TYPE_CHOICES = [("ds", "Data synthesis"), ("fs", "Feed synthesis"),
                       ("ts", "Topology synthesis")]


class ComputeResource(models.Model):
    """
    Model class that defines a remote compute resource for plugins.
    """
    creation_date = models.DateTimeField(auto_now_add=True)
    modification_date = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=100, unique=True)
    compute_url = models.URLField(max_length=300)
    compute_auth_url = models.URLField(max_length=350, blank=True)
    compute_user = models.CharField(max_length=32)
    compute_password = models.CharField(max_length=100)
    compute_auth_token = models.CharField(max_length=500, blank=True,
                                          default='initial_token')
    description = models.CharField(max_length=600, blank=True)
    max_job_exec_seconds = models.IntegerField(blank=True, default=86400)  # 24h

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        """
        Overriden to save a default value for the auth url.
        """
        if not self.compute_auth_url:
            self.compute_auth_url = str(self.compute_url) + 'auth-token/'  # set default
        super().save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        """
        Overriden to only allow the delete if no plugin would be left without a compute
        resource after the operation.
        """
        plg_ids = self.get_plugins_with_self_as_single_compute_resource()
        if plg_ids:
            plg_ids.sort()
            msg = "Can not delete compute resource '%s'. Please first register the " \
                  "following plugins with another compute resource. Plugin IDs: %s"
            raise ValidationError(msg % (self.name, plg_ids))
        super().delete()

    def get_plugins_with_self_as_single_compute_resource(self):
        """
        Custom method to get the list of plugin ids for the plugins that are only
        registered with this single compute resource.
        """
        return [pl.id for pl in self.plugins.all() if pl.compute_resources.count() == 1]


class ComputeResourceFilter(FilterSet):
    """
    Filter class for the ComputeResource model.
    """
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    name_exact = django_filters.CharFilter(field_name='name', lookup_expr='exact')
    description = django_filters.CharFilter(field_name='description',
                                            lookup_expr='icontains')
    plugin_id = django_filters.CharFilter(field_name='plugins__id',
                                          lookup_expr='exact')

    class Meta:
        model = ComputeResource
        fields = ['id', 'name', 'name_exact', 'description', 'plugin_id']


class PluginMeta(models.Model):
    """
    Model class that defines the meta info for a plugin that is the same across
    plugin's versions.
    """
    creation_date = models.DateTimeField(auto_now_add=True)
    modification_date = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=100, unique=True)
    title = models.CharField(max_length=400, blank=True)
    stars = models.IntegerField(default=0)
    public_repo = models.URLField(max_length=300, blank=True)
    license = models.CharField(max_length=50, blank=True)
    type = models.CharField(choices=PLUGIN_TYPE_CHOICES, default='ds', max_length=4)
    icon = models.URLField(max_length=300, blank=True)
    category = models.CharField(max_length=100, blank=True)
    authors = models.CharField(max_length=200, blank=True)
    documentation = models.CharField(max_length=800, blank=True)

    class Meta:
        ordering = ('type', '-creation_date',)

    def __str__(self):
        return str(self.name)


class PluginMetaFilter(FilterSet):
    """
    Filter class for the PluginMeta model.
    """
    min_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='lte')
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    name_exact = django_filters.CharFilter(field_name='name', lookup_expr='exact')
    title = django_filters.CharFilter(field_name='title', lookup_expr='icontains')
    category = django_filters.CharFilter(field_name='category', lookup_expr='icontains')
    type = django_filters.CharFilter(field_name='type', lookup_expr='exact')
    authors = django_filters.CharFilter(field_name='authors', lookup_expr='icontains')
    name_title_category = django_filters.CharFilter(method='search_name_title_category')
    name_authors_category = django_filters.CharFilter(
        method='search_name_authors_category')

    def search_name_title_category(self, queryset, name, value):
        """
        Custom method to get a filtered queryset with all plugins for which name or title
        or category matches the search value.
        """
        # construct the full lookup expression.
        lookup = models.Q(name__icontains=value)
        lookup = lookup | models.Q(title__icontains=value)
        lookup = lookup | models.Q(category__icontains=value)
        return queryset.filter(lookup)

    def search_name_authors_category(self, queryset, name, value):
        """
        Custom method to get a filtered queryset with all plugins for which name or author
        or category matches the search value.
        """
        # construct the full lookup expression.
        lookup = models.Q(name__icontains=value)
        lookup = lookup | models.Q(authors__icontains=value)
        lookup = lookup | models.Q(category__icontains=value)
        return queryset.filter(lookup)

    class Meta:
        model = PluginMeta
        fields = ['id', 'name', 'name_exact', 'title', 'category', 'type', 'authors',
                  'min_creation_date', 'max_creation_date', 'name_title_category',
                  'name_authors_category']


class Plugin(models.Model):
    """
    Model class that defines the versioned plugin.
    """
    # default resource limits inserted at registration time
    defaults = {
                'min_cpu_limit': 1000,    # in millicores
                'min_memory_limit': 200,  # in Mi
                'max_limit': 2147483647   # maxint
               }
    creation_date = models.DateTimeField(auto_now_add=True)
    meta = models.ForeignKey(PluginMeta, on_delete=models.CASCADE, related_name='plugins')
    version = models.CharField(max_length=10)
    dock_image = models.CharField(max_length=500)
    execshell = models.CharField(max_length=50)
    selfpath = models.CharField(max_length=512)
    selfexec = models.CharField(max_length=50)
    description = models.CharField(max_length=2000, blank=True)
    min_gpu_limit = models.IntegerField(null=True, blank=True, default=0)
    max_gpu_limit = models.IntegerField(null=True, blank=True, default=0)
    min_number_of_workers = models.IntegerField(null=True, blank=True, default=1)
    max_number_of_workers = models.IntegerField(null=True, blank=True,
                                                default=defaults['max_limit'])
    min_cpu_limit = CPUField(null=True, blank=True,
                             default=defaults['min_cpu_limit'])  # In millicores
    max_cpu_limit = CPUField(null=True, blank=True,
                             default=defaults['max_limit'])  # In millicores
    min_memory_limit = MemoryField(null=True, blank=True,
                                   default=defaults['min_memory_limit'])  # In Mi
    max_memory_limit = MemoryField(null=True, blank=True,
                                   default=defaults['max_limit'])  # In Mi
    compute_resources = models.ManyToManyField(ComputeResource, related_name='plugins')

    class Meta:
        unique_together = [['meta', 'version'], ['meta', 'dock_image']]
        ordering = ('meta', '-creation_date',)

    def __str__(self):
        return self.meta.name

    def get_plugin_parameter_names(self):
        """
        Custom method to get the list of plugin parameter names.
        """
        return [param.name for param in self.parameters.all()]

    def get_registered_compute_resources(self):
        return [cr.name for cr in self.compute_resources.all()]
    get_registered_compute_resources.admin_order_field = 'id'
    get_registered_compute_resources.short_description = 'Associated compute resources'


class PluginFilter(FilterSet):
    """
    Filter class for the Plugin model.
    """
    min_creation_date = django_filters.IsoDateTimeFilter(field_name="creation_date",
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name="creation_date",
                                                         lookup_expr='lte')
    name = django_filters.CharFilter(field_name='meta__name', lookup_expr='icontains')
    name_exact = django_filters.CharFilter(field_name='meta__name', lookup_expr='exact')
    title = django_filters.CharFilter(field_name='meta__title', lookup_expr='icontains')
    category = django_filters.CharFilter(field_name='meta__category',
                                         lookup_expr='icontains')
    type = django_filters.CharFilter(field_name='meta__type', lookup_expr='exact')
    description = django_filters.CharFilter(field_name='description',
                                            lookup_expr='icontains')
    name_title_category = django_filters.CharFilter(method='search_name_title_category')
    compute_resource_id = django_filters.CharFilter(field_name='compute_resources__id',
                                                    lookup_expr='exact')

    def search_name_title_category(self, queryset, name, value):
        """
        Custom method to get a filtered queryset with all plugins for which name or title
        or category matches the search value.
        """
        # construct the full lookup expression.
        lookup = models.Q(meta__name__icontains=value)
        lookup = lookup | models.Q(meta__title__icontains=value)
        lookup = lookup | models.Q(meta__category__icontains=value)
        return queryset.filter(lookup)

    class Meta:
        model = Plugin
        fields = ['id', 'name', 'name_exact', 'version', 'dock_image', 'type', 'category',
                  'min_creation_date', 'max_creation_date', 'title',  'description',
                  'name_title_category', 'compute_resource_id']


class PluginParameter(models.Model):
    """
    Model class that defines a plugin parameter.
    """
    name = models.CharField(max_length=50)
    flag = models.CharField(max_length=52)
    short_flag = models.CharField(max_length=52, blank=True)
    action = models.CharField(max_length=20, default='store')
    optional = models.BooleanField(default=False)
    type = models.CharField(choices=TYPE_CHOICES, default='string', max_length=10)
    help = models.TextField(blank=True)
    ui_exposed = models.BooleanField(default=True)
    plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE,
                               related_name='parameters')
    
    class Meta:
        ordering = ('plugin',)

    def __str__(self):
        return self.name

    def get_default(self):
        """
        Overriden to get the default parameter instance regardless of its type.
        """
        default_attr_name = '%s_default' % self.type
        return getattr(self, default_attr_name, None)


class DefaultStrParameter(models.Model):
    """
    Model class that defines a default value for a plugin parameter of type string.
    """
    value = models.CharField(max_length=600, blank=True)
    plugin_param = models.OneToOneField(PluginParameter, on_delete=models.CASCADE,
                                        related_name='string_default')

    def __str__(self):
        return self.value


class DefaultIntParameter(models.Model):
    """
    Model class that defines a default value for a plugin parameter of type integer.
    """
    value = models.IntegerField()
    plugin_param = models.OneToOneField(PluginParameter, on_delete=models.CASCADE,
                                        related_name='integer_default')

    def __str__(self):
        return str(self.value)


class DefaultFloatParameter(models.Model):
    """
    Model class that defines a default value for a plugin parameter of type float.
    """
    value = models.FloatField()
    plugin_param = models.OneToOneField(PluginParameter, on_delete=models.CASCADE,
                                        related_name='float_default')

    def __str__(self):
        return str(self.value)


class DefaultBoolParameter(models.Model):
    """
    Model class that defines a default value for a plugin parameter of type boolean.
    """
    value = models.BooleanField()
    plugin_param = models.OneToOneField(PluginParameter, on_delete=models.CASCADE,
                                        related_name='boolean_default')

    def __str__(self):
        return str(self.value)


# chris_backend/plugins/serializers.py


import re

from django.utils import timezone
from rest_framework import serializers

from .models import ComputeResource, PluginMeta, Plugin, PluginParameter
from .models import (DefaultFloatParameter, DefaultIntParameter, DefaultBoolParameter,
                     DefaultStrParameter)
from .fields import MemoryInt, CPUInt


class ComputeResourceSerializer(serializers.HyperlinkedModelSerializer):
    compute_user = serializers.CharField(min_length=4, max_length=32, write_only=True)
    compute_password = serializers.CharField(min_length=8, max_length=100,
                                             write_only=True)
    compute_auth_token = serializers.CharField(max_length=500, write_only=True,
                                               required=False)

    class Meta:
        model = ComputeResource
        fields = ('url', 'id', 'creation_date', 'modification_date', 'name',
                  'compute_url', 'compute_auth_url', 'compute_user',
                  'compute_password', 'compute_auth_token', 'description',
                  'max_job_exec_seconds')


class PluginMetaSerializer(serializers.HyperlinkedModelSerializer):
    plugins = serializers.HyperlinkedIdentityField(view_name='pluginmeta-plugin-list')

    class Meta:
        model = PluginMeta
        fields = ('url', 'id', 'creation_date', 'modification_date', 'name', 'title',
                  'stars', 'public_repo', 'license', 'type', 'icon', 'category',
                  'authors', 'documentation', 'plugins')

    def update(self, instance, validated_data):
        """
        Overriden to add modification date.
        """
        instance.modification_date = timezone.now()
        instance.save()
        return super(PluginMetaSerializer, self).update(instance, validated_data)


class PluginSerializer(serializers.HyperlinkedModelSerializer):
    name = serializers.ReadOnlyField(source='meta.name')
    title = serializers.ReadOnlyField(source='meta.title')
    public_repo = serializers.ReadOnlyField(source='meta.public_repo')
    license = serializers.ReadOnlyField(source='meta.license')
    type = serializers.ReadOnlyField(source='meta.type')
    icon = serializers.ReadOnlyField(source='meta.icon')
    category = serializers.ReadOnlyField(source='meta.category')
    authors = serializers.ReadOnlyField(source='meta.authors')
    documentation = serializers.ReadOnlyField(source='meta.documentation')
    stars = serializers.ReadOnlyField(source='meta.stars')
    meta = serializers.HyperlinkedRelatedField(view_name='pluginmeta-detail',
                                               read_only=True)
    parameters = serializers.HyperlinkedIdentityField(view_name='pluginparameter-list')
    instances = serializers.HyperlinkedIdentityField(view_name='plugininstance-list')
    compute_resources = serializers.HyperlinkedIdentityField(
        view_name='plugin-computeresource-list')

    class Meta:
        model = Plugin
        fields = ('url', 'id', 'creation_date', 'name', 'version', 'dock_image',
                  'public_repo', 'icon', 'type', 'stars', 'authors', 'title', 'category',
                  'description', 'documentation', 'license',  'execshell', 'selfpath',
                  'selfexec', 'min_number_of_workers', 'max_number_of_workers',
                  'min_cpu_limit', 'max_cpu_limit', 'min_memory_limit',
                  'max_memory_limit', 'min_gpu_limit', 'max_gpu_limit', 'meta',
                  'parameters', 'instances', 'compute_resources')

    def validate(self, data):
        """
        Overriden to validate compute-related descriptors in the plugin app
        representation.
        """
        # validate compute-related descriptors
        if 'min_number_of_workers' in data:
            data['min_number_of_workers'] = self.validate_app_workers_descriptor(
                {'name': 'min_number_of_workers', 'value': data['min_number_of_workers']})

        if 'max_number_of_workers' in data:
            data['max_number_of_workers'] = self.validate_app_workers_descriptor(
                {'name': 'max_number_of_workers', 'value': data['max_number_of_workers']})

        if 'min_gpu_limit' in data:
            data['min_gpu_limit'] = self.validate_app_gpu_descriptor(
                {'name': 'min_gpu_limit', 'value': data['min_gpu_limit']})

        if 'max_gpu_limit' in data:
            data['max_gpu_limit'] = self.validate_app_gpu_descriptor(
                {'name': 'max_gpu_limit', 'value': data['max_gpu_limit']})

        if 'min_cpu_limit' in data:
            data['min_cpu_limit'] = self.validate_app_cpu_descriptor(
                {'name': 'min_cpu_limit', 'value': data['min_cpu_limit']})

        if 'max_cpu_limit' in data:
            data['max_cpu_limit'] = self.validate_app_cpu_descriptor(
                {'name': 'max_cpu_limit', 'value': data['max_cpu_limit']})

        if 'min_memory_limit' in data:
            data['min_memory_limit'] = self.validate_app_memory_descriptor(
                {'name': 'min_memory_limit', 'value': data['min_memory_limit']})

        if 'max_memory_limit' in data:
            data['max_memory_limit'] = self.validate_app_memory_descriptor(
                {'name': 'max_memory_limit', 'value': data['max_memory_limit']})

        # validate descriptor limits
        err_msg = "Minimum number of workers should be less than maximum number of " \
                  "workers."
        self.validate_app_descriptor_limits(data, 'min_number_of_workers',
                                            'max_number_of_workers', err_msg)
        err_msg = "Minimum cpu limit should be less than maximum cpu limit."
        self.validate_app_descriptor_limits(data, 'min_cpu_limit', 'max_cpu_limit',
                                            err_msg)
        err_msg = "Minimum memory limit should be less than maximum memory limit."
        self.validate_app_descriptor_limits(data, 'min_memory_limit',
                                            'max_memory_limit', err_msg)
        err_msg = "Minimum gpu limit should be less than maximum gpu limit."
        self.validate_app_descriptor_limits(data, 'min_gpu_limit', 'max_gpu_limit',
                                            err_msg)
        return data

    def validate_version(self, version):
        """
        Overriden to check that a proper version type and format has been submitted.
        """
        if not isinstance(version, str):
            raise serializers.ValidationError(["Invalid type for plugin app version "
                                               "field. Must be a string."])
        if not re.match(r"^[0-9.]+$", version):
            raise serializers.ValidationError(["Invalid plugin app version number "
                                               "format %s." % version])
        return version

    @staticmethod
    def validate_app_workers_descriptor(descriptor_dict):
        """
        Custom method to validate plugin maximum and minimum workers descriptors.
        """
        error_msg = "This field must be a positive integer."
        int_d = PluginSerializer.validate_app_int_descriptor(descriptor_dict, error_msg)
        if int_d < 1:
            raise serializers.ValidationError({descriptor_dict['name']: [error_msg]})
        return int_d

    @staticmethod
    def validate_app_cpu_descriptor(descriptor_dict):
        """
        Custom method to validate plugin maximum and minimum cpu descriptors.
        """
        try:
            return CPUInt(descriptor_dict['value'])
        except ValueError as e:
            raise serializers.ValidationError({descriptor_dict['name']: [str(e)]})

    @staticmethod
    def validate_app_memory_descriptor(descriptor_dict):
        """
        Custom method to validate plugin maximum and minimum memory descriptors.
        """
        try:
            return MemoryInt(descriptor_dict['value'])
        except ValueError as e:
            raise serializers.ValidationError({descriptor_dict['name']: [str(e)]})

    @staticmethod
    def validate_app_gpu_descriptor(descriptor_dict):
        """
        Custom method to validate plugin maximum and minimum gpu descriptors.
        """
        error_msg = "This field must be a non-negative integer."
        return PluginSerializer.validate_app_int_descriptor(descriptor_dict, error_msg)

    @staticmethod
    def validate_app_int_descriptor(descriptor_dict, error_msg):
        """
        Custom method to validate a positive integer descriptor.
        """
        try:
            int_d = int(descriptor_dict['value'])
            assert int_d >= 0
        except (ValueError, AssertionError):
            raise serializers.ValidationError({descriptor_dict['name']: [error_msg]})
        return int_d

    @staticmethod
    def validate_app_descriptor_limits(app_repr, min_descriptor_name, max_descriptor_name,
                                       error_msg=''):
        """
        Custom method to validate that a descriptor's minimum is smaller than its maximum.
        """
        if (min_descriptor_name in app_repr) and (max_descriptor_name in app_repr) \
                and (app_repr[max_descriptor_name] < app_repr[min_descriptor_name]):
            raise serializers.ValidationError({'non_field_errors': [error_msg]})


class PluginParameterSerializer(serializers.HyperlinkedModelSerializer):
    plugin = serializers.HyperlinkedRelatedField(view_name='plugin-detail',
                                                 read_only=True)
    default = serializers.SerializerMethodField()

    class Meta:
        model = PluginParameter
        fields = ('url', 'id', 'name', 'type', 'optional', 'default', 'flag',
                  'short_flag', 'action', 'help', 'ui_exposed', 'plugin')

    def validate(self, data):
        """
        Overriden to check that optional is always false for parameters of type 'path'
        and 'unextpath'.
        """
        optional = data.get('optional')
        param_type = data.get('type')
        if optional and param_type in ('path', 'unextpath'):
            error_msg = "Parameters of type 'path' or 'unextpath' cannot be optional."
            raise serializers.ValidationError({'non_field_errors': [error_msg]})
        return data

    def get_default(self, obj):
        """
        Overriden to get the default parameter value regardless of type.
        """
        default = obj.get_default()
        return default.value if default else None


class DefaultStrParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultStrParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_param')


class DefaultIntParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultIntParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_param')


class DefaultFloatParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultFloatParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_param')


class DefaultBoolParameterSerializer(serializers.HyperlinkedModelSerializer):
    param_name = serializers.ReadOnlyField(source='plugin_param.name')
    type = serializers.ReadOnlyField(source='plugin_param.type')
    plugin_param = serializers.HyperlinkedRelatedField(view_name='pluginparameter-detail',
                                                       read_only=True)

    class Meta:
        model = DefaultBoolParameter
        fields = ('url', 'id', 'param_name', 'value', 'type', 'plugin_param')


DEFAULT_PARAMETER_SERIALIZERS = {'string': DefaultStrParameterSerializer,
                                 'integer': DefaultIntParameterSerializer,
                                 'float': DefaultFloatParameterSerializer,
                                 'boolean': DefaultBoolParameterSerializer}


# chris_backend/plugins/views.py


from rest_framework import generics, permissions
from rest_framework.reverse import reverse

from collectionjson import services

from .models import ComputeResource, ComputeResourceFilter
from .models import PluginMeta, PluginMetaFilter, Plugin, PluginFilter, PluginParameter
from .serializers import ComputeResourceSerializer
from .serializers import PluginMetaSerializer, PluginSerializer, PluginParameterSerializer


class ComputeResourceList(generics.ListAPIView):
    """
    A view for the collection of compute resources.
    """
    http_method_names = ['get']
    serializer_class = ComputeResourceSerializer
    queryset = ComputeResource.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a query list to the
        response.
        """
        response = super(ComputeResourceList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('computeresource-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'feeds': reverse('feed-list', request=request)}
        return services.append_collection_links(response, links)


class ComputeResourceListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of compute resources resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = ComputeResourceSerializer
    queryset = ComputeResource.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = ComputeResourceFilter


class ComputeResourceDetail(generics.RetrieveAPIView):
    """
    A compute resource view.
    """
    http_method_names = ['get']
    serializer_class = ComputeResourceSerializer
    queryset = ComputeResource.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


class PluginMetaList(generics.ListAPIView):
    """
    A view for the collection of plugin metas.
    """
    http_method_names = ['get']
    queryset = PluginMeta.objects.all()
    serializer_class = PluginMetaSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a query list to the
        response.
        """
        response = super(PluginMetaList, self).list(request, *args, **kwargs)
        # append document-level link relations
        links = {'feeds': reverse('feed-list', request=request),
                 'plugins': reverse('plugin-list', request=request)}
        response = services.append_collection_links(response, links)
        # append query list
        query_list = [reverse('pluginmeta-list-query-search', request=request)]
        return services.append_collection_querylist(response, query_list)


class PluginMetaListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of plugin metas resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PluginMetaSerializer
    queryset = PluginMeta.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PluginMetaFilter


class PluginMetaDetail(generics.RetrieveAPIView):
    """
    A plugin meta view.
    """
    http_method_names = ['get']
    serializer_class = PluginMetaSerializer
    queryset = PluginMeta.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


class PluginMetaPluginList(generics.ListAPIView):
    """
    A view for the collection of meta-specific plugins.
    """
    http_method_names = ['get']
    queryset = PluginMeta.objects.all()
    serializer_class = PluginSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the plugins for the queried meta.
        Document-level link relations are also added to the response.
        """
        queryset = self.get_plugins_queryset()
        response = services.get_list_response(self, queryset)
        meta = self.get_object()
        links = {'meta': reverse('pluginmeta-detail', request=request,
                                 kwargs={"pk": meta.id})}
        return services.append_collection_links(response, links)

    def get_plugins_queryset(self):
        """
        Custom method to get the actual plugins queryset.
        """
        meta = self.get_object()
        return self.filter_queryset(meta.plugins.all())


class PluginList(generics.ListAPIView):
    """
    A view for the collection of plugins.
    """
    http_method_names = ['get']
    serializer_class = PluginSerializer
    queryset = Plugin.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a query list to the
        response.
        """
        response = super(PluginList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('plugin-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append document-level link relations
        links = {'feeds': reverse('feed-list', request=request),
                 'plugin_metas': reverse('pluginmeta-list', request=request)}
        return services.append_collection_links(response, links)


class PluginListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of plugins resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = PluginSerializer
    queryset = Plugin.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = PluginFilter


class PluginComputeResourceList(generics.ListAPIView):
    """
    A view for a plugin-specific collection of compute resources.
    """
    http_method_names = ['get']
    queryset = Plugin.objects.all()
    serializer_class = ComputeResourceSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return a list of the compute resources for the queried plugin.
        Document-level link relations are also added to the response.
        """
        queryset = self.get_computeresources_queryset()
        response = services.get_list_response(self, queryset)
        plugin = self.get_object()
        links = {'plugin': reverse('plugin-detail', request=request,
                                   kwargs={"pk": plugin.id})}
        return services.append_collection_links(response, links)

    def get_computeresources_queryset(self):
        """
        Custom method to get the actual compute resources queryset for the plugin.
        """
        plugin = self.get_object()
        return plugin.compute_resources.all()


class PluginDetail(generics.RetrieveAPIView):
    """
    A plugin view.
    """
    http_method_names = ['get']
    serializer_class = PluginSerializer
    queryset = Plugin.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


class PluginParameterList(generics.ListAPIView):
    """
    A view for the collection of plugin parameters.
    """
    http_method_names = ['get']
    serializer_class = PluginParameterSerializer
    queryset = Plugin.objects.all()
    permission_classes = (permissions.IsAuthenticated,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to return the list of parameters for the queried plugin.
        A document-level link relation is also added to the response.
        """
        queryset = self.get_plugin_parameters_queryset()
        response = services.get_list_response(self, queryset)
        plugin = self.get_object()
        links = {'plugin': reverse('plugin-detail', request=request,
                                   kwargs={"pk": plugin.id})}
        return services.append_collection_links(response, links)

    def get_plugin_parameters_queryset(self):
        """
        Custom method to get the actual plugin parameters' queryset.
        """
        plugin = self.get_object()
        return self.filter_queryset(plugin.parameters.all())


class PluginParameterDetail(generics.RetrieveAPIView):
    """
    A plugin parameter view.
    """
    http_method_names = ['get']
    serializer_class = PluginParameterSerializer
    queryset = PluginParameter.objects.all()
    permission_classes = (permissions.IsAuthenticated,)


# chris_backend/servicefiles/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/servicefiles/apps.py

from django.apps import AppConfig


class ServicefilesConfig(AppConfig):
    name = 'servicefiles'


# chris_backend/servicefiles/models.py


from django.db import models
import django_filters
from django_filters.rest_framework import FilterSet

from core.utils import filter_files_by_n_slashes


REGISTERED_SERVICES = ['PACS']


class Service(models.Model):
    identifier = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return self.identifier


class ServiceFile(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    fname = models.FileField(max_length=512, unique=True)
    service = models.ForeignKey(Service, db_index=True, on_delete=models.CASCADE)

    class Meta:
        ordering = ('-fname',)

    def __str__(self):
        return self.fname.name


class ServiceFileFilter(FilterSet):
    min_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='lte')
    fname = django_filters.CharFilter(field_name='fname', lookup_expr='startswith')
    fname_exact = django_filters.CharFilter(field_name='fname', lookup_expr='exact')
    fname_icontains = django_filters.CharFilter(field_name='fname',
                                                lookup_expr='icontains')
    fname_nslashes = django_filters.CharFilter(method='filter_by_n_slashes')
    service_identifier = django_filters.CharFilter(field_name='service__identifier',
                                                   lookup_expr='exact')
    service_id = django_filters.CharFilter(field_name='service_id', lookup_expr='exact')

    class Meta:
        model = ServiceFile
        fields = ['id', 'min_creation_date', 'max_creation_date', 'fname', 'fname_exact',
                  'fname_icontains', 'fname_nslashes', 'service_identifier', 'service_id']

    def filter_by_n_slashes(self, queryset, name, value):
        """
        Custom method to return the files that have the queried number of slashes in
        their fname property. If the queried number ends in 'u' or 'U' then only one
        file per each last "folder" in the path is returned (useful to efficiently get
        the list of immediate folders under the path).
        """
        return filter_files_by_n_slashes(queryset, value)


# chris_backend/servicefiles/permissions.py


from rest_framework import permissions


class IsChrisOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow superuser 'chris' to create it.
    Read only is allowed to other users.
    """

    def has_permission(self, request, view):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the superuser 'chris'.
        return request.user.username == 'chris'


# chris_backend/servicefiles/serializers.py


import logging

from django.conf import settings
from rest_framework import serializers

from collectionjson.fields import ItemLinkField
from core.utils import get_file_resource_link
from core.swiftmanager import SwiftManager

from .models import Service, ServiceFile
from .models import REGISTERED_SERVICES


logger = logging.getLogger(__name__)


class ServiceSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = Service
        fields = ('url', 'id', 'identifier')


class ServiceFileSerializer(serializers.HyperlinkedModelSerializer):
    file_resource = ItemLinkField('get_file_link')
    path = serializers.CharField(write_only=True)
    fname = serializers.FileField(use_url=False, required=False)
    fsize = serializers.ReadOnlyField(source='fname.size')
    service_identifier = serializers.ReadOnlyField(source='service.identifier')
    service_name = serializers.CharField(write_only=True)

    class Meta:
        model = ServiceFile
        fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'path',
                  'service_identifier', 'service_name', 'file_resource')

    def get_file_link(self, obj):
        """
        Custom method to get the hyperlink to the actual file resource.
        """
        return get_file_resource_link(self, obj)

    def create(self, validated_data):
        """
        Overriden to associate a Swift storage path with the newly created pacs file.
        """
        # remove path as it is not part of the model and compute fname
        path = validated_data.pop('path')
        validated_data['fname'] = path
        return super(ServiceFileSerializer, self).create(validated_data)

    def validate_service_name(self, service_name):
        """
        Overriden to check whether the provided service name is a valid service
        identifier.
        """
        if service_name in REGISTERED_SERVICES:
            error_msg = "This is the name of a registered service. Please use the " \
                        "service-specific API."
            raise serializers.ValidationError([error_msg])
        try:
            Service.objects.get(identifier=service_name)
        except Service.DoesNotExist:
            # validate new Service identifier
            service_serializer = ServiceSerializer(data={'identifier': service_name})
            try:
                service_serializer.is_valid(raise_exception=True)
            except serializers.ValidationError as e:
                raise serializers.ValidationError(e.detail['identifier'])
        return service_name

    def validate(self, data):
        """
        Overriden to check whether the provided path is under SERVICES/<service_name>/
        path and already registered.
        """
        # remove service_name as it is not part of the model
        service_name = data.pop('service_name')
        # verify that the file path is correct
        path = data.get('path')
        path = path.strip(' ').strip('/')
        prefix = 'SERVICES/%s/' % service_name
        if not path.startswith(prefix):
            error_msg = "File path must start with '%s'." % prefix
            raise serializers.ValidationError([error_msg])
        # verify that the file is indeed already in Swift
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        try:
            swift_path_exists = swift_manager.obj_exists(path)
        except Exception as e:
            logger.error('Swift storage error, detail: %s' % str(e))
            raise serializers.ValidationError({'path': ["Could not find this path."]})
        if not swift_path_exists:
            raise serializers.ValidationError({'path': ["Could not find this path."]})
        # verify that the file has not already been registered
        try:
            ServiceFile.objects.get(fname=path)
        except ServiceFile.DoesNotExist:
            pass
        else:
            error_msg = "File has already been registered."
            raise serializers.ValidationError({'path': [error_msg]})
        # update validated data
        (service, tf) = Service.objects.get_or_create(identifier=service_name)
        data.update({'path': path, 'service': service})
        return data


# chris_backend/servicefiles/views.py


from django.http import FileResponse
from rest_framework import generics, permissions
from rest_framework.reverse import reverse

from collectionjson import services
from core.renderers import BinaryFileRenderer

from .models import ServiceFile, ServiceFileFilter
from .serializers import ServiceFileSerializer
from .permissions import IsChrisOrReadOnly


class ServiceFileList(generics.ListCreateAPIView):
    """
    A view for the collection of PACS files.
    """
    http_method_names = ['get', 'post']
    queryset = ServiceFile.objects.all()
    serializer_class = ServiceFileSerializer
    permission_classes = (permissions.IsAuthenticated, IsChrisOrReadOnly,)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a collection+json
        template to the response.
        """
        response = super(ServiceFileList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('servicefile-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append write template
        template_data = {'path': "", 'service_name': ""}
        return services.append_collection_template(response, template_data)

    def create(self, request, *args, **kwargs):
        """
        Overriden to remove computed descriptors from the request if submitted.
        """
        self.request.data.pop('fname', None)
        return super(ServiceFileList, self).create(request, *args, **kwargs)


class ServiceFileListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of Service files resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = ServiceFileSerializer
    queryset = ServiceFile.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = ServiceFileFilter


class ServiceFileDetail(generics.RetrieveAPIView):
    """
    A Service file view.
    """
    http_method_names = ['get']
    queryset = ServiceFile.objects.all()
    serializer_class = ServiceFileSerializer
    permission_classes = (permissions.IsAuthenticated,)


class ServiceFileResource(generics.GenericAPIView):
    """
    A view to enable downloading of a file resource .
    """
    http_method_names = ['get']
    queryset = ServiceFile.objects.all()
    renderer_classes = (BinaryFileRenderer,)
    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, *args, **kwargs):
        """
        Overriden to be able to make a GET request to an actual file resource.
        """
        service_file = self.get_object()
        return FileResponse(service_file.fname)


# chris_backend/uploadedfiles/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/uploadedfiles/apps.py

from django.apps import AppConfig


class UploadedfilesConfig(AppConfig):
    name = 'uploadedfiles'


# chris_backend/uploadedfiles/models.py


from django.db import models
import django_filters
from django_filters.rest_framework import FilterSet

from core.utils import filter_files_by_n_slashes


def uploaded_file_path(instance, filename):
    # file will be stored to Swift at:
    # SWIFT_CONTAINER_NAME/<username>/uploads/<upload_path>
    return instance.owner.upload_path


class UploadedFile(models.Model):
    creation_date = models.DateTimeField(auto_now_add=True)
    fname = models.FileField(max_length=512, upload_to=uploaded_file_path, unique=True)
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    class Meta:
        ordering = ('-fname',)

    def __str__(self):
        return self.fname.name


class UploadedFileFilter(FilterSet):
    min_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='gte')
    max_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
                                                         lookup_expr='lte')
    fname = django_filters.CharFilter(field_name='fname', lookup_expr='startswith')
    fname_exact = django_filters.CharFilter(field_name='fname', lookup_expr='exact')
    fname_icontains = django_filters.CharFilter(field_name='fname',
                                                lookup_expr='icontains')
    fname_nslashes = django_filters.CharFilter(method='filter_by_n_slashes')
    owner_username = django_filters.CharFilter(field_name='owner__username',
                                               lookup_expr='exact')

    class Meta:
        model = UploadedFile
        fields = ['id', 'min_creation_date', 'max_creation_date', 'fname', 'fname_exact',
                  'fname_icontains', 'fname_nslashes', 'owner_username']

    def filter_by_n_slashes(self, queryset, name, value):
        """
        Custom method to return the files that have the queried number of slashes in
        their fname property. If the queried number ends in 'u' or 'U' then only one
        file per each last "folder" in the path is returned (useful to efficiently get
        the list of immediate folders under the path).
        """
        return filter_files_by_n_slashes(queryset, value)


# chris_backend/uploadedfiles/permissions.py

from rest_framework import permissions


class IsOwnerOrChris(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read and write permissions are only allowed to
        # the owner and superuser 'chris'.
        if hasattr(obj.owner, 'all'):
            return (request.user in obj.owner.all()) or (request.user.username == 'chris')
        return (obj.owner == request.user) or (request.user.username == 'chris')




# chris_backend/uploadedfiles/serializers.py


from rest_framework import serializers

from collectionjson.fields import ItemLinkField
from core.utils import get_file_resource_link

from .models import UploadedFile


class UploadedFileSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
    file_resource = ItemLinkField('get_file_link')
    fname = serializers.FileField(use_url=False)
    fsize = serializers.ReadOnlyField(source='fname.size')
    upload_path = serializers.CharField(write_only=True)

    class Meta:
        model = UploadedFile
        fields = ('url', 'id', 'creation_date', 'upload_path', 'fname', 'fsize',
                  'file_resource', 'owner')

    def get_file_link(self, obj):
        """
        Custom method to get the hyperlink to the actual file resource.
        """
        return get_file_resource_link(self, obj)

    def validate_upload_path(self, upload_path):
        """
        Overriden to check whether the provided path is under <username>/<uploads>/.
        """
        # remove leading and trailing slashes
        upload_path = upload_path.strip(' ').strip('/')
        user = self.context['request'].user
        prefix = '{}/{}/'.format(user.username, 'uploads')
        if not upload_path.startswith(prefix):
            error_msg = "File path must start with '%s'." % prefix
            raise serializers.ValidationError([error_msg])
        return upload_path

    def validate(self, data):
        """
        Overriden to attach the upload path to the owner object.
        """
        # remove upload_path as it is not part of the model and attach it to the owner obj
        upload_path = data.pop('upload_path')
        owner = self.context['request'].user
        owner.upload_path = upload_path
        data['owner'] = owner
        return data


# chris_backend/uploadedfiles/views.py


import logging

from django.conf import settings
from django.http import FileResponse
from rest_framework import generics, permissions
from rest_framework.reverse import reverse

from collectionjson import services
from core.renderers import BinaryFileRenderer
from core.swiftmanager import SwiftManager

from .models import UploadedFile, UploadedFileFilter
from .serializers import UploadedFileSerializer
from .permissions import IsOwnerOrChris


logger = logging.getLogger(__name__)


class UploadedFileList(generics.ListCreateAPIView):
    """
    A view for the collection of uploaded user files.
    """
    http_method_names = ['get', 'post']
    queryset = UploadedFile.objects.all()
    serializer_class = UploadedFileSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def get_queryset(self):
        """
        Overriden to return a custom queryset that is only comprised by the files
        owned by the currently authenticated user.
        """
        user = self.request.user
        # if the user is chris then return all the files in the sandboxed filesystem
        if user.username == 'chris':
            return UploadedFile.objects.all()
        return UploadedFile.objects.filter(owner=user)

    def list(self, request, *args, **kwargs):
        """
        Overriden to append document-level link relations and a collection+json
        template to the response.
        """
        response = super(UploadedFileList, self).list(request, *args, **kwargs)
        # append query list
        query_list = [reverse('uploadedfile-list-query-search', request=request)]
        response = services.append_collection_querylist(response, query_list)
        # append write template
        template_data = {'upload_path': "", 'fname': ""}
        return services.append_collection_template(response, template_data)


class UploadedFileListQuerySearch(generics.ListAPIView):
    """
    A view for the collection of uploaded files resulting from a query search.
    """
    http_method_names = ['get']
    serializer_class = UploadedFileSerializer
    queryset = UploadedFile.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    filterset_class = UploadedFileFilter


class UploadedFileDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    An uploaded file view.
    """
    http_method_names = ['get', 'put', 'delete']
    queryset = UploadedFile.objects.all()
    serializer_class = UploadedFileSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        response = super(UploadedFileDetail, self).retrieve(request, *args, **kwargs)
        template_data = {"upload_path": ""}
        return services.append_collection_template(response, template_data)

    def update(self, request, *args, **kwargs):
        """
        Overriden to include the current fname in the request.
        """
        user_file = self.get_object()
        request.data['fname'] = user_file.fname.file  # fname required in the serializer
        return super(UploadedFileDetail, self).update(request, *args, **kwargs)

    def perform_update(self, serializer):
        """
        Overriden to delete the old path in swift.
        """
        user_file = self.get_object()
        old_swift_path = user_file.fname.name
        serializer.save()
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        try:
            swift_manager.delete_obj(old_swift_path)
        except Exception as e:
            logger.error('Swift storage error, detail: %s' % str(e))

    def perform_destroy(self, instance):
        """
        Overriden to delete the file from swift storage.
        """
        swift_path = instance.fname.name
        instance.delete()
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        try:
            swift_manager.delete_obj(swift_path)
        except Exception as e:
            logger.error('Swift storage error, detail: %s' % str(e))


class UploadedFileResource(generics.GenericAPIView):
    """
    A view to enable downloading of a file resource .
    """
    http_method_names = ['get']
    queryset = UploadedFile.objects.all()
    renderer_classes = (BinaryFileRenderer,)
    permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris)

    def get(self, request, *args, **kwargs):
        """
        Overriden to be able to make a GET request to an actual file resource.
        """
        user_file = self.get_object()
        return FileResponse(user_file.fname)


# chris_backend/users/admin.py

from django.contrib import admin

# Register your models here.


# chris_backend/users/apps.py

from django.apps import AppConfig


class UsersConfig(AppConfig):
    name = 'users'


# chris_backend/users/models.py

from django.db import models

# Create your models here.


# chris_backend/users/permissions.py

from rest_framework import permissions


class IsUserOrChrisOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object or superuser
    'chris' to modify/edit it. Read only is allowed to other users.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the authenticated user and
        # superuser 'chris'.
        return (obj == request.user) or (request.user.username == 'chris')




# chris_backend/users/serializers.py


import logging
import io

from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import serializers
from rest_framework.validators import UniqueValidator

from core.swiftmanager import SwiftManager
from uploadedfiles.models import UploadedFile


logger = logging.getLogger(__name__)


class UserSerializer(serializers.HyperlinkedModelSerializer):
    feed = serializers.HyperlinkedRelatedField(many=True, view_name='feed-detail',
                                               read_only=True)
    username = serializers.CharField(min_length=4, max_length=32,
                                     validators=[UniqueValidator(
                                         queryset=User.objects.all())])
    email = serializers.EmailField(required=True,
                                   validators=[UniqueValidator(
                                       queryset=User.objects.all())])
    password = serializers.CharField(min_length=8, max_length=100, write_only=True)

    class Meta:
        model = User
        fields = ('url', 'id', 'username', 'email', 'password', 'feed')

    def create(self, validated_data):
        """
        Overriden to take care of the password hashing and create a welcome file
        for the user in its personal storage space.
        """
        username = validated_data.get('username')
        email = validated_data.get('email')
        password = validated_data.get('password')
        user = User.objects.create_user(username, email, password)
        swift_manager = SwiftManager(settings.SWIFT_CONTAINER_NAME,
                                     settings.SWIFT_CONNECTION_PARAMS)
        welcome_file_path = '%s/uploads/welcome.txt' % username
        try:
            with io.StringIO('Welcome to ChRIS!') as f:
                swift_manager.upload_obj(welcome_file_path, f.read(),
                                         content_type='text/plain')
            welcome_file = UploadedFile(owner=user)
            welcome_file.fname.name = welcome_file_path
            welcome_file.save()
        except Exception as e:
            logger.error('Could not create welcome file in user space, detail: %s' %
                         str(e))
        return user

    def validate_username(self, username):
        """
        Overriden to check that the username does not contain forward slashes and it is
        not 'chris' or 'SERVICES' special identifiers.
        """
        if '/' in username:
            raise serializers.ValidationError(
                ["This field may not contain forward slashes."])
        if username == 'chris' or username == 'SERVICES':
            raise serializers.ValidationError(
                ["Username %s is not available." % username])
        return username


# chris_backend/users/views.py


from django.contrib.auth.models import User
from rest_framework import generics, permissions
from rest_framework.response import Response

from collectionjson import services

from .serializers import UserSerializer
from .permissions import IsUserOrChrisOrReadOnly


class UserCreate(generics.ListCreateAPIView):
    http_method_names = ['get', 'post']
    queryset = User.objects.all()
    serializer_class = UserSerializer

    def list(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json write template.
        """
        response = services.get_list_response(self, [])
        template_data = {"username": "", "password": "", "email": ""}
        return services.append_collection_template(response, template_data)


class UserDetail(generics.RetrieveUpdateAPIView):
    http_method_names = ['get', 'put']
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = (permissions.IsAuthenticated, IsUserOrChrisOrReadOnly)

    def retrieve(self, request, *args, **kwargs):
        """
        Overriden to append a collection+json template.
        """
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        response = Response(serializer.data)
        template_data = {"password": "", "email": ""}
        return services.append_collection_template(response, template_data)

    def update(self, request, *args, **kwargs):
        """
        Overriden to add required username before serializer validation.
        """
        user = self.get_object()
        request.data['username'] = user.username
        return super(UserDetail, self).update(request, *args, **kwargs)

    def perform_update(self, serializer):
        """
        Overriden to update user's password and email when requested by a PUT request.
        """
        serializer.save(email=serializer.validated_data.get("email"))
        user = self.get_object()
        password = serializer.validated_data.get("password")
        user.set_password(password)
        user.save()


# chris_backend/manage.py

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()


# chris_backend/migratedb.py

#!/usr/bin/env python

import time
import sys
import psycopg2
from argparse import ArgumentParser

# django needs to be loaded
import django
django.setup()

from django.core.management import call_command

from core.models import ChrisInstance


parser = ArgumentParser(description="Check database service connection")
parser.add_argument('-u', '--user', help="Database user name")
parser.add_argument('-p', '--password', help="Database user password")
parser.add_argument('-d', '--database', help="Database name")
parser.add_argument('--host', help="Database host")
parser.add_argument('--max-attempts', type=int, dest='attempts',
                    help="Maximum number of connection attempts")
parser.add_argument('--noinput', action='store_true',
                    help="Perform migrations in non-interactive mode")


# Parse the arguments and perform the appropriate action
args = parser.parse_args()

host = args.host if args.host else 'localhost'
max_tries = args.attempts if args.attempts else 30
db = None
while max_tries > 0 and db is None:
    try:
        db = psycopg2.connect(host=host, user=args.user, password=args.password,
                              dbname=args.database)
    except Exception:
        time.sleep(5)
        max_tries -= 1

if db is None:
    print('Could not connect to database service!')
    sys.exit(1)
else:
    print('Database service ready to accept connections!')
    if args.noinput:
        call_command("migrate", interactive=False)
    else:
        call_command("migrate", interactive=True)
    ChrisInstance.load()


# chris_backend/workflows/tests/test_views.py

import logging
import json
from unittest import mock

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
from rest_framework import status

from plugins.models import PluginMeta, Plugin
from plugins.models import ComputeResource
from plugins.models import PluginParameter, DefaultStrParameter, DefaultIntParameter
from plugininstances.models import PluginInstance
from pipelines.models import Pipeline, PluginPiping
from workflows.models import Workflow
from workflows import views


COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL


class ViewTests(TestCase):
    
    def setUp(self):
        # avoid cluttered console output (for instance logging all the http requests)
        logging.disable(logging.WARNING)

        self.content_type = 'application/vnd.collection+json'

        self.plugin_fs_name = "simplefsapp"
        self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True,
                                             'default': "./"}}
        self.plugin_ds_name = "simpledsapp"
        self.plugin_ds_parameters = {'dummyInt': {'type': 'integer', 'optional': True,
                                                  'default': 111111}}
        self.username = 'foo'
        self.password = 'foo-pass'

        (self.compute_resource, tf) = ComputeResource.objects.get_or_create(
            name="host", compute_url=COMPUTE_RESOURCE_URL)

        # create plugins
        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_fs_name, type='fs')
        (plugin_fs, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_fs.compute_resources.set([self.compute_resource])
        plugin_fs.save()

        (pl_meta, tf) = PluginMeta.objects.get_or_create(name=self.plugin_ds_name, type='ds')
        (plugin_ds, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1')
        plugin_ds.compute_resources.set([self.compute_resource])
        plugin_ds.save()

        # add plugins' parameters
        (plg_param_fs, tf) = PluginParameter.objects.get_or_create(
            plugin=plugin_fs,
            name='dir',
            type=self.plugin_fs_parameters['dir']['type'],
            optional=self.plugin_fs_parameters['dir']['optional'])
        default = self.plugin_fs_parameters['dir']['default']
        DefaultStrParameter.objects.get_or_create(plugin_param=plg_param_fs,
                                                   value=default)  # set plugin parameter default

        # add a parameter with a default
        (plg_param_ds, tf)= PluginParameter.objects.get_or_create(
            plugin=plugin_ds,
            name='dummyInt',
            type=self.plugin_ds_parameters['dummyInt']['type'],
            optional=self.plugin_ds_parameters['dummyInt']['optional']
        )
        default = self.plugin_ds_parameters['dummyInt']['default']
        DefaultIntParameter.objects.get_or_create(plugin_param=plg_param_ds,
                                                  value=default)  # set plugin parameter default

        # create user
        user = User.objects.create_user(username=self.username, password=self.password)

        # create a pipeline
        self.pipeline_name = 'Pipeline1'
        (pipeline, tf) = Pipeline.objects.get_or_create(name=self.pipeline_name,
                                                        owner=user, category='test')

        # create two plugin pipings
        self.pips = []
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds,
                                                       pipeline=pipeline)
        self.pips.append(pip)
        (pip, tf) = PluginPiping.objects.get_or_create(plugin=plugin_ds, previous=pip,
                                                       pipeline=pipeline)
        self.pips.append(pip)

        # create another user
        self.other_username = 'boo'
        self.other_password = 'far'
        User.objects.create_user(username=self.other_username, password=self.other_password)

    def tearDown(self):
        # re-enable logging
        logging.disable(logging.NOTSET)


class WorkflowListViewTests(ViewTests):
    """
    Test the workflow-list view.
    """

    def setUp(self):
        super(WorkflowListViewTests, self).setUp()
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        self.create_read_url = reverse("workflow-list", kwargs={"pk": pipeline.id})
        owner = User.objects.get(username=self.username)
        plugin_fs = Plugin.objects.get(meta__name=self.plugin_fs_name)
        (self.pl_inst, tf) = PluginInstance.objects.get_or_create(
            status='finishedSuccessfully',
            plugin=plugin_fs, owner=owner,
            compute_resource=plugin_fs.compute_resources.all()[0]
        )

    def test_workflow_create_success(self):
        post = json.dumps(
            {"template": {"data": [{"name": "previous_plugin_inst_id", "value": self.pl_inst.id},
                                   {"name": "nodes_info",
                                    "value": json.dumps([{"piping_id": self.pips[0].id,
                                           "compute_resource_name": "host", "title": "Inst1",
                                           "plugin_parameter_defaults": [
                                               {"name": "dummyInt", "default": 3}]},
                                          {"piping_id": self.pips[1].id,
                                           "compute_resource_name": "host"}])}]}})

        plg_instances_count = PluginInstance.objects.count()
        with mock.patch.object(views.run_plugin_instance, 'delay',
                               return_value=None) as delay_mock:
            self.client.login(username=self.username, password=self.password)
            response = self.client.post(self.create_read_url, data=post,
                                        content_type=self.content_type)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
            self.assertEqual(PluginInstance.objects.count(), plg_instances_count + 2)

    def test_workflow_list_success(self):
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        Workflow.objects.get_or_create(created_plugin_inst_ids="1,2",
                                               pipeline=pipeline,
                                               owner=owner)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.create_read_url)
        self.assertContains(response, "1,2")

    def test_workflow_list_failure_unauthenticated(self):
        response = self.client.get(self.create_read_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class WorkflowListQuerySearchViewTests(ViewTests):
    """
    Test the workflow-list-query-search view.
    """

    def setUp(self):
        super(WorkflowListQuerySearchViewTests, self).setUp()

        self.query_url = reverse("allworkflow-list-query-search") + f"?owner_username={self.username}"

    def test_workflow_query_search_list_success(self):
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        Workflow.objects.get_or_create(created_plugin_inst_ids="1,2",
                                               pipeline=pipeline,
                                               owner=owner)
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.query_url)
        self.assertContains(response, self.username)

    def test_workflow_query_search_list_failure_unauthenticated(self):
        response = self.client.get(self.query_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class WorkflowDetailViewTests(ViewTests):
    """
    Test the workflow-detail view.
    """

    def setUp(self):
        super(WorkflowDetailViewTests, self).setUp()
        pipeline = Pipeline.objects.get(name=self.pipeline_name)
        owner = User.objects.get(username=self.username)
        (workflow, tf) = Workflow.objects.get_or_create(created_plugin_inst_ids="1,2",
                                                        pipeline=pipeline, owner=owner)
        self.read_delete_url = reverse("workflow-detail", kwargs={"pk": workflow.id})

    def test_workflow_detail_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(self.read_delete_url)
        self.assertContains(response, "1,2")

    def test_workflow_detail_failure_unauthenticated(self):
        response = self.client.get(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_workflow_delete_success(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.delete(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Workflow.objects.count(), 0)

    def test_workflow_delete_failure_unauthenticated(self):
        response = self.client.delete(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_workflow_delete_failure_access_denied(self):
        self.client.login(username=self.other_username, password=self.other_password)
        response = self.client.delete(self.read_delete_url)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
