Repository: ckan/ckanext-showcase (language: python)

Issue:
Ensure Travis uses CKAN 2.3 when available
Change which version of CKAN Travis checkout to a stable release in travis-build.bash.



Discussion / hints:
Fixed in ca98eb88f174c11869c6785b93e504c8c793bc50.


Fix patch:
diff --git a/.gitignore b/.gitignore
index c21b782c..14608b91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,8 @@ sdist/
 *.egg-info/
 .installed.cfg
 *.egg
+.eggs/
+.venv
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
@@ -39,4 +41,4 @@ nosetests.xml
 coverage.xml
 
 # Sphinx documentation
-docs/_build/
\ No newline at end of file
+docs/_build/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 02ea5f19..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-language: python
-
-# the new trusty images of Travis cause build errors with psycopg2, see https://github.com/travis-ci/travis-ci/issues/8897
-dist: trusty
-group: deprecated-2017Q4
-
-python:
-    - "2.7"
-env:
-    - CKANVERSION=master
-    - CKANVERSION=2.8
-    - CKANVERSION=2.7
-    - CKANVERSION=2.6
-    - CKANVERSION=2.5
-    - CKANVERSION=2.4
-    - CKANVERSION=2.3
-install:
-    - bash bin/travis-build.bash
-services:
-    -postgresql
-script: sh bin/travis-run.sh
-after_success: coveralls
-sudo: required
diff --git a/README.rst b/README.rst
index 54e3c9f9..d23a44a4 100644
--- a/README.rst
+++ b/README.rst
@@ -31,7 +31,7 @@ Requirements
 ------------
 
 
-Compatible with CKAN 2.3+.
+Compatible with CKAN 2.9.
 
 N.B. The ``migrate`` command, detailed below, requires the Related Item models
 and actions, which have been removed in CKAN 2.6. If you wish to migrate your
diff --git a/bin/travis-build.bash b/bin/travis-build.bash
deleted file mode 100644
index 2e088487..00000000
--- a/bin/travis-build.bash
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/bash
-set -e
-
-echo "This is travis-build.bash..."
-
-echo "Installing the packages that CKAN requires..."
-sudo apt-get update -qq
-sudo apt-get install solr-jetty libcommons-fileupload-java
-
-echo "Installing CKAN and its Python dependencies..."
-git clone https://github.com/ckan/ckan
-cd ckan
-if [ $CKANVERSION == 'master' ]
-then
-    echo "CKAN version: master"
-else
-    CKAN_TAG=$(git tag | grep ^ckan-$CKANVERSION | sort --version-sort | tail -n 1)
-    git checkout $CKAN_TAG
-    echo "CKAN version: ${CKAN_TAG#ckan-}"
-fi
-python setup.py develop
-if [ -f requirements-py2.txt ]
-then
-    pip install -r requirements-py2.txt
-else
-    pip install -r requirements.txt
-fi
-pip install -r dev-requirements.txt --allow-all-external
-cd -
-
-echo "Creating the PostgreSQL user and database..."
-sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';"
-sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;'
-
-echo "Initialising the database..."
-cd ckan
-paster db init -c test-core.ini
-cd -
-
-echo "Installing ckanext-showcase and its requirements..."
-python setup.py develop
-pip install -r dev-requirements.txt
-
-echo "Moving test.ini into a subdir..."
-mkdir subdir
-mv test-travis.ini subdir
-
-echo "travis-build.bash is done."
diff --git a/bin/travis-run.sh b/bin/travis-run.sh
deleted file mode 100644
index d3f36649..00000000
--- a/bin/travis-run.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh -e
-
-echo "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty
-sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml
-sudo service jetty restart
-nosetests --ckan --nologcapture --with-pylons=subdir/test-travis.ini --with-coverage --cover-package=ckanext.showcase --cover-inclusive --cover-erase --cover-tests
diff --git a/ckanext/showcase/commands/cli.py b/ckanext/showcase/commands/cli.py
new file mode 100644
index 00000000..4df2423f
--- /dev/null
+++ b/ckanext/showcase/commands/cli.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+
+import click
+
+from ckanext.showcase import utils
+
+# Click commands for CKAN 2.9 and above
+
+
+@click.group()
+def showcase():
+    '''showcase commands
+    '''
+    pass
+
+
+@showcase.command()
+@click.option('--allow-duplicates',
+                default=False,
+                help='Allow related items with duplicate titles to be migrated. Duplicate showcases will be created as "duplicate_<related-name>_<related-id>".')
+def migrate(allow_duplicates):
+    """
+        showcase migrate [options]
+    """
+    utils.migrate(allow_duplicates)
+
+
+@showcase.command()
+def markdown_to_html():
+    '''
+        showcase markdown-to-html
+    '''
+    utils.markdown_to_html()
+
+
+def get_commands():
+    return [showcase]
\ No newline at end of file
diff --git a/ckanext/showcase/commands/migrate.py b/ckanext/showcase/commands/migrate.py
deleted file mode 100644
index 569c8de2..00000000
--- a/ckanext/showcase/commands/migrate.py
+++ /dev/null
@@ -1,183 +0,0 @@
-from ckan import model
-from ckan.lib.cli import CkanCommand
-from ckan.lib.munge import munge_title_to_name, substitute_ascii_equivalents
-from ckan.logic import get_action
-from ckan.lib.helpers import render_markdown
-from ckan.plugins import toolkit
-
-
-import logging
-log = logging.getLogger(__name__)
-
-
-class MigrationCommand(CkanCommand):
-    '''
-    CKAN 'Related Items' to 'Showcase' migration command.
-
-    Usage::
-
-        paster showcase migrate -c <path to config file>
-            - Migrate Related Items to Showcases
-
-        paster showcase migrate -c <path to config file> [--allow-duplicates]
-            - Migrate Related Items to Showcases and allow duplicates
-
-        paster showcase markdown-to-html -c <path to config file>
-            - Migrate the notes of all showcases from markdown to html.
-
-    Must be run from the ckanext-showcase directory.
-    '''
-    summary = __doc__.split('\n')[0]
-    usage = __doc__
-
-    def __init__(self,name):
-        super(CkanCommand, self).__init__(name)
-
-        self.parser.add_option('--allow-duplicates', dest='allow_duplicates',
-                            default=False, help='''Use this option to allow
-                            related items with duplicate titles to be migrated.
-                            Duplicate showcases will be created as
-                            'duplicate_<related-name>_<related-id>'.''', action='store_true')
-
-    def command(self):
-        '''
-        Parse command line arguments and call appropriate method.
-        '''
-        if not self.args or self.args[0] in ['--help', '-h', 'help']:
-            print(self.__doc__)
-            return
-
-        cmd = self.args[0]
-        self._load_config()
-
-        if cmd == 'migrate':
-            self.migrate()
-        elif cmd == 'markdown-to-html':
-            self.markdown_to_html()
-        else:
-            print('Command "{0}" not recognized'.format(cmd))
-
-    def migrate(self):
-        '''
-
-        '''
-        # determine whether migration should allow duplicates
-        allow_duplicates = self.options.allow_duplicates
-
-        related_items = get_action('related_list')(data_dict={})
-
-        # preflight:
-        # related items must have unique titles before migration
-        related_titles = [i['title'] for i in related_items]
-        # make a list of duplicate titles
-        duplicate_titles = self._find_duplicates(related_titles)
-        if duplicate_titles and allow_duplicates == False:
-            print(
-                """All Related Items must have unique titles before migration. The following
-Related Item titles are used more than once and need to be corrected before
-migration can continue. Please correct and try again:"""
-            )
-            for i in duplicate_titles:
-                print(i)
-            return
-
-        for related in related_items:
-            existing_showcase = get_action('package_search')(
-                data_dict={'fq': '+dataset_type:showcase original_related_item_id:{0}'.format(related['id'])})
-            normalized_title = substitute_ascii_equivalents(related['title'])
-            if existing_showcase['count'] > 0:
-                print('Showcase for Related Item "{0}" already exists.'.format(
-                    normalized_title))
-            else:
-                showcase_title = self._gen_new_title(related.get('title'), related['id'])
-                data_dict = {
-                    'original_related_item_id': related.get('id'),
-                    'title': showcase_title,
-                    'name': munge_title_to_name(showcase_title),
-                    'notes': related.get('description'),
-                    'image_url': related.get('image_url'),
-                    'url': related.get('url'),
-                    'tags': [{"name": related.get('type').lower()}]
-                }
-                # make the showcase
-                try:
-                    new_showcase = get_action('ckanext_showcase_create')(
-                        data_dict=data_dict)
-                except Exception as e:
-                    print('There was a problem migrating "{0}": {1}'.format(
-                        normalized_title, e))
-                else:
-                    print('Created Showcase from the Related Item "{0}"'.format(normalized_title))
-
-                    # make the showcase_package_association, if needed
-                    try:
-                        related_pkg_id = self._get_related_dataset(
-                            related['id'])
-                        if related_pkg_id:
-                            get_action('ckanext_showcase_package_association_create')(
-                                data_dict={'showcase_id': new_showcase['id'],
-                                           'package_id': related_pkg_id})
-                    except Exception as e:
-                        print('There was a problem creating the showcase_package_association for "{0}": {1}'.format(
-                            normalized_title, e))
-
-    def _get_related_dataset(self, related_id):
-        '''Get the id of a package from related_dataset, if one exists.'''
-        related_dataset = model.Session.query(model.RelatedDataset).filter_by(
-            related_id=related_id).first()
-        if related_dataset:
-            return related_dataset.dataset_id
-
-    def _find_duplicates(self, lst):
-        '''From a list, return a set of duplicates.
-
-        >>> MigrationCommand('cmd')._find_duplicates([1, 2, 3, 4, 5])
-        []
-
-        >>> MigrationCommand('cmd')._find_duplicates([1, 2, 3, 4, 3, 1, 1])
-        [1, 3]
-
-        >>> MigrationCommand('cmd')._find_duplicates(['one', 'two', 'three', 'four', 'two', 'three'])
-        ['two', 'three']
-        '''
-        return list(set(x for x in lst if lst.count(x) >= 2))
-
-    def _gen_new_title(self, title, related_id):
-        name = munge_title_to_name(title)
-        pkg_obj = model.Session.query(model.Package).filter_by(name=name).first()
-        if pkg_obj:
-            title.replace('duplicate_', '')
-            return 'duplicate_' + title + '_' + related_id
-        else:
-            return title
-
-    def markdown_to_html(self):
-        ''' Migrates the notes of all showcases from markdown to html.
-
-        When using CKEditor, notes on showcases are stored in html instead of
-        markdown, this command will migrate all nothes using CKAN's
-        render_markdown core helper.
-        '''
-        showcases = toolkit.get_action('ckanext_showcase_list')(data_dict={})
-
-        site_user = toolkit.get_action('get_site_user')({
-            'model': model,
-            'ignore_auth': True},
-            {}
-        )
-        context = {
-            'model': model,
-            'session': model.Session,
-            'ignore_auth': True,
-            'user': site_user['name'],
-        }
-
-        for showcase in showcases:
-            toolkit.get_action('package_patch')(
-                context,
-                {
-                    'id': showcase['id'],
-                    'notes': render_markdown(showcase['notes'])
-                }
-            )
-        print('All notes were migrated successfully.')
diff --git a/ckanext/showcase/commands/paster.py b/ckanext/showcase/commands/paster.py
new file mode 100644
index 00000000..c58a63fe
--- /dev/null
+++ b/ckanext/showcase/commands/paster.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+
+from ckan.lib.cli import CkanCommand
+
+from ckanext.showcase import utils
+
+# Paster commands for CKAN 2.8 and below
+
+
+class MigrationCommand(CkanCommand):
+    '''
+    CKAN 'Related Items' to 'Showcase' migration command.
+
+    Usage::
+
+        paster showcase migrate -c <path to config file>
+            - Migrate Related Items to Showcases
+
+        paster showcase migrate -c <path to config file> [--allow-duplicates]
+            - Migrate Related Items to Showcases and allow duplicates
+
+        paster showcase markdown-to-html -c <path to config file>
+            - Migrate the notes of all showcases from markdown to html.
+
+    Must be run from the ckanext-showcase directory.
+    '''
+    summary = __doc__.split('\n')[0]
+    usage = __doc__
+
+    def __init__(self, name):
+        super(CkanCommand, self).__init__(name)
+
+        self.parser.add_option('--allow-duplicates',
+                               dest='allow_duplicates',
+                               default=False,
+                               help='''Use this option to allow
+                            related items with duplicate titles to be migrated.
+                            Duplicate showcases will be created as
+                            'duplicate_<related-name>_<related-id>'.''',
+                               action='store_true')
+
+    def command(self):
+        '''
+        Parse command line arguments and call appropriate method.
+        '''
+        if not self.args or self.args[0] in ['--help', '-h', 'help']:
+            print(self.__doc__)
+            return
+
+        cmd = self.args[0]
+        self._load_config()
+
+        if cmd == 'migrate':
+            self.migrate()
+        elif cmd == 'markdown-to-html':
+            self.markdown_to_html()
+        else:
+            print('Command "{0}" not recognized'.format(cmd))
+
+    def migrate(self):
+        utils.migrate(self.options.allow_duplicates)
+
+    def markdown_to_html(self):
+        utils.markdown_to_html()
diff --git a/ckanext/showcase/controller.py b/ckanext/showcase/controller.py
index 8340b655..d0ad1272 100644
--- a/ckanext/showcase/controller.py
+++ b/ckanext/showcase/controller.py
@@ -1,22 +1,16 @@
-from urllib import urlencode
 import logging
 import json
 
-from pylons import config
 
 from ckan.plugins import toolkit as tk
-import ckan.model as model
 import ckan.lib.helpers as h
 import ckan.lib.navl.dictization_functions as dict_fns
 import ckan.logic as logic
-import ckan.plugins as p
-from ckan.common import OrderedDict, ungettext
-from ckan.controllers.package import (PackageController,
-                                      url_with_params,
-                                      _encode_params)
+from ckan.controllers.package import (PackageController)
 
-from ckanext.showcase.model import ShowcasePackageAssociation
-from ckanext.showcase.plugin import DATASET_TYPE_NAME
+
+from ckanext.showcase import utils
+from ckanext.showcase.utils import DATASET_TYPE_NAME
 
 _ = tk._
 c = tk.c
@@ -33,46 +27,24 @@
 parse_params = logic.parse_params
 NotAuthorized = tk.NotAuthorized
 
-
 log = logging.getLogger(__name__)
 
 
 class ShowcaseController(PackageController):
-
     def new(self, data=None, errors=None, error_summary=None):
 
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'auth_user_obj': c.userobj,
-                   'save': 'save' in request.params}
-
-        # Check access here, then continue with PackageController.new()
-        # PackageController.new will also check access for package_create.
-        # This is okay for now, while only sysadmins can create Showcases, but
-        # may not work if we allow other users to create Showcases, who don't
-        # have access to create dataset package types. Same for edit below.
-        try:
-            check_access('ckanext_showcase_create', context)
-        except NotAuthorized:
-            abort(401, _('Unauthorized to create a package'))
-
-        return super(ShowcaseController, self).new(data=data, errors=errors,
+        utils.check_new_view_auth()
+        return super(ShowcaseController, self).new(data=data,
+                                                   errors=errors,
                                                    error_summary=error_summary)
 
     def edit(self, id, data=None, errors=None, error_summary=None):
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'auth_user_obj': c.userobj,
-                   'save': 'save' in request.params,
-                   'moderated': config.get('moderated'),
-                   'pending': True}
-
-        try:
-            check_access('ckanext_showcase_update', context)
-        except NotAuthorized:
-            abort(401, _('User not authorized to edit {showcase_id}').format(
-                showcase_id=id))
-
-        return super(ShowcaseController, self).edit(
-            id, data=data, errors=errors, error_summary=error_summary)
+        utils.check_edit_view_auth(id)
+        return super(ShowcaseController,
+                     self).edit(id,
+                                data=data,
+                                errors=errors,
+                                error_summary=error_summary)
 
     def _guess_package_type(self, expecting_name=False):
         """Showcase packages are always DATASET_TYPE_NAME."""
@@ -85,8 +57,8 @@ def _save_new(self, context, package_type=None):
         associated packages with the new showcase.
         '''
 
-        data_dict = clean_dict(dict_fns.unflatten(
-                tuplize_dict(parse_params(request.POST))))
+        data_dict = clean_dict(
+            dict_fns.unflatten(tuplize_dict(parse_params(request.POST))))
 
         data_dict['type'] = package_type
         context['message'] = data_dict.get('log_message', '')
@@ -101,9 +73,7 @@ def _save_new(self, context, package_type=None):
             return self.new(data_dict, errors, error_summary)
 
         # redirect to manage datasets
-        url = h.url_for(
-            controller='ckanext.showcase.controller:ShowcaseController',
-            action='manage_datasets', id=pkg_dict['name'])
+        url = h.url_for('showcase_manage_datasets', id=pkg_dict['name'])
         redirect(url)
 
     def _save_edit(self, name_or_id, context, package_type=None):
@@ -111,8 +81,8 @@ def _save_edit(self, name_or_id, context, package_type=None):
         Edit a showcase's details, then redirect to the showcase read page.
         '''
 
-        data_dict = clean_dict(dict_fns.unflatten(
-            tuplize_dict(parse_params(request.POST))))
+        data_dict = clean_dict(
+            dict_fns.unflatten(tuplize_dict(parse_params(request.POST))))
 
         data_dict['id'] = name_or_id
         try:
@@ -125,9 +95,7 @@ def _save_edit(self, name_or_id, context, package_type=None):
         c.pkg_dict = pkg
 
         # redirect to showcase details page
-        url = h.url_for(
-            controller='ckanext.showcase.controller:ShowcaseController',
-            action='read', id=pkg['name'])
+        url = h.url_for('showcase_read', id=pkg['name'])
         redirect(url)
 
     def read(self, id, format='html'):
@@ -135,485 +103,29 @@ def read(self, id, format='html'):
         Detail view for a single showcase, listing its associated datasets.
         '''
 
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'for_view': True,
-                   'auth_user_obj': c.userobj}
-        data_dict = {'id': id}
-
-        # check if showcase exists
-        try:
-            c.pkg_dict = get_action('package_show')(context, data_dict)
-        except NotFound:
-            abort(404, _('Showcase not found'))
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read showcase'))
-
-        # get showcase packages
-        c.showcase_pkgs = get_action('ckanext_showcase_package_list')(
-            context, {'showcase_id': c.pkg_dict['id']})
-
-        package_type = DATASET_TYPE_NAME
-        return render(self._read_template(package_type),
-                      extra_vars={'dataset_type': package_type})
+        return utils.read_view(id)
 
     def delete(self, id):
-        if 'cancel' in request.params:
-            tk.redirect_to(
-                controller='ckanext.showcase.controller:ShowcaseController',
-                action='edit', id=id)
-
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'auth_user_obj': c.userobj}
-
-        try:
-            check_access('ckanext_showcase_delete', context, {'id': id})
-        except NotAuthorized:
-            abort(401, _('Unauthorized to delete showcase'))
-
-        try:
-            if request.method == 'POST':
-                get_action('ckanext_showcase_delete')(context, {'id': id})
-                h.flash_notice(_('Showcase has been deleted.'))
-                tk.redirect_to(
-                    controller='ckanext.showcase.controller:ShowcaseController',
-                    action='search')
-            c.pkg_dict = get_action('package_show')(context, {'id': id})
-        except NotAuthorized:
-            abort(401, _('Unauthorized to delete showcase'))
-        except NotFound:
-            abort(404, _('Showcase not found'))
-        return render('showcase/confirm_delete.html',
-                      extra_vars={'dataset_type': DATASET_TYPE_NAME})
+        return utils.delete_view(id)
 
     def dataset_showcase_list(self, id):
         '''
         Display a list of showcases a dataset is associated with, with an
         option to add to showcase from a list.
         '''
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'for_view': True,
-                   'auth_user_obj': c.userobj}
-        data_dict = {'id': id}
-
-        try:
-            check_access('package_show', context, data_dict)
-        except NotFound:
-            abort(404, _('Dataset not found'))
-        except NotAuthorized:
-            abort(401, _('Not authorized to see this page'))
-
-        try:
-            c.pkg_dict = get_action('package_show')(context, data_dict)
-            c.showcase_list = get_action('ckanext_package_showcase_list')(
-                context, {'package_id': c.pkg_dict['id']})
-        except NotFound:
-            abort(404, _('Dataset not found'))
-        except logic.NotAuthorized:
-            abort(401, _('Unauthorized to read package'))
-
-        if request.method == 'POST':
-            # Are we adding the dataset to a showcase?
-            new_showcase = request.POST.get('showcase_added')
-            if new_showcase:
-                data_dict = {"showcase_id": new_showcase,
-                             "package_id": c.pkg_dict['id']}
-                try:
-                    get_action('ckanext_showcase_package_association_create')(
-                        context, data_dict)
-                except NotFound:
-                    abort(404, _('Showcase not found'))
-                else:
-                    h.flash_success(
-                        _("The dataset has been added to the showcase."))
-
-            # Are we removing a dataset from a showcase?
-            showcase_to_remove = request.POST.get('remove_showcase_id')
-            if showcase_to_remove:
-                data_dict = {"showcase_id": showcase_to_remove,
-                             "package_id": c.pkg_dict['id']}
-                try:
-                    get_action('ckanext_showcase_package_association_delete')(
-                        context, data_dict)
-                except NotFound:
-                    abort(404, _('Showcase not found'))
-                else:
-                    h.flash_success(
-                        _("The dataset has been removed from the showcase."))
-            redirect(h.url_for(
-                controller='ckanext.showcase.controller:ShowcaseController',
-                action='dataset_showcase_list', id=c.pkg_dict['name']))
-
-        pkg_showcase_ids = [showcase['id'] for showcase in c.showcase_list]
-        site_showcases = get_action('ckanext_showcase_list')(context, {})
-
-        c.showcase_dropdown = [[showcase['id'], showcase['title']]
-                               for showcase in site_showcases
-                               if showcase['id'] not in pkg_showcase_ids]
-
-        return render("package/dataset_showcase_list.html")
+        return utils.dataset_showcase_list(id)
 
     def manage_datasets(self, id):
         '''
         List datasets associated with the given showcase id.
         '''
-
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author}
-        data_dict = {'id': id}
-
-        try:
-            check_access('ckanext_showcase_update', context)
-        except NotAuthorized:
-            abort(401, _('User not authorized to edit {showcase_id}').format(
-                showcase_id=id))
-
-        # check if showcase exists
-        try:
-            c.pkg_dict = get_action('package_show')(context, data_dict)
-        except NotFound:
-            abort(404, _('Showcase not found'))
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read showcase'))
-
-        # Are we removing a showcase/dataset association?
-        if (request.method == 'POST'
-                and 'bulk_action.showcase_remove' in request.params):
-            # Find the datasets to perform the action on, they are prefixed by
-            # dataset_ in the form data
-            dataset_ids = []
-            for param in request.params:
-                if param.startswith('dataset_'):
-                    dataset_ids.append(param[8:])
-            if dataset_ids:
-                for dataset_id in dataset_ids:
-                    get_action('ckanext_showcase_package_association_delete')(
-                        context,
-                        {'showcase_id': c.pkg_dict['id'],
-                         'package_id': dataset_id})
-                h.flash_success(
-                    ungettext(
-                        "The dataset has been removed from the showcase.",
-                        "The datasets have been removed from the showcase.",
-                        len(dataset_ids)))
-                url = h.url_for(
-                    controller='ckanext.showcase.controller:ShowcaseController',
-                    action='manage_datasets', id=id)
-                redirect(url)
-
-        # Are we creating a showcase/dataset association?
-        elif (request.method == 'POST'
-                and 'bulk_action.showcase_add' in request.params):
-            # Find the datasets to perform the action on, they are prefixed by
-            # dataset_ in the form data
-            dataset_ids = []
-            for param in request.params:
-                if param.startswith('dataset_'):
-                    dataset_ids.append(param[8:])
-            if dataset_ids:
-                successful_adds = []
-                for dataset_id in dataset_ids:
-                    try:
-                        get_action(
-                            'ckanext_showcase_package_association_create')(
-                                context, {'showcase_id': c.pkg_dict['id'],
-                                          'package_id': dataset_id})
-                    except ValidationError as e:
-                        h.flash_notice(e.error_summary)
-                    else:
-                        successful_adds.append(dataset_id)
-                if successful_adds:
-                    h.flash_success(
-                        ungettext(
-                            "The dataset has been added to the showcase.",
-                            "The datasets have been added to the showcase.",
-                            len(successful_adds)))
-                url = h.url_for(
-                    controller='ckanext.showcase.controller:ShowcaseController',
-                    action='manage_datasets', id=id)
-                redirect(url)
-
-        self._add_dataset_search(c.pkg_dict['id'], c.pkg_dict['name'])
-
-        # get showcase packages
-        c.showcase_pkgs = get_action('ckanext_showcase_package_list')(
-            context, {'showcase_id': c.pkg_dict['id']})
-
-        return render('showcase/manage_datasets.html')
-
-    def _search_url(self, params, name):
-        url = h.url_for(
-            controller='ckanext.showcase.controller:ShowcaseController',
-            action='manage_datasets', id=name)
-        return url_with_params(url, params)
-
-    def _add_dataset_search(self, showcase_id, showcase_name):
-        '''
-        Search logic for discovering datasets to add to a showcase.
-        '''
-
-        from ckan.lib.search import SearchError
-
-        package_type = 'dataset'
-
-        # unicode format (decoded from utf8)
-        q = c.q = request.params.get('q', u'')
-        c.query_error = False
-        try:
-            page = self._get_page_number(request.params)
-        except AttributeError:
-            # in CKAN >= 2.5 _get_page_number has been moved
-            page = h.get_page_number(request.params)
-
-        limit = int(config.get('ckan.datasets_per_page', 20))
-
-        # most search operations should reset the page counter:
-        params_nopage = [(k, v) for k, v in request.params.items()
-                         if k != 'page']
-
-        def drill_down_url(alternative_url=None, **by):
-            return h.add_url_param(alternative_url=alternative_url,
-                                   controller='package', action='search',
-                                   new_params=by)
-
-        c.drill_down_url = drill_down_url
-
-        def remove_field(key, value=None, replace=None):
-            return h.remove_url_param(key, value=value, replace=replace,
-                                      controller='package', action='search')
-
-        c.remove_field = remove_field
-
-        sort_by = request.params.get('sort', None)
-        params_nosort = [(k, v) for k, v in params_nopage if k != 'sort']
-
-        def _sort_by(fields):
-            """
-            Sort by the given list of fields.
-
-            Each entry in the list is a 2-tuple: (fieldname, sort_order)
-
-            eg - [('metadata_modified', 'desc'), ('name', 'asc')]
-
-            If fields is empty, then the default ordering is used.
-            """
-            params = params_nosort[:]
-
-            if fields:
-                sort_string = ', '.join('%s %s' % f for f in fields)
-                params.append(('sort', sort_string))
-            return self._search_url(params, showcase_name)
-
-        c.sort_by = _sort_by
-        if sort_by is None:
-            c.sort_by_fields = []
-        else:
-            c.sort_by_fields = [field.split()[0]
-                                for field in sort_by.split(',')]
-
-        def pager_url(q=None, page=None):
-            params = list(params_nopage)
-            params.append(('page', page))
-            return self._search_url(params, showcase_name)
-
-        c.search_url_params = urlencode(_encode_params(params_nopage))
-
-        try:
-            c.fields = []
-            # c.fields_grouped will contain a dict of params containing
-            # a list of values eg {'tags':['tag1', 'tag2']}
-            c.fields_grouped = {}
-            search_extras = {}
-            fq = ''
-            for (param, value) in request.params.items():
-                if param not in ['q', 'page', 'sort'] \
-                        and len(value) and not param.startswith('_'):
-                    if not param.startswith('ext_'):
-                        c.fields.append((param, value))
-                        fq += ' %s:"%s"' % (param, value)
-                        if param not in c.fields_grouped:
-                            c.fields_grouped[param] = [value]
-                        else:
-                            c.fields_grouped[param].append(value)
-                    else:
-                        search_extras[param] = value
-
-            context = {'model': model, 'session': model.Session,
-                       'user': c.user or c.author, 'for_view': True,
-                       'auth_user_obj': c.userobj}
-
-            if package_type and package_type != 'dataset':
-                # Only show datasets of this particular type
-                fq += ' +dataset_type:{type}'.format(type=package_type)
-            else:
-                # Unless changed via config options, don't show non standard
-                # dataset types on the default search page
-                if not tk.asbool(config.get('ckan.search.show_all_types',
-                                            'False')):
-                    fq += ' +dataset_type:dataset'
-
-            # Only search for packages that aren't already associated with the
-            # Showcase
-            associated_package_ids = ShowcasePackageAssociation.get_package_ids_for_showcase(showcase_id)
-            # flatten resulting list to space separated string
-            if associated_package_ids:
-                associated_package_ids_str = \
-                    ' OR '.join([id[0] for id in associated_package_ids])
-                fq += ' !id:({0})'.format(associated_package_ids_str)
-
-            facets = OrderedDict()
-
-            default_facet_titles = {
-                    'organization': _('Organizations'),
-                    'groups': _('Groups'),
-                    'tags': _('Tags'),
-                    'res_format': _('Formats'),
-                    'license_id': _('Licenses'),
-                    }
-
-            # for CKAN-Versions that do not provide the facets-method from
-            # helper-context, import facets from ckan.common
-            if hasattr(h, 'facets'):
-                current_facets = h.facets()
-            else:
-                from ckan.common import g
-                current_facets = g.facets
-
-            for facet in current_facets:
-                if facet in default_facet_titles:
-                    facets[facet] = default_facet_titles[facet]
-                else:
-                    facets[facet] = facet
-
-            # Facet titles
-            for plugin in p.PluginImplementations(p.IFacets):
-                facets = plugin.dataset_facets(facets, package_type)
-
-            c.facet_titles = facets
-
-            data_dict = {
-                'q': q,
-                'fq': fq.strip(),
-                'facet.field': facets.keys(),
-                'rows': limit,
-                'start': (page - 1) * limit,
-                'sort': sort_by,
-                'extras': search_extras
-            }
-
-            query = get_action('package_search')(context, data_dict)
-            c.sort_by_selected = query['sort']
-
-            c.page = h.Page(
-                collection=query['results'],
-                page=page,
-                url=pager_url,
-                item_count=query['count'],
-                items_per_page=limit
-            )
-            c.facets = query['facets']
-            c.search_facets = query['search_facets']
-            c.page.items = query['results']
-        except SearchError, se:
-            log.error('Dataset search error: %r', se.args)
-            c.query_error = True
-            c.facets = {}
-            c.search_facets = {}
-            c.page = h.Page(collection=[])
-        c.search_facets_limits = {}
-        for facet in c.search_facets.keys():
-            try:
-                limit = int(request.params.get('_%s_limit' % facet,
-                                               int(config.get('search.facets.default', 10))))
-            except ValueError:
-                abort(400, _("Parameter '{parameter_name}' is not an integer").format(
-                                 parameter_name='_%s_limit' % facet
-                             ))
-            c.search_facets_limits[facet] = limit
+        return utils.manage_datasets_view(id)
 
     def manage_showcase_admins(self):
-        '''
-        A ckan-admin page to list and add showcase admin users.
-        '''
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author}
-
-        try:
-            check_access('sysadmin', context, {})
-        except NotAuthorized:
-            abort(401, _('User not authorized to view page'))
-
-        # We're trying to add a user to the showcase admins list.
-        if request.method == 'POST' and request.params['username']:
-            username = request.params['username']
-            try:
-                get_action('ckanext_showcase_admin_add')(
-                    data_dict={'username': username})
-            except NotAuthorized:
-                abort(401, _('Unauthorized to perform that action'))
-            except NotFound:
-                h.flash_error(_("User '{user_name}' not found.").format(
-                    user_name=username))
-            except ValidationError as e:
-                h.flash_notice(e.error_summary)
-            else:
-                h.flash_success(_("The user is now a Showcase Admin"))
-
-            return redirect(h.url_for(
-                controller='ckanext.showcase.controller:ShowcaseController',
-                action='manage_showcase_admins'))
-
-        c.showcase_admins = get_action('ckanext_showcase_admin_list')()
-
-        return render('admin/manage_showcase_admins.html')
+        return utils.manage_showcase_admins()
 
     def remove_showcase_admin(self):
-        '''
-        Remove a user from the Showcase Admin list.
-        '''
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author}
-
-        try:
-            check_access('sysadmin', context, {})
-        except NotAuthorized:
-            abort(401, _('User not authorized to view page'))
-
-        if 'cancel' in request.params:
-            tk.redirect_to(
-                controller='ckanext.showcase.controller:ShowcaseController',
-                action='manage_showcase_admins')
-
-        user_id = request.params['user']
-        if request.method == 'POST' and user_id:
-            user_id = request.params['user']
-            try:
-                get_action('ckanext_showcase_admin_remove')(
-                    data_dict={'username': user_id})
-            except NotAuthorized:
-                abort(401, _('Unauthorized to perform that action'))
-            except NotFound:
-                h.flash_error(_('The user is not a Showcase Admin'))
-            else:
-                h.flash_success(_('The user is no longer a Showcase Admin'))
-
-            return redirect(h.url_for(
-                controller='ckanext.showcase.controller:ShowcaseController',
-                action='manage_showcase_admins'))
-
-        c.user_dict = get_action('user_show')(data_dict={'id': user_id})
-        c.user_id = user_id
-        return render('admin/confirm_remove_showcase_admin.html')
+        return utils.remove_showcase_admin()
 
     def showcase_upload(self):
-        if not tk.request.method == 'POST':
-            tk.abort(409, _('Only Posting is availiable'))
-
-        try:
-            url = tk.get_action('ckanext_showcase_upload')(
-                None,
-                dict(tk.request.POST)
-                )
-        except tk.NotAuthorized:
-            tk.abort(401, _('Unauthorized to upload file %s') % id)
-
-        return json.dumps(url)
+        return utils.upload()
diff --git a/ckanext/showcase/fanstatic/.gitignore b/ckanext/showcase/fanstatic/.gitignore
deleted file mode 100644
index e69de29b..00000000
diff --git a/ckanext/showcase/fanstatic/resource.config b/ckanext/showcase/fanstatic/resource.config
new file mode 100644
index 00000000..07a8ea2a
--- /dev/null
+++ b/ckanext/showcase/fanstatic/resource.config
@@ -0,0 +1,8 @@
+[depends]
+
+ckeditor = base/main
+
+[groups]
+ckeditor =
+         dist/ckeditor.js
+          js/showcase-ckeditor.js
diff --git a/ckanext/showcase/fanstatic/webassets.yml b/ckanext/showcase/fanstatic/webassets.yml
new file mode 100644
index 00000000..d4ce090b
--- /dev/null
+++ b/ckanext/showcase/fanstatic/webassets.yml
@@ -0,0 +1,21 @@
+ckeditor:
+  filter: rjsmin
+  output: ckanext-showcase/%(version)s_ckeditor.json
+  extra:
+    preload:
+      - base/main
+  contents:
+    - dist/ckeditor.js
+    - js/showcase-ckeditor.js
+
+ckanext-showcase-css:
+  contents:
+    - ckanext_showcase.css
+  output: showcase/%(version)s_ckanext_showcase.css
+  filter: cssrewrite
+
+ckeditor-content-css:
+  contents:
+    - ckeditor-content-style.css
+  output: showcase/%(version)s_ckeditor-content-style.css
+  filter: cssrewrite
diff --git a/ckanext/showcase/logic/action/__init__.py b/ckanext/showcase/logic/action/__init__.py
index e69de29b..43ab2d14 100644
--- a/ckanext/showcase/logic/action/__init__.py
+++ b/ckanext/showcase/logic/action/__init__.py
@@ -0,0 +1,36 @@
+import ckanext.showcase.logic.action.create
+import ckanext.showcase.logic.action.delete
+import ckanext.showcase.logic.action.update
+import ckanext.showcase.logic.action.get
+
+
+def get_actions():
+    action_functions = {
+        'ckanext_showcase_create':
+            ckanext.showcase.logic.action.create.showcase_create,
+        'ckanext_showcase_update':
+            ckanext.showcase.logic.action.update.showcase_update,
+        'ckanext_showcase_delete':
+            ckanext.showcase.logic.action.delete.showcase_delete,
+        'ckanext_showcase_show':
+            ckanext.showcase.logic.action.get.showcase_show,
+        'ckanext_showcase_list':
+            ckanext.showcase.logic.action.get.showcase_list,
+        'ckanext_showcase_package_association_create':
+            ckanext.showcase.logic.action.create.showcase_package_association_create,
+        'ckanext_showcase_package_association_delete':
+            ckanext.showcase.logic.action.delete.showcase_package_association_delete,
+        'ckanext_showcase_package_list':
+            ckanext.showcase.logic.action.get.showcase_package_list,
+        'ckanext_package_showcase_list':
+            ckanext.showcase.logic.action.get.package_showcase_list,
+        'ckanext_showcase_admin_add':
+            ckanext.showcase.logic.action.create.showcase_admin_add,
+        'ckanext_showcase_admin_remove':
+            ckanext.showcase.logic.action.delete.showcase_admin_remove,
+        'ckanext_showcase_admin_list':
+            ckanext.showcase.logic.action.get.showcase_admin_list,
+        'ckanext_showcase_upload':
+            ckanext.showcase.logic.action.create.showcase_upload,
+    }
+    return action_functions
diff --git a/ckanext/showcase/logic/action/create.py b/ckanext/showcase/logic/action/create.py
index a952df4e..f58dafec 100644
--- a/ckanext/showcase/logic/action/create.py
+++ b/ckanext/showcase/logic/action/create.py
@@ -24,7 +24,6 @@ def showcase_create(context, data_dict):
 
     # force type to 'showcase'
     data_dict['type'] = 'showcase'
-
     # If get_uploader is available (introduced for IUploader in CKAN 2.5), use
     # it, otherwise use the default uploader.
     # https://github.com/ckan/ckan/pull/2510
diff --git a/ckanext/showcase/logic/auth.py b/ckanext/showcase/logic/auth.py
index 09f2c714..2d8f08e1 100644
--- a/ckanext/showcase/logic/auth.py
+++ b/ckanext/showcase/logic/auth.py
@@ -7,6 +7,24 @@
 log = logging.getLogger(__name__)
 
 
+def get_auth_functions():
+    return {
+        'ckanext_showcase_create': create,
+        'ckanext_showcase_update': update,
+        'ckanext_showcase_delete': delete,
+        'ckanext_showcase_show': show,
+        'ckanext_showcase_list': showcase_list,
+        'ckanext_showcase_package_association_create': package_association_create,
+        'ckanext_showcase_package_association_delete': package_association_delete,
+        'ckanext_showcase_package_list': showcase_package_list,
+        'ckanext_package_showcase_list': package_showcase_list,
+        'ckanext_showcase_admin_add': add_showcase_admin,
+        'ckanext_showcase_admin_remove': remove_showcase_admin,
+        'ckanext_showcase_admin_list': showcase_admin_list,
+        'ckanext_showcase_updload': showcase_upload,
+    }
+
+
 def _is_showcase_admin(context):
     '''
     Determines whether user in context is in the showcase admin list.
@@ -47,7 +65,7 @@ def show(context, data_dict):
 
 
 @toolkit.auth_allow_anonymous_access
-def list(context, data_dict):
+def showcase_list(context, data_dict):
     '''All users can access a showcase list'''
     return {'success': True}
 
diff --git a/ckanext/showcase/logic/helpers.py b/ckanext/showcase/logic/helpers.py
index 717e8e86..be6b87f8 100644
--- a/ckanext/showcase/logic/helpers.py
+++ b/ckanext/showcase/logic/helpers.py
@@ -9,8 +9,7 @@ def facet_remove_field(key, value=None, replace=None):
     '''
     return h.remove_url_param(
         key, value=value, replace=replace,
-        controller='ckanext.showcase.controller:ShowcaseController',
-        action='search')
+        alternative_url=h.url_for('showcase_index'))
 
 
 def get_site_statistics():
diff --git a/ckanext/showcase/logic/schema.py b/ckanext/showcase/logic/schema.py
index bcce6aa1..27f9f516 100644
--- a/ckanext/showcase/logic/schema.py
+++ b/ckanext/showcase/logic/schema.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+
+import six
 import ckan.plugins.toolkit as toolkit
 from ckan.lib.navl.validators import (not_empty,
                                       empty,
@@ -25,15 +28,15 @@ def showcase_base_schema():
     schema = {
         'id': [empty],
         'revision_id': [ignore],
-        'name': [not_empty, unicode, name_validator, package_name_validator],
-        'title': [if_empty_same_as("name"), unicode],
-        'author': [ignore_missing, unicode],
-        'author_email': [ignore_missing, unicode],
-        'notes': [ignore_missing, unicode],
-        'url': [ignore_missing, unicode],
+        'name': [not_empty, six.text_type, name_validator, package_name_validator],
+        'title': [if_empty_same_as("name"), six.text_type],
+        'author': [ignore_missing, six.text_type],
+        'author_email': [ignore_missing, six.text_type],
+        'notes': [ignore_missing, six.text_type],
+        'url': [ignore_missing, six.text_type],
         'state': [ignore_not_package_admin, ignore_missing],
-        'type': [ignore_missing, unicode],
-        'log_message': [ignore_missing, unicode, no_http],
+        'type': [ignore_missing, six.text_type],
+        'log_message': [ignore_missing, six.text_type, no_http],
         '__extras': [ignore],
         '__junk': [empty],
         'resources': default_resource_schema(),
@@ -65,11 +68,11 @@ def showcase_update_schema():
     # Supplying the package name when updating a package is optional (you can
     # supply the id to identify the package instead).
     schema['name'] = [ignore_missing, name_validator,
-                      package_name_validator, unicode]
+                      package_name_validator, six.text_type]
 
     # Supplying the package title when updating a package is optional, if it's
     # not supplied the title will not be changed.
-    schema['title'] = [ignore_missing, unicode]
+    schema['title'] = [ignore_missing, six.text_type]
 
     return schema
 
@@ -100,8 +103,8 @@ def showcase_show_schema():
     schema['metadata_modified'] = []
     schema['creator_user_id'] = []
     schema['num_tags'] = []
-    schema['revision_id'] = []
-    schema['tracking_summary'] = []
+    schema['revision_id'] = [ignore_missing]
+    schema['tracking_summary'] = [ignore_missing]
 
     schema.update({
         'image_url': [toolkit.get_converter('convert_from_extras'),
@@ -116,9 +119,9 @@ def showcase_show_schema():
 
 def showcase_package_association_create_schema():
     schema = {
-        'package_id': [not_empty, unicode,
+        'package_id': [not_empty, six.text_type,
                        convert_package_name_or_id_to_id_for_type_dataset],
-        'showcase_id': [not_empty, unicode,
+        'showcase_id': [not_empty, six.text_type,
                         convert_package_name_or_id_to_id_for_type_showcase]
     }
     return schema
@@ -130,7 +133,7 @@ def showcase_package_association_delete_schema():
 
 def showcase_package_list_schema():
     schema = {
-        'showcase_id': [not_empty, unicode,
+        'showcase_id': [not_empty, six.text_type,
                         convert_package_name_or_id_to_id_for_type_showcase]
     }
     return schema
@@ -138,7 +141,7 @@ def showcase_package_list_schema():
 
 def package_showcase_list_schema():
     schema = {
-        'package_id': [not_empty, unicode,
+        'package_id': [not_empty, six.text_type,
                        convert_package_name_or_id_to_id_for_type_dataset]
     }
     return schema
@@ -146,7 +149,7 @@ def package_showcase_list_schema():
 
 def showcase_admin_add_schema():
     schema = {
-        'username': [not_empty, user_id_or_name_exists, unicode],
+        'username': [not_empty, user_id_or_name_exists, six.text_type],
     }
     return schema
 
diff --git a/ckanext/showcase/plugin.py b/ckanext/showcase/plugin/__init__.py
similarity index 54%
rename from ckanext/showcase/plugin.py
rename to ckanext/showcase/plugin/__init__.py
index 5fdbe35f..3aeaef50 100644
--- a/ckanext/showcase/plugin.py
+++ b/ckanext/showcase/plugin/__init__.py
@@ -1,39 +1,47 @@
+# -*- coding: utf-8 -*-
+
 import os
 import sys
+import json
 import logging
+from collections import OrderedDict
+
+from six import string_types
 
 import ckan.plugins as plugins
 import ckan.lib.plugins as lib_plugins
 import ckan.lib.helpers as h
-from ckan.plugins import toolkit as tk
-from ckan.common import OrderedDict
 from ckan import model as ckan_model
 
-from routes.mapper import SubMapper
+import ckantoolkit as tk
+
+
+import ckanext.showcase.utils as utils
+from ckanext.showcase.logic import auth, action
 
-import ckanext.showcase.logic.auth
-import ckanext.showcase.logic.action.create
-import ckanext.showcase.logic.action.delete
-import ckanext.showcase.logic.action.update
-import ckanext.showcase.logic.action.get
 import ckanext.showcase.logic.schema as showcase_schema
 import ckanext.showcase.logic.helpers as showcase_helpers
 from ckanext.showcase.model import setup as model_setup
 
+if tk.check_ckan_version(u'2.9'):
+    from ckanext.showcase.plugin.flask_plugin import MixinPlugin
+else:
+    from ckanext.showcase.plugin.pylons_plugin import MixinPlugin
+
 c = tk.c
 _ = tk._
 
 log = logging.getLogger(__name__)
 
-DATASET_TYPE_NAME = 'showcase'
+DATASET_TYPE_NAME = utils.DATASET_TYPE_NAME
 
 
-class ShowcasePlugin(plugins.SingletonPlugin, lib_plugins.DefaultDatasetForm):
+class ShowcasePlugin(
+        MixinPlugin, plugins.SingletonPlugin, lib_plugins.DefaultDatasetForm):
     plugins.implements(plugins.IConfigurable)
     plugins.implements(plugins.IConfigurer)
     plugins.implements(plugins.IDatasetForm)
     plugins.implements(plugins.IFacets, inherit=True)
-    plugins.implements(plugins.IRoutes, inherit=True)
     plugins.implements(plugins.IAuthFunctions)
     plugins.implements(plugins.IActions)
     plugins.implements(plugins.IPackageController, inherit=True)
@@ -48,13 +56,30 @@ class ShowcasePlugin(plugins.SingletonPlugin, lib_plugins.DefaultDatasetForm):
     # IConfigurer
 
     def update_config(self, config):
-        tk.add_template_directory(config, 'templates')
-        tk.add_public_directory(config, 'public')
-        tk.add_resource('fanstatic', 'showcase')
+        tk.add_template_directory(config, '../templates')
+        tk.add_public_directory(config, '../public')
+        tk.add_resource('../fanstatic', 'showcase')
         if tk.check_ckan_version(min_version='2.4'):
-            tk.add_ckan_admin_tab(config, 'ckanext_showcase_admins',
+            tk.add_ckan_admin_tab(config, 'showcase_admins',
                                   'Showcase Config')
 
+        if tk.check_ckan_version(min_version='2.9.0'):
+            mappings = config.get('ckan.legacy_route_mappings', {})
+            if isinstance(mappings, string_types):
+                mappings = json.loads(mappings)
+
+            bp_routes = [
+                'index', 'new', 'delete',
+                'read', 'edit', 'manage_datasets',
+                'dataset_showcase_list', 'admins', 'admin_remove'
+            ]
+            mappings.update({
+                'showcase_' + route: 'showcase_blueprint.' + route
+                for route in bp_routes
+            })
+            # https://github.com/ckan/ckan/pull/4521
+            config['ckan.legacy_route_mappings'] = json.dumps(mappings)
+
     # IConfigurable
 
     def configure(self, config):
@@ -112,94 +137,12 @@ def dataset_facets(self, facets_dict, package_type):
     # IAuthFunctions
 
     def get_auth_functions(self):
-        return {
-            'ckanext_showcase_create': ckanext.showcase.logic.auth.create,
-            'ckanext_showcase_update': ckanext.showcase.logic.auth.update,
-            'ckanext_showcase_delete': ckanext.showcase.logic.auth.delete,
-            'ckanext_showcase_show': ckanext.showcase.logic.auth.show,
-            'ckanext_showcase_list': ckanext.showcase.logic.auth.list,
-            'ckanext_showcase_package_association_create':
-                ckanext.showcase.logic.auth.package_association_create,
-            'ckanext_showcase_package_association_delete':
-                ckanext.showcase.logic.auth.package_association_delete,
-            'ckanext_showcase_package_list':
-                ckanext.showcase.logic.auth.showcase_package_list,
-            'ckanext_package_showcase_list':
-                ckanext.showcase.logic.auth.package_showcase_list,
-            'ckanext_showcase_admin_add':
-                ckanext.showcase.logic.auth.add_showcase_admin,
-            'ckanext_showcase_admin_remove':
-                ckanext.showcase.logic.auth.remove_showcase_admin,
-            'ckanext_showcase_admin_list':
-                ckanext.showcase.logic.auth.showcase_admin_list,
-            'ckanext_showcase_upload':
-                ckanext.showcase.logic.auth.showcase_upload
-        }
-
-    # IRoutes
-
-    def before_map(self, map):
-        # These named routes are used for custom dataset forms which will use
-        # the names below based on the dataset.type ('dataset' is the default
-        # type)
-        with SubMapper(map, controller='ckanext.showcase.controller:ShowcaseController') as m:
-            m.connect('ckanext_showcase_index', '/showcase', action='search',
-                      highlight_actions='index search')
-            m.connect('ckanext_showcase_new', '/showcase/new', action='new')
-            m.connect('ckanext_showcase_delete', '/showcase/delete/{id}',
-                      action='delete')
-            m.connect('ckanext_showcase_read', '/showcase/{id}', action='read',
-                      ckan_icon='picture')
-            m.connect('ckanext_showcase_edit', '/showcase/edit/{id}',
-                      action='edit', ckan_icon='edit')
-            m.connect('ckanext_showcase_manage_datasets',
-                      '/showcase/manage_datasets/{id}',
-                      action="manage_datasets", ckan_icon="sitemap")
-            m.connect('dataset_showcase_list', '/dataset/showcases/{id}',
-                      action='dataset_showcase_list', ckan_icon='picture')
-            m.connect('ckanext_showcase_admins', '/ckan-admin/showcase_admins',
-                      action='manage_showcase_admins', ckan_icon='picture'),
-            m.connect('ckanext_showcase_admin_remove',
-                      '/ckan-admin/showcase_admin_remove',
-                      action='remove_showcase_admin'),
-            m.connect('showcase_upload', '/showcase_upload',
-                    action='showcase_upload')
-        map.redirect('/showcases', '/showcase')
-        map.redirect('/showcases/{url:.*}', '/showcase/{url}')
-        return map
+        return auth.get_auth_functions()
 
     # IActions
 
     def get_actions(self):
-        action_functions = {
-            'ckanext_showcase_create':
-                ckanext.showcase.logic.action.create.showcase_create,
-            'ckanext_showcase_update':
-                ckanext.showcase.logic.action.update.showcase_update,
-            'ckanext_showcase_delete':
-                ckanext.showcase.logic.action.delete.showcase_delete,
-            'ckanext_showcase_show':
-                ckanext.showcase.logic.action.get.showcase_show,
-            'ckanext_showcase_list':
-                ckanext.showcase.logic.action.get.showcase_list,
-            'ckanext_showcase_package_association_create':
-                ckanext.showcase.logic.action.create.showcase_package_association_create,
-            'ckanext_showcase_package_association_delete':
-                ckanext.showcase.logic.action.delete.showcase_package_association_delete,
-            'ckanext_showcase_package_list':
-                ckanext.showcase.logic.action.get.showcase_package_list,
-            'ckanext_package_showcase_list':
-                ckanext.showcase.logic.action.get.package_showcase_list,
-            'ckanext_showcase_admin_add':
-                ckanext.showcase.logic.action.create.showcase_admin_add,
-            'ckanext_showcase_admin_remove':
-                ckanext.showcase.logic.action.delete.showcase_admin_remove,
-            'ckanext_showcase_admin_list':
-                ckanext.showcase.logic.action.get.showcase_admin_list,
-            'ckanext_showcase_upload':
-                ckanext.showcase.logic.action.create.showcase_upload,
-        }
-        return action_functions
+        return action.get_actions()
 
     # IPackageController
 
diff --git a/ckanext/showcase/plugin/flask_plugin.py b/ckanext/showcase/plugin/flask_plugin.py
new file mode 100644
index 00000000..54cdef90
--- /dev/null
+++ b/ckanext/showcase/plugin/flask_plugin.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+import ckan.plugins as p
+import ckanext.showcase.views as views
+
+from ckanext.showcase.commands import cli
+
+
+class MixinPlugin(p.SingletonPlugin):
+    p.implements(p.IBlueprint)
+    p.implements(p.IClick)
+
+    # IBlueprint
+
+    def get_blueprint(self):
+        return views.get_blueprints()
+
+    # IClick
+
+    def get_commands(self):
+        return cli.get_commands()
diff --git a/ckanext/showcase/plugin/pylons_plugin.py b/ckanext/showcase/plugin/pylons_plugin.py
new file mode 100644
index 00000000..bda3916e
--- /dev/null
+++ b/ckanext/showcase/plugin/pylons_plugin.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+from routes.mapper import SubMapper
+import ckan.plugins as p
+
+
+class MixinPlugin(p.SingletonPlugin):
+    p.implements(p.IRoutes, inherit=True)
+
+    # IRoutes
+
+    def before_map(self, map):
+        # These named routes are used for custom dataset forms which will use
+        # the names below based on the dataset.type ('dataset' is the default
+        # type)
+        with SubMapper(
+                map, controller='ckanext.showcase.controller:ShowcaseController'
+        ) as m:
+            m.connect('showcase_index', '/showcase', action='search',
+                      highlight_actions='index search')
+            m.connect('showcase_new', '/showcase/new', action='new')
+            m.connect('showcase_delete', '/showcase/delete/{id}',
+                      action='delete')
+            m.connect('showcase_read', '/showcase/{id}', action='read',
+                      ckan_icon='picture')
+            m.connect('showcase_edit', '/showcase/edit/{id}',
+                      action='edit', ckan_icon='edit')
+            m.connect('showcase_manage_datasets',
+                      '/showcase/manage_datasets/{id}',
+                      action="manage_datasets", ckan_icon="sitemap")
+            m.connect('showcase_dataset_showcase_list', '/dataset/showcases/{id}',
+                      action='dataset_showcase_list', ckan_icon='picture')
+            m.connect('showcase_admins', '/ckan-admin/showcase_admins',
+                      action='manage_showcase_admins', ckan_icon='picture'),
+            m.connect('showcase_admin_remove',
+                      '/ckan-admin/showcase_admin_remove',
+                      action='remove_showcase_admin'),
+            m.connect('showcase_upload', '/showcase_upload',
+                      action='showcase_upload')
+        map.redirect('/showcases', '/showcase')
+        map.redirect('/showcases/{url:.*}', '/showcase/{url}')
+        return map
diff --git a/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html b/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html
index 73dd6a28..f37bf3bf 100644
--- a/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html
+++ b/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html
@@ -10,7 +10,7 @@
       {% block form %}
         <p>{{ _('Are you sure you want to remove this user as a Showcase Admin - {name}?').format(name=c.user_dict.name) }}</p>
         <p class="form-actions">
-          <form action="{% url_for controller='ckanext.showcase.controller:ShowcaseController', action='remove_showcase_admin' %}" method="post">
+            <form action="{{ h.url_for('showcase_admin_remove') }}" method="post">
             <input type="hidden" name="user" value="{{ c.user_id }}" />
             <button class="btn" type="submit" name="cancel" >{{ _('Cancel') }}</button>
             <button class="btn btn-primary" type="submit" name="delete" >{{ _('Confirm Remove') }}</button>
diff --git a/ckanext/showcase/templates/admin/manage_showcase_admins.html b/ckanext/showcase/templates/admin/manage_showcase_admins.html
index 10a3d2cb..15bc37b4 100644
--- a/ckanext/showcase/templates/admin/manage_showcase_admins.html
+++ b/ckanext/showcase/templates/admin/manage_showcase_admins.html
@@ -50,7 +50,7 @@ <h3 class="page-heading">{{ _('Showcase Admins') }}</h3>
           {{ h.linked_user(user_dict['id'], maxlength=20) }}
           {% set locale = h.dump_json({'content': _('Are you sure you want to remove this user from the Showcase Admin list?')}) %}
           <div class="btn-group pull-right">
-            <a class="btn btn-danger btn-small" href="{% url_for controller='ckanext.showcase.controller:ShowcaseController', action='remove_showcase_admin', user=user_dict['id'] %}" data-module="confirm-action" data-module-i18n="{{ locale }}" title="{{ _('Remove') }}">{% block delete_button_text %}<i class="icon-remove"></i> {{ _('Remove') }}{% endblock %}</a>
+            <a class="btn btn-danger btn-small" href="{{ h.url_for('showcase_admin_remove', user=user_dict['id']) }}" data-module="confirm-action" data-module-i18n="{{ locale }}" title="{{ _('Remove') }}">{% block delete_button_text %}<i class="icon-remove"></i> {{ _('Remove') }}{% endblock %}</a>
           </div>
         </td>
       </tr>
diff --git a/ckanext/showcase/templates/header.html b/ckanext/showcase/templates/header.html
index c66a2648..fd582d88 100644
--- a/ckanext/showcase/templates/header.html
+++ b/ckanext/showcase/templates/header.html
@@ -8,7 +8,7 @@
     ('search', _('Datasets')),
     ('organizations_index', _('Organizations')),
     ('group_index', _('Groups')),
-    ('ckanext_showcase_index', _('Showcases')),
+    ('showcase_index', _('Showcases')),
     (about_route, _('About'))
   ) }}
 {% endblock %}
diff --git a/ckanext/showcase/templates/home/snippets/stats.html b/ckanext/showcase/templates/home/snippets/stats.html
index 48c0f0e6..4e52ab9e 100644
--- a/ckanext/showcase/templates/home/snippets/stats.html
+++ b/ckanext/showcase/templates/home/snippets/stats.html
@@ -23,7 +23,7 @@ <h3>{{ _('{site_title} statistics').format(site_title=g.site_title) }}</h3>
         </a>
       </li>
       <li>
-        <a href="{{ h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='search') }}">
+        <a href="{{ h.url_for('showcase_index') }}">
           <b>{{ h.SI_number_span(stats.showcase_count) }}</b>
           {{ ungettext('showcase', 'showcases', stats.showcase_count) }}
         </a>
diff --git a/ckanext/showcase/templates/package/dataset_showcase_list.html b/ckanext/showcase/templates/package/dataset_showcase_list.html
index 4733d72e..972b3e6e 100644
--- a/ckanext/showcase/templates/package/dataset_showcase_list.html
+++ b/ckanext/showcase/templates/package/dataset_showcase_list.html
@@ -3,23 +3,23 @@
 {% block subtitle %}{{ _('Showcases') }} - {{ super() }}{% endblock %}
 
 {% block primary_content_inner %}
-  {% if h.check_access('ckanext_showcase_update') and c.showcase_dropdown %}
-    <form method="post" class="form-horizontal" id="showcase-add">
-      <select id="field-add_showcase" name="showcase_added" data-module="autocomplete">
-        {% for option in c.showcase_dropdown %}
-          <option value="{{ option[0] }}"> {{ option[1] }}</option>
-        {% endfor %}
-      </select>
-      <button type="submit" class="btn btn-primary" title="{{ _('Associate this showcase with this dataset') }}">{{ _('Add to dataset') }}</button>
-    </form>
-  {% endif %}
-
-  <h2>{% block page_heading %}{{ _('Showcases featuring {dataset_name}').format(dataset_name=h.dataset_display_name(c.pkg_dict)) }}{% endblock %}</h2>
-  {% block showcase_list %}
-    {% if c.showcase_list %}
-      {% snippet "showcase/snippets/showcase_list.html", packages=c.showcase_list, pkg_id=c.pkg_dict.name, show_remove=h.check_access('ckanext_showcase_update') %}
-    {% else %}
-      <p class="empty">{{ _('There are no showcases that feature this dataset') }}</p>
+    {% if h.check_access('ckanext_showcase_update') and c.showcase_dropdown %}
+        <form method="post" class="form-horizontal" id="showcase-add">
+            <select id="field-add_showcase" name="showcase_added" data-module="autocomplete">
+                {% for option in c.showcase_dropdown %}
+                    <option value="{{ option[0] }}"> {{ option[1] }}</option>
+                {% endfor %}
+            </select>
+            <button type="submit" class="btn btn-primary" title="{{ _('Associate this showcase with this dataset') }}">{{ _('Add to dataset') }}</button>
+        </form>
     {% endif %}
-  {% endblock %}
+
+    <h2>{% block page_heading %}{{ _('Showcases featuring {dataset_name}').format(dataset_name=h.dataset_display_name(c.pkg_dict)) }}{% endblock %}</h2>
+    {% block showcase_list %}
+        {% if c.showcase_list %}
+            {% snippet "showcase/snippets/showcase_list.html", packages=c.showcase_list, pkg_id=c.pkg_dict.name, show_remove=h.check_access('ckanext_showcase_update') %}
+        {% else %}
+            <p class="empty">{{ _('There are no showcases that feature this dataset') }}</p>
+        {% endif %}
+    {% endblock %}
 {% endblock %}
diff --git a/ckanext/showcase/templates/package/read_base.html b/ckanext/showcase/templates/package/read_base.html
index 4a2817d5..0af5adaf 100644
--- a/ckanext/showcase/templates/package/read_base.html
+++ b/ckanext/showcase/templates/package/read_base.html
@@ -2,5 +2,5 @@
 
 {% block content_primary_nav %}
   {{ super() }}
-  {{ h.build_nav_icon('dataset_showcase_list', _('Showcases'), id=pkg.name) }}
+  {{ h.build_nav_icon('showcase_dataset_showcase_list', _('Showcases'), id=pkg.name) }}
 {% endblock %}
diff --git a/ckanext/showcase/templates/showcase/confirm_delete.html b/ckanext/showcase/templates/showcase/confirm_delete.html
index 67b13113..6acfe581 100644
--- a/ckanext/showcase/templates/showcase/confirm_delete.html
+++ b/ckanext/showcase/templates/showcase/confirm_delete.html
@@ -1,21 +1,25 @@
 {% extends "page.html" %}
 
+{% set pkg = pkg_dict or c.pkg_dict %}
+
 {% block subtitle %}{{ _("Confirm Delete") }}{% endblock %}
 
-{% block maintag %}<div class="row" role="main">{% endblock %}
+{% block maintag %}
+    <div class="row" role="main">
+{% endblock %}
 
 {% block main_content %}
-  <section class="module span6 offset3">
-    <div class="module-content">
-      {% block form %}
-        <p>{{ _('Are you sure you want to delete showcase - {showcase_name}?').format(showcase_name=c.pkg_dict.name) }}</p>
-        <p class="form-actions">
-          <form action="{% url_for controller='ckanext.showcase.controller:ShowcaseController', action='delete', id=c.pkg_dict.name %}" method="post">
-            <button class="btn" type="submit" name="cancel" >{{ _('Cancel') }}</button>
-            <button class="btn btn-primary" type="submit" name="delete" >{{ _('Confirm Delete') }}</button>
-          </form>
-        </p>
-      {% endblock %}
-    </div>
-  </section>
+    <section class="module span6 offset3">
+        <div class="module-content">
+            {% block form %}
+                <p>{{ _('Are you sure you want to delete showcase - {showcase_name}?').format(showcase_name=pkg.name) }}</p>
+                <p class="form-actions">
+                    <form action="{{ h.url_for('showcase_delete', id=c.pkg_dict.name) }}" method="post">
+                        <button class="btn" type="submit" name="cancel" >{{ _('Cancel') }}</button>
+                        <button class="btn btn-primary" type="submit" name="delete" >{{ _('Confirm Delete') }}</button>
+                    </form>
+                </p>
+            {% endblock %}
+        </div>
+    </section>
 {% endblock %}
diff --git a/ckanext/showcase/templates/showcase/edit_base.html b/ckanext/showcase/templates/showcase/edit_base.html
index fe4a4ebf..5d897678 100644
--- a/ckanext/showcase/templates/showcase/edit_base.html
+++ b/ckanext/showcase/templates/showcase/edit_base.html
@@ -1,12 +1,13 @@
 {% extends 'page.html' %}
 
-{% set pkg = c.pkg_dict %}
+{% set pkg = pkg_dict or c.pkg_dict  %}
 
 {% block subtitle %}{{ _('Showcases') }}{% endblock %}
 
 {% block styles %}
   {{ super() }}
-  {% resource "showcase/ckanext_showcase.css" %}
+  {% set _type = 'asset' if h.ckan_version().split('.')[1] | int >= 9 else 'resource'  %}
+  {% snippet 'showcase/snippets/showcase_css_' ~ _type ~ '.html' %}
 {% endblock %}
 
 {% block breadcrumb_content_selected %}{% endblock %}
@@ -14,11 +15,11 @@
 {% block breadcrumb_content %}
   {% if pkg %}
     {% set showcase = pkg.title or pkg.name %}
-    <li>{% link_for _('Showcases'), controller='ckanext.showcase.controller:ShowcaseController', action='search' %}</li>
-    <li{{ self.breadcrumb_content_selected() }}>{% link_for showcase|truncate(30), controller='ckanext.showcase.controller:ShowcaseController', action='read', id=pkg.name %}</li>
-    <li class="active">{% link_for _('Edit'), controller='ckanext.showcase.controller:ShowcaseController', action='edit', id=pkg.name %}</li>
+    <li>{% link_for _('Showcases'), named_route='showcase_index' %}</li>
+    <li{{ self.breadcrumb_content_selected() }}>{% link_for showcase|truncate(30), named_route='showcase_read', id=pkg.name %}</li>
+    <li class="active">{% link_for _('Edit'), named_route='showcase_edit', id=pkg.name %}</li>
   {% else %}
-    <li>{% link_for _('Showcases'), controller='ckanext.showcase.controller:ShowcaseController', action='search' %}</li>
+    <li>{% link_for _('Showcases'), named_route='showcase_index' %}</li>
     <li class="active"><a href="">{{ _('Create Showcase') }}</a></li>
   {% endif %}
 {% endblock %}
@@ -32,14 +33,14 @@
             {% if self.content_action() | trim %}
               <div class="content_action">
                 {% block content_action %}
-                  {% link_for _('View showcase'), controller='ckanext.showcase.controller:ShowcaseController', action='read', id=pkg.name, class_='btn', icon='eye-open' %}
+                  {% link_for _('View showcase'), named_route='showcase_read', id=pkg.name, class_='btn', icon='eye-open' %}
                 {% endblock %}
               </div>
             {% endif %}
             <ul class="nav nav-tabs">
               {% block content_primary_nav %}
-                {{ h.build_nav_icon('ckanext_showcase_edit', _('Edit showcase'), id=pkg.name) }}
-                {{ h.build_nav_icon('ckanext_showcase_manage_datasets', _('Manage datasets'), id=pkg.name) }}
+                {{ h.build_nav_icon('showcase_edit', _('Edit showcase'), id=pkg.name) }}
+                {{ h.build_nav_icon('showcase_manage_datasets', _('Manage datasets'), id=pkg.name) }}
               {% endblock %}
             </ul>
           </header>
diff --git a/ckanext/showcase/templates/showcase/manage_datasets.html b/ckanext/showcase/templates/showcase/manage_datasets.html
index 10a35ef2..78eabb52 100644
--- a/ckanext/showcase/templates/showcase/manage_datasets.html
+++ b/ckanext/showcase/templates/showcase/manage_datasets.html
@@ -65,22 +65,22 @@ <h3 class="page-heading">{{ _('Datasets available to add to this showcase') }}</
                           <input type="checkbox" name="dataset_{{ package.id }}">
                         </td>
                         <td class="context">
-                          <h3 class="dataset-heading">
-                            {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
-                          </h3>
-                          {% if notes %}
-                            <p>{{ notes|urlize }}</p>
-                          {% endif %}
+                            <h3 class="dataset-heading">
+                                {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='dataset' if h.ckan_version().split('.')[1] | int >= 9 else 'package', action='read', id=package.name)) }}
+                            </h3>
+                            {% if notes %}
+                                <p>{{ notes|urlize }}</p>
+                            {% endif %}
                         </td>
                       </tr>
                     {% endfor %}
                   </tbody>
                   {% if c.page.pager() %}
-                    <tfoot>
-                      <tr>
-                        <td colspan="2" class="ckanext_showcase_pagination_footer">{{ c.page.pager(q=c.q) }}</td>
-                      </tr>
-                    </tfoot>
+                      <tfoot>
+                          <tr>
+                              <td colspan="2" class="ckanext_showcase_pagination_footer">{{ c.page.pager(q=c.q) }}</td>
+                          </tr>
+                      </tfoot>
                   {% endif %}
                 </table>
               </form>
@@ -92,39 +92,39 @@ <h3 class="dataset-heading">
       </section>
 
       <section class="span6">
-        <div class="module-content">
-          <h3 class="page-heading">{{ _('Datasets in this showcase') }}</h3>
-          {% if c.showcase_pkgs %}
-            <form method="POST" data-module="basic-form">
-              <table class="table table-bordered table-header table-hover table-bulk-edit table-edit-hover" data-module="table-selectable-rows">
-                <col width="8">
-                <col width="120">
-                <thead>
-                  <tr>
-                    <th></th>
-                    <th class="table-actions">
-                      <div class="btn-group">
-                        <button name="bulk_action.showcase_remove" value="remove" class="btn btn-danger" type="submit">
-                          <i class="fa fa-times icon-remove"></i>
-                          {{ _('Remove from Showcase') }}
-                        </button>
-                      </div>
-                    </th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {% for package in c.showcase_pkgs %}
-                    {% set truncate = truncate or 180 %}
-                    {% set truncate_title = truncate_title or 80 %}
-                    {% set title = package.title or package.name %}
-                    {% set notes = h.markdown_extract(package.notes, extract_length=truncate) %}
-                    <tr>
-                      <td>
-                        <input type="checkbox" name="dataset_{{ package.id }}">
-                      </td>
-                      <td class="context">
-                        <h3 class="dataset-heading">
-                          {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
+          <div class="module-content">
+              <h3 class="page-heading">{{ _('Datasets in this showcase') }}</h3>
+              {% if c.showcase_pkgs %}
+                  <form method="POST" data-module="basic-form">
+                      <table class="table table-bordered table-header table-hover table-bulk-edit table-edit-hover" data-module="table-selectable-rows">
+                          <col width="8">
+                          <col width="120">
+                          <thead>
+                              <tr>
+                                  <th></th>
+                                  <th class="table-actions">
+                                      <div class="btn-group">
+                                          <button name="bulk_action.showcase_remove" value="remove" class="btn btn-danger" type="submit">
+                                              <i class="fa fa-times icon-remove"></i>
+                                              {{ _('Remove from Showcase') }}
+                                          </button>
+                                      </div>
+                                  </th>
+                              </tr>
+                          </thead>
+                          <tbody>
+                              {% for package in c.showcase_pkgs %}
+                                  {% set truncate = truncate or 180 %}
+                                  {% set truncate_title = truncate_title or 80 %}
+                                  {% set title = package.title or package.name %}
+                                  {% set notes = h.markdown_extract(package.notes, extract_length=truncate) %}
+                                  <tr>
+                                      <td>
+                                          <input type="checkbox" name="dataset_{{ package.id }}">
+                                      </td>
+                                      <td class="context">
+                                          <h3 class="dataset-heading">
+                                              {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='dataset' if h.ckan_version().split('.')[1] | int >= 9 else 'package', action='read', id=package.name)) }}
                         </h3>
                         {% if notes %}
                           <p>{{ notes|urlize }}</p>
diff --git a/ckanext/showcase/templates/showcase/new_package_form.html b/ckanext/showcase/templates/showcase/new_package_form.html
index dcfee8a0..4be49730 100644
--- a/ckanext/showcase/templates/showcase/new_package_form.html
+++ b/ckanext/showcase/templates/showcase/new_package_form.html
@@ -16,8 +16,8 @@
       {% endblock %}
 
       {% block package_basic_fields_url %}
-        {% set prefix = h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='read', id='') %}
-        {% set domain = h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='read', id='', qualified=true) %}
+        {% set prefix = h.url_for('showcase_read', id='') %}
+        {% set domain = h.url_for('showcase_read', id='', qualified=true) %}
         {% set domain = domain|replace("http://", "")|replace("https://", "") %}
         {% set attrs = {'data-module': 'slug-preview-slug', 'data-module-prefix': domain, 'data-module-placeholder': '<showcase>'} %}
 
@@ -27,8 +27,8 @@
       {% block package_basic_fields_description %}
         {% set editor = h.get_wysiwyg_editor() %}
         {% if editor == 'ckeditor' %}
-          {% resource 'showcase/dist/ckeditor.js' %}
-          {% resource 'showcase/js/showcase-ckeditor.js' %}
+          {% set _type = 'asset' if h.ckan_version().split('.')[1] | int >= 9 else 'resource'  %}
+          {% snippet 'showcase/snippets/ckeditor_' ~ _type ~ '.html' %}
           {%
             set ckeditor_attrs = {
               'data-module': 'showcase-ckeditor',
@@ -64,11 +64,11 @@
   {% endblock %}
 
   {% block form_actions %}
-    <div class="form-actions">
-      {% block delete_button %}
-        {% if form_style == 'edit' and h.check_access('ckanext_showcase_delete', {'id': data.id}) and not data.state == 'deleted' %}
-          {% set locale = h.dump_json({'content': _('Are you sure you want to delete this showcase?')}) %}
-          <a class="btn btn-danger pull-left" href="{% url_for controller='ckanext.showcase.controller:ShowcaseController', action='delete', id=data.id %}" data-module="confirm-action" data-module-i18n="{{ locale }}">{% block delete_button_text %}{{ _('Delete') }}{% endblock %}</a>
+      <div class="form-actions">
+          {% block delete_button %}
+              {% if form_style == 'edit' and h.check_access('ckanext_showcase_delete', {'id': data.id}) and not data.state == 'deleted' %}
+                  {% set locale = h.dump_json({'content': _('Are you sure you want to delete this showcase?')}) %}
+                  <a class="btn btn-danger pull-left" href="{{ h.url_for('showcase_delete', id=data.id) }}" data-module="confirm-action" data-module-i18n="{{ locale }}">{% block delete_button_text %}{{ _('Delete') }}{% endblock %}</a>
         {% endif %}
       {% endblock %}
       {% block save_button %}
diff --git a/ckanext/showcase/templates/showcase/read.html b/ckanext/showcase/templates/showcase/read.html
index e2df3144..e12dd712 100644
--- a/ckanext/showcase/templates/showcase/read.html
+++ b/ckanext/showcase/templates/showcase/read.html
@@ -1,6 +1,6 @@
 {% extends "page.html" %}
 
-{% set pkg = c.pkg_dict %}
+{% set pkg = pkg_dict or c.pkg_dict %}
 {% set name = pkg.title or pkg.name %}
 {% set editor = h.get_wysiwyg_editor() %}
 
@@ -8,34 +8,35 @@
 
 {% block styles %}
   {{ super() }}
-  {% resource "showcase/ckanext_showcase.css" %}
+  {% set _type = 'asset' if h.ckan_version().split('.')[1] | int >= 9 else 'resource'  %}
+  {% snippet 'showcase/snippets/showcase_css_' ~ _type ~ '.html' %}
   {% if editor == 'ckeditor' %}
-    {% resource "showcase/ckeditor-content-style.css" %}
+    {% snippet 'showcase/snippets/ckeditor_content_css_' ~ _type ~ '.html' %}
   {% endif %}
 {% endblock %}
 
 {% block links -%}
-  {{ super() }}
-  <link rel="alternate" type="application/rdf+xml" href="{{ h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='read', id=pkg.id, format='rdf', qualified=True) }}"/>
+    {{ super() }}
+    <link rel="alternate" type="application/rdf+xml" href="{{ h.url_for('showcase_read', id=pkg.id, format='rdf', qualified=True) }}"/>
 {% endblock -%}
 
 {% block head_extras -%}
-  {{ super() }}
-  {% set description = h.markdown_extract(pkg.notes, extract_length=200)|forceescape %}
-  <meta property="og:title" content="{{ h.dataset_display_name(pkg) }} - {{ g.site_title }}">
-  <meta property="og:description" content="{{ description }}">
-  <meta property="og:url" content="{{ h.full_current_url() }}">
-  {% if pkg.image_display_url %}
-     <meta property="og:image" content="{{ pkg.image_display_url }}">
-  {% endif %}
+    {{ super() }}
+    {% set description = h.markdown_extract(pkg.notes, extract_length=200)|forceescape %}
+    <meta property="og:title" content="{{ h.dataset_display_name(pkg) }} - {{ g.site_title }}">
+    <meta property="og:description" content="{{ description }}">
+    <meta property="og:url" content="{{ h.full_current_url() }}">
+    {% if pkg.image_display_url %}
+        <meta property="og:image" content="{{ pkg.image_display_url }}">
+    {% endif %}
 {% endblock -%}
 
 {% block breadcrumb_content_selected %} class="active"{% endblock %}
 
 {% block breadcrumb_content %}
     {% set showcase = pkg.title or pkg.name %}
-    <li>{{ h.nav_link(_('Showcases'), controller='ckanext.showcase.controller:ShowcaseController', action='search', highlight_actions = 'new index') }}</li>
-    <li{{ self.breadcrumb_content_selected() }}>{% link_for showcase|truncate(30), controller='ckanext.showcase.controller:ShowcaseController', action='read', id=pkg.name %}</li>
+    <li>{{ h.nav_link(_('Showcases'), named_route='showcase_index', highlight_actions = 'new index') }}</li>
+    <li{{ self.breadcrumb_content_selected() }}>{% link_for showcase|truncate(30), named_route='showcase_read', id=pkg.name %}</li>
 {% endblock %}
 
 {% block page_header %}
@@ -45,30 +46,30 @@
 {% endblock %}
 
 {% block primary_content_inner %}
-  {% if h.check_access('ckanext_showcase_update', {'id':pkg.id }) %}
-    <div class="actions">
-    {% link_for _('Manage'), controller='ckanext.showcase.controller:ShowcaseController', action='edit', id=pkg.name, class_='btn', icon='wrench' %}
-    </div>
-  {% endif %}
-  {% block package_description %}
-    {% if pkg.private %}
-      <span class="dataset-private label label-inverse pull-right">
-        <i class="fa fa-lock icon-lock"></i>
-        {{ _('Private') }}
-      </span>
+    {% if h.check_access('ckanext_showcase_update', {'id':pkg.id }) %}
+        <div class="actions">
+            {% link_for _('Manage'), named_route='showcase_edit', id=pkg.name, class_='btn btn-default', icon='wrench' %}
+        </div>
     {% endif %}
-    <h1>
-      {% block page_heading %}
-        {{ name }}
-        {% if pkg.state.startswith('draft') %}
-          [{{ _('Draft') }}]
+    {% block package_description %}
+        {% if pkg.private %}
+            <span class="dataset-private label label-inverse pull-right">
+                <i class="fa fa-lock icon-lock"></i>
+                {{ _('Private') }}
+            </span>
+        {% endif %}
+        <h1>
+            {% block page_heading %}
+                {{ name }}
+                {% if pkg.state.startswith('draft') %}
+                    [{{ _('Draft') }}]
+                {% endif %}
+            {% endblock %}
+        </h1>
+
+        {% if pkg.image_display_url %}
+            <p class="ckanext-showcase-image-container"><img src="{{ pkg.image_display_url }}" alt="{{ name }}" class="media-image ckanext-showcase-image img-responsive"></p>
         {% endif %}
-      {% endblock %}
-    </h1>
-
-    {% if pkg.image_display_url %}
-      <p class="ckanext-showcase-image-container"><img src="{{ pkg.image_display_url }}" alt="{{ name }}" class="media-image ckanext-showcase-image img-responsive"></p>
-    {% endif %}
 
     {% block package_notes %}
       {% if pkg.showcase_notes_formatted and editor == 'ckeditor' %}
@@ -82,29 +83,29 @@ <h1>
       {% endif %}
     {% endblock %}
 
-    {% if pkg.url %}
-      <p><a class="btn btn-primary ckanext-showcase-launch" href="{{ pkg.url }}" target="_blank"><i class="fa fa-external-link icon-external-link"></i> {{ _('Launch website') }}</a></p>
-    {% endif %}
+        {% if pkg.url %}
+            <p><a class="btn btn-primary ckanext-showcase-launch" href="{{ pkg.url }}" target="_blank"><i class="fa fa-external-link icon-external-link"></i> {{ _('Launch website') }}</a></p>
+        {% endif %}
 
-  {% endblock %}
+    {% endblock %}
 
-  {% block package_tags %}
-    {% snippet "showcase/snippets/tags.html", tags=pkg.tags %}
-  {% endblock %}
+    {% block package_tags %}
+        {% snippet "showcase/snippets/tags.html", tags=pkg.tags %}
+    {% endblock %}
 
-  {% block package_search_results_list %}
-  {% endblock %}
+    {% block package_search_results_list %}
+    {% endblock %}
 
 {% endblock %}
 
 {% block secondary_content %}
-  {% block secondary_help_content %}{% endblock %}
+    {% block secondary_help_content %}{% endblock %}
 
-  {% block package_info %}
-    {% snippet 'showcase/snippets/showcase_info.html', pkg=pkg, showcase_pkgs=c.showcase_pkgs %}
-  {% endblock %}
+    {% block package_info %}
+        {% snippet 'showcase/snippets/showcase_info.html', pkg=pkg, showcase_pkgs=c.showcase_pkgs %}
+    {% endblock %}
 
-  {% block package_social %}
-    {% snippet "snippets/social.html" %}
-  {% endblock %}
+    {% block package_social %}
+        {% snippet "snippets/social.html" %}
+    {% endblock %}
 {% endblock %}
diff --git a/ckanext/showcase/templates/showcase/search.html b/ckanext/showcase/templates/showcase/search.html
index 4d0d35ab..488f9e97 100644
--- a/ckanext/showcase/templates/showcase/search.html
+++ b/ckanext/showcase/templates/showcase/search.html
@@ -4,13 +4,13 @@
 {% block subtitle %}{{ _("Showcases") }}{% endblock %}
 
 {% block breadcrumb_content %}
-  <li class="active">{{ h.nav_link(_('Showcases'), controller='ckanext.showcase.controller:ShowcaseController', action='search', highlight_actions = 'new index') }}</li>
+  <li class="active">{{ h.nav_link(_('Showcases'), named_route='showcase_index', highlight_actions = 'new index') }}</li>
 {% endblock %}
 
 {% block page_primary_action %}
   {% if h.check_access('ckanext_showcase_create') %}
     <div class="page_primary_action">
-      {% link_for _('Add Showcase'), controller='ckanext.showcase.controller:ShowcaseController', action='new', class_='btn btn-primary', icon='plus-sign-alt' %}
+      {% link_for _('Add Showcase'), named_route='showcase_new', class_='btn btn-primary', icon='plus-square' %}
     </div>
   {% endif %}
 {% endblock %}
diff --git a/ckanext/showcase/templates/showcase/snippets/ckeditor_asset.html b/ckanext/showcase/templates/showcase/snippets/ckeditor_asset.html
new file mode 100644
index 00000000..732e7e92
--- /dev/null
+++ b/ckanext/showcase/templates/showcase/snippets/ckeditor_asset.html
@@ -0,0 +1,1 @@
+{% asset 'showcase/ckeditor' %}
diff --git a/ckanext/showcase/templates/showcase/snippets/ckeditor_content_css_asset.html b/ckanext/showcase/templates/showcase/snippets/ckeditor_content_css_asset.html
new file mode 100644
index 00000000..d3bc8c9c
--- /dev/null
+++ b/ckanext/showcase/templates/showcase/snippets/ckeditor_content_css_asset.html
@@ -0,0 +1,1 @@
+{% asset "showcase/ckeditor-content-css" %}
\ No newline at end of file
diff --git a/ckanext/showcase/templates/showcase/snippets/ckeditor_content_css_resource.html b/ckanext/showcase/templates/showcase/snippets/ckeditor_content_css_resource.html
new file mode 100644
index 00000000..57ef2812
--- /dev/null
+++ b/ckanext/showcase/templates/showcase/snippets/ckeditor_content_css_resource.html
@@ -0,0 +1,1 @@
+{% resource "showcase/ckeditor-content-style.css" %}
\ No newline at end of file
diff --git a/ckanext/showcase/templates/showcase/snippets/ckeditor_resource.html b/ckanext/showcase/templates/showcase/snippets/ckeditor_resource.html
new file mode 100644
index 00000000..8a65271b
--- /dev/null
+++ b/ckanext/showcase/templates/showcase/snippets/ckeditor_resource.html
@@ -0,0 +1,1 @@
+{% resource 'showcase/ckeditor' %}
diff --git a/ckanext/showcase/templates/showcase/snippets/helper.html b/ckanext/showcase/templates/showcase/snippets/helper.html
index fdc1894f..0b2a7d19 100644
--- a/ckanext/showcase/templates/showcase/snippets/helper.html
+++ b/ckanext/showcase/templates/showcase/snippets/helper.html
@@ -9,7 +9,7 @@ <h2 class="module-heading">
     </p>
     {% if h.check_access('ckanext_showcase_admin_add') %}
       <p>
-        {% trans url=h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='manage_showcase_admins') %}Sysadmins can manage Showcase Admins from the <a href='{{ url }}'>Showcase configuration page</a>.{% endtrans %}
+        {% trans url=h.url_for('showcase_admins') %}Sysadmins can manage Showcase Admins from the <a href='{{ url }}'>Showcase configuration page</a>.{% endtrans %}
       </p>
     {% endif %}
   </div>
diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_css_asset.html b/ckanext/showcase/templates/showcase/snippets/showcase_css_asset.html
new file mode 100644
index 00000000..a7c6dabe
--- /dev/null
+++ b/ckanext/showcase/templates/showcase/snippets/showcase_css_asset.html
@@ -0,0 +1,1 @@
+{% asset "showcase/ckanext-showcase-css" %}
\ No newline at end of file
diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_css_resource.html b/ckanext/showcase/templates/showcase/snippets/showcase_css_resource.html
new file mode 100644
index 00000000..dee33209
--- /dev/null
+++ b/ckanext/showcase/templates/showcase/snippets/showcase_css_resource.html
@@ -0,0 +1,1 @@
+{% resource "showcase/ckanext_showcase.css" %}
\ No newline at end of file
diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_info.html b/ckanext/showcase/templates/showcase/snippets/showcase_info.html
index 957ae82e..a684a934 100644
--- a/ckanext/showcase/templates/showcase/snippets/showcase_info.html
+++ b/ckanext/showcase/templates/showcase/snippets/showcase_info.html
@@ -5,47 +5,47 @@
 
 Example:
 
-  {% snippet "package/snippets/info.html", pkg=pkg %}
+{% snippet "package/snippets/info.html", pkg=pkg %}
 
 #}
 {% block package_info %}
-  {% if pkg %}
-    <section class="module module-narrow">
-      <div class="module context-info">
-        <div class="module-content">
-          {% block package_info_inner %}
-            {% block heading %}
-              <h1 class="heading">{{ pkg.title or pkg.name }}</h1>
-            {% endblock %}
-            {% if pkg.author %}
-                <span class="smallest">{{_('Submitted by')}}</span>
-                <p>{{ pkg.author }}</p>
-            {% endif %}
-            {% if pkg.url %}
-              <div class="info">
-                <a class="btn btn-primary" href="{{ pkg.url }}" target="_blank">
-                  <i class="fa fa-external-link icon-external-link"></i> {{ _('Launch website') }}
-                </a>
-              </div>
-            {% endif %}
-          {% endblock %}
-        </div>
-      </div>
-    </section>
+    {% if pkg %}
+        <section class="module module-narrow">
+            <div class="module context-info">
+                <div class="module-content">
+                    {% block package_info_inner %}
+                        {% block heading %}
+                            <h1 class="heading">{{ pkg.title or pkg.name }}</h1>
+                        {% endblock %}
+                        {% if pkg.author %}
+                            <span class="smallest">{{_('Submitted by')}}</span>
+                            <p>{{ pkg.author }}</p>
+                        {% endif %}
+                        {% if pkg.url %}
+                            <div class="info">
+                                <a class="btn btn-primary" href="{{ pkg.url }}" target="_blank">
+                                    <i class="fa fa-external-link icon-external-link"></i> {{ _('Launch website') }}
+                                </a>
+                            </div>
+                        {% endif %}
+                    {% endblock %}
+                </div>
+            </div>
+        </section>
 
-    <section class="module module-narrow">
-      <h3 class="module-heading"><i class="fa fa-sitemap icon-medium icon-sitemap"></i> {{ _('Datasets in Showcase') }}</h2>
-      {% if showcase_pkgs %}
-        <ul class="nav nav-simple">
-        {% for package in showcase_pkgs %}
-          {% set truncate_title = truncate_title or 80 %}
-          {% set title = package.title or package.name %}
-          <li class="nav-item">{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}</li>
-        {% endfor %}
-        </ul>
-      {% else %}
-        <p class="module-content empty">{{_('There are no Datasets in this Showcase')}}</p>
-      {% endif %}
-    </section>
-  {% endif %}
+        <section class="module module-narrow">
+            <h3 class="module-heading"><i class="fa fa-sitemap icon-medium icon-sitemap"></i> {{ _('Datasets in Showcase') }}</h2>
+                {% if showcase_pkgs %}
+                    <ul class="nav nav-simple">
+                        {% for package in showcase_pkgs %}
+                            {% set truncate_title = truncate_title or 80 %}
+                            {% set title = package.title or package.name %}
+                            <li class="nav-item">{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='dataset' if h.ckan_version().split('.')[1] | int >= 9 else 'package', action='read', id=package.name)) }}</li>
+                        {% endfor %}
+                    </ul>
+                {% else %}
+                    <p class="module-content empty">{{_('There are no Datasets in this Showcase')}}</p>
+                {% endif %}
+        </section>
+    {% endif %}
 {% endblock %}
diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_item.html b/ckanext/showcase/templates/showcase/snippets/showcase_item.html
index 0250f9e1..10c4344b 100644
--- a/ckanext/showcase/templates/showcase/snippets/showcase_item.html
+++ b/ckanext/showcase/templates/showcase/snippets/showcase_item.html
@@ -21,7 +21,7 @@
       <img src="{{ package.image_display_url or h.url_for_static('/base/images/placeholder-group.png') }}" alt="{{ package.name }}" class="media-image img-responsive">
     {% endblock %}
     {% block title %}
-      <h3 class="media-heading">{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='read', id=package.name)) }}</h3>
+      <h3 class="media-heading">{{ h.link_to(h.truncate(title, truncate_title), h.url_for('showcase_read', id=package.name)) }}</h3>
     {% endblock %}
     {% block notes %}
       {% if notes %}
@@ -38,7 +38,7 @@ <h3 class="media-heading">{{ h.link_to(h.truncate(title, truncate_title), h.url_
       {% endif %}
     {% endblock %}
     {% block link %}
-      <a href="{{ h.url_for(controller='ckanext.showcase.controller:ShowcaseController', action='read', id=package.name) }}" title="{{ _('View {showcase_title}').format(showcase_title=package.title) }}" class="media-view">
+      <a href="{{ h.url_for('showcase_read', id=package.name) }}" title="{{ _('View {showcase_title}').format(showcase_title=package.title) }}" class="media-view">
         <span>{{ _('View {showcase_title}').format(showcase_title=package.title) }}</span>
       </a>
     {% endblock %}
diff --git a/ckanext/showcase/templates/showcase/snippets/tags.html b/ckanext/showcase/templates/showcase/snippets/tags.html
index 7ca4d57b..41500485 100644
--- a/ckanext/showcase/templates/showcase/snippets/tags.html
+++ b/ckanext/showcase/templates/showcase/snippets/tags.html
@@ -5,7 +5,7 @@
     <ul class="{{ _class }}">
       {% for tag in tags %}
         <li>
-          <a class="{% block tag_list_item_class %}tag{% endblock %}" href="{% url_for controller='ckanext.showcase.controller:ShowcaseController', action='search', tags=tag.name %}">{{ h.truncate(tag.display_name, 22) }}</a>
+            <a class="{% block tag_list_item_class %}tag{% endblock %}" href="{{ h.url_for('showcase_index', tags=tag.name) }}">{{ h.truncate(tag.display_name, 22) }}</a>
         </li>
       {% endfor %}
     </ul>
diff --git a/ckanext/showcase/utils.py b/ckanext/showcase/utils.py
new file mode 100644
index 00000000..e8e12c43
--- /dev/null
+++ b/ckanext/showcase/utils.py
@@ -0,0 +1,729 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+import json
+import logging
+
+from collections import OrderedDict
+import six
+from six.moves.urllib.parse import urlencode
+
+import ckan.plugins as p
+from ckan import model
+from ckan.lib.munge import munge_title_to_name, substitute_ascii_equivalents
+from ckan.logic import get_action
+import ckan.lib.helpers as h
+import ckantoolkit as tk
+from ckanext.showcase.model import ShowcasePackageAssociation
+
+_ = tk._
+abort = tk.abort
+c = tk.c
+
+log = logging.getLogger(__name__)
+DATASET_TYPE_NAME = 'showcase'
+
+
+def check_edit_view_auth(id):
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': c.user or c.author,
+        'auth_user_obj': c.userobj,
+        'save': 'save' in tk.request.params,
+        'moderated': tk.config.get('moderated'),
+        'pending': True
+    }
+
+    try:
+        tk.check_access('ckanext_showcase_update', context)
+    except tk.NotAuthorized:
+        return tk.abort(
+            401,
+            _('User not authorized to edit {showcase_id}').format(
+                showcase_id=id))
+
+
+def check_new_view_auth():
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': tk.c.user or tk.c.author,
+        'auth_user_obj': tk.c.userobj,
+        'save': 'save' in tk.request.params
+    }
+
+    # Check access here, then continue with PackageController.new()
+    # PackageController.new will also check access for package_create.
+    # This is okay for now, while only sysadmins can create Showcases, but
+    # may not work if we allow other users to create Showcases, who don't
+    # have access to create dataset package types. Same for edit below.
+    try:
+        tk.check_access('ckanext_showcase_create', context)
+    except tk.NotAuthorized:
+        return tk.abort(401, _('Unauthorized to create a package'))
+
+
+def read_view(id):
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': c.user or c.author,
+        'for_view': True,
+        'auth_user_obj': c.userobj
+    }
+    data_dict = {'id': id}
+
+    # check if showcase exists
+    try:
+        c.pkg_dict = tk.get_action('package_show')(context, data_dict)
+    except tk.ObjectNotFound:
+        return tk.abort(404, _('Showcase not found'))
+    except tk.NotAuthorized:
+        return tk.abort(401, _('Unauthorized to read showcase'))
+
+    # get showcase packages
+    c.showcase_pkgs = tk.get_action('ckanext_showcase_package_list')(
+        context, {
+            'showcase_id': c.pkg_dict['id']
+        })
+
+    package_type = DATASET_TYPE_NAME
+    return tk.render('showcase/read.html',
+                     extra_vars={'dataset_type': package_type})
+
+
+def manage_datasets_view(id):
+
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': tk.c.user or tk.c.author
+    }
+    data_dict = {'id': id}
+
+    try:
+        tk.check_access('ckanext_showcase_update', context)
+    except tk.NotAuthorized:
+        return tk.abort(
+            401,
+            _('User not authorized to edit {showcase_id}').format(
+                showcase_id=id))
+
+    # check if showcase exists
+    try:
+        tk.c.pkg_dict = tk.get_action('package_show')(context, data_dict)
+    except tk.ObjectNotFound:
+        return tk.abort(404, _('Showcase not found'))
+    except tk.NotAuthorized:
+        return tk.abort(401, _('Unauthorized to read showcase'))
+
+    # Are we removing a showcase/dataset association?
+    form_data = tk.request.form if tk.check_ckan_version(
+        '2.9') else tk.request.params
+
+    if (tk.request.method == 'POST'
+            and 'bulk_action.showcase_remove' in form_data):
+        # Find the datasets to perform the action on, they are prefixed by
+        # dataset_ in the form data
+        dataset_ids = []
+        for param in form_data:
+            if param.startswith('dataset_'):
+                dataset_ids.append(param[8:])
+        if dataset_ids:
+            for dataset_id in dataset_ids:
+                tk.get_action('ckanext_showcase_package_association_delete')(
+                    context, {
+                        'showcase_id': tk.c.pkg_dict['id'],
+                        'package_id': dataset_id
+                    })
+            h.flash_success(
+                tk.ungettext(
+                    "The dataset has been removed from the showcase.",
+                    "The datasets have been removed from the showcase.",
+                    len(dataset_ids)))
+            url = h.url_for('showcase_manage_datasets', id=id)
+            return h.redirect_to(url)
+
+    # Are we creating a showcase/dataset association?
+    elif (tk.request.method == 'POST'
+          and 'bulk_action.showcase_add' in form_data):
+        # Find the datasets to perform the action on, they are prefixed by
+        # dataset_ in the form data
+        dataset_ids = []
+        for param in form_data:
+            if param.startswith('dataset_'):
+                dataset_ids.append(param[8:])
+        if dataset_ids:
+            successful_adds = []
+            for dataset_id in dataset_ids:
+                try:
+                    tk.get_action(
+                        'ckanext_showcase_package_association_create')(
+                            context, {
+                                'showcase_id': tk.c.pkg_dict['id'],
+                                'package_id': dataset_id
+                            })
+                except tk.ValidationError as e:
+                    h.flash_notice(e.error_summary)
+                else:
+                    successful_adds.append(dataset_id)
+            if successful_adds:
+                h.flash_success(
+                    tk.ungettext(
+                        "The dataset has been added to the showcase.",
+                        "The datasets have been added to the showcase.",
+                        len(successful_adds)))
+            url = h.url_for('showcase_manage_datasets', id=id)
+            return h.redirect_to(url)
+
+    _add_dataset_search(tk.c.pkg_dict['id'], tk.c.pkg_dict['name'])
+
+    # get showcase packages
+    tk.c.showcase_pkgs = tk.get_action('ckanext_showcase_package_list')(
+        context, {
+            'showcase_id': tk.c.pkg_dict['id']
+        })
+
+    return tk.render('showcase/manage_datasets.html')
+
+
+def migrate(allow_duplicates):
+    related_items = get_action('related_list')(data_dict={})
+
+    # preflight:
+    # related items must have unique titles before migration
+    related_titles = [i['title'] for i in related_items]
+    # make a list of duplicate titles
+    duplicate_titles = _find_duplicates(related_titles)
+    if duplicate_titles and allow_duplicates == False:
+        print(
+            """All Related Items must have unique titles before migration. The following
+Related Item titles are used more than once and need to be corrected before
+migration can continue. Please correct and try again:""")
+        for i in duplicate_titles:
+            print(i)
+        return
+
+    for related in related_items:
+        existing_showcase = get_action('package_search')(data_dict={
+            'fq':
+            '+dataset_type:showcase original_related_item_id:{0}'.format(
+                related['id'])
+        })
+        normalized_title = substitute_ascii_equivalents(related['title'])
+        if existing_showcase['count'] > 0:
+            print('Showcase for Related Item "{0}" already exists.'.format(
+                normalized_title))
+        else:
+            showcase_title = _gen_new_title(related.get('title'),
+                                            related['id'])
+            data_dict = {
+                'original_related_item_id': related.get('id'),
+                'title': showcase_title,
+                'name': munge_title_to_name(showcase_title),
+                'notes': related.get('description'),
+                'image_url': related.get('image_url'),
+                'url': related.get('url'),
+                'tags': [{
+                    "name": related.get('type').lower()
+                }]
+            }
+            # make the showcase
+            try:
+                new_showcase = get_action('ckanext_showcase_create')(
+                    data_dict=data_dict)
+            except Exception as e:
+                print('There was a problem migrating "{0}": {1}'.format(
+                    normalized_title, e))
+            else:
+                print('Created Showcase from the Related Item "{0}"'.format(
+                    normalized_title))
+
+                # make the showcase_package_association, if needed
+                try:
+                    related_pkg_id = _get_related_dataset(related['id'])
+                    if related_pkg_id:
+                        get_action(
+                            'ckanext_showcase_package_association_create')(
+                                data_dict={
+                                    'showcase_id': new_showcase['id'],
+                                    'package_id': related_pkg_id
+                                })
+                except Exception as e:
+                    print(
+                        'There was a problem creating the showcase_package_association for "{0}": {1}'
+                        .format(normalized_title, e))
+
+
+def _get_related_dataset(related_id):
+    '''Get the id of a package from related_dataset, if one exists.'''
+    related_dataset = model.Session.query(
+        model.RelatedDataset).filter_by(related_id=related_id).first()
+    if related_dataset:
+        return related_dataset.dataset_id
+
+
+def _find_duplicates(lst):
+    '''From a list, return a set of duplicates.
+
+    >>> MigrationCommand('cmd')._find_duplicates([1, 2, 3, 4, 5])
+    []
+
+    >>> MigrationCommand('cmd')._find_duplicates([1, 2, 3, 4, 3, 1, 1])
+    [1, 3]
+
+    >>> MigrationCommand('cmd')._find_duplicates(['one', 'two', 'three', 'four', 'two', 'three'])
+    ['two', 'three']
+    '''
+    return list(set(x for x in lst if lst.count(x) >= 2))
+
+
+def _gen_new_title(title, related_id):
+    name = munge_title_to_name(title)
+    pkg_obj = model.Session.query(model.Package).filter_by(name=name).first()
+    if pkg_obj:
+        title.replace('duplicate_', '')
+        return 'duplicate_' + title + '_' + related_id
+    else:
+        return title
+
+
+def _add_dataset_search(showcase_id, showcase_name):
+    '''
+    Search logic for discovering datasets to add to a showcase.
+    '''
+
+    from ckan.lib.search import SearchError
+
+    package_type = 'dataset'
+
+    # unicode format (decoded from utf8)
+    q = c.q = tk.request.params.get('q', u'')
+    c.query_error = False
+    page = h.get_page_number(tk.request.params)
+
+    limit = int(tk.config.get('ckan.datasets_per_page', 20))
+
+    # most search operations should reset the page counter:
+    params_nopage = [(k, v) for k, v in tk.request.params.items()
+                     if k != 'page']
+
+    def drill_down_url(alternative_url=None, **by):
+        return h.add_url_param(alternative_url=alternative_url,
+                               controller='dataset'
+                               if tk.check_ckan_version('2.9') else 'package',
+                               action='search',
+                               new_params=by)
+
+    c.drill_down_url = drill_down_url
+
+    def remove_field(key, value=None, replace=None):
+        return h.remove_url_param(key,
+                                  value=value,
+                                  replace=replace,
+                                  controller='dataset' if
+                                  tk.check_ckan_version('2.9') else 'package',
+                                  action='search')
+
+    c.remove_field = remove_field
+
+    sort_by = tk.request.params.get('sort', None)
+    params_nosort = [(k, v) for k, v in params_nopage if k != 'sort']
+
+    def _sort_by(fields):
+        """
+        Sort by the given list of fields.
+
+        Each entry in the list is a 2-tuple: (fieldname, sort_order)
+
+        eg - [('metadata_modified', 'desc'), ('name', 'asc')]
+
+        If fields is empty, then the default ordering is used.
+        """
+        params = params_nosort[:]
+
+        if fields:
+            sort_string = ', '.join('%s %s' % f for f in fields)
+            params.append(('sort', sort_string))
+        return _search_url(params, showcase_name)
+
+    c.sort_by = _sort_by
+    if sort_by is None:
+        c.sort_by_fields = []
+    else:
+        c.sort_by_fields = [field.split()[0] for field in sort_by.split(',')]
+
+    def pager_url(q=None, page=None):
+        params = list(params_nopage)
+        params.append(('page', page))
+        return _search_url(params, showcase_name)
+
+    c.search_url_params = urlencode(_encode_params(params_nopage))
+
+    try:
+        c.fields = []
+        # c.fields_grouped will contain a dict of params containing
+        # a list of values eg {'tags':['tag1', 'tag2']}
+        c.fields_grouped = {}
+        search_extras = {}
+        fq = ''
+        for (param, value) in tk.request.params.items():
+            if param not in ['q', 'page', 'sort'] \
+                    and len(value) and not param.startswith('_'):
+                if not param.startswith('ext_'):
+                    c.fields.append((param, value))
+                    fq += ' %s:"%s"' % (param, value)
+                    if param not in c.fields_grouped:
+                        c.fields_grouped[param] = [value]
+                    else:
+                        c.fields_grouped[param].append(value)
+                else:
+                    search_extras[param] = value
+
+        context = {
+            'model': model,
+            'session': model.Session,
+            'user': c.user or c.author,
+            'for_view': True,
+            'auth_user_obj': c.userobj
+        }
+
+        if package_type and package_type != 'dataset':
+            # Only show datasets of this particular type
+            fq += ' +dataset_type:{type}'.format(type=package_type)
+        else:
+            # Unless changed via config options, don't show non standard
+            # dataset types on the default search page
+            if not tk.asbool(
+                    tk.config.get('ckan.search.show_all_types', 'False')):
+                fq += ' +dataset_type:dataset'
+
+        # Only search for packages that aren't already associated with the
+        # Showcase
+        associated_package_ids = ShowcasePackageAssociation.get_package_ids_for_showcase(
+            showcase_id)
+        # flatten resulting list to space separated string
+        if associated_package_ids:
+            associated_package_ids_str = \
+                ' OR '.join([id[0] for id in associated_package_ids])
+            fq += ' !id:({0})'.format(associated_package_ids_str)
+
+        facets = OrderedDict()
+
+        default_facet_titles = {
+            'organization': _('Organizations'),
+            'groups': _('Groups'),
+            'tags': _('Tags'),
+            'res_format': _('Formats'),
+            'license_id': _('Licenses'),
+        }
+
+        # for CKAN-Versions that do not provide the facets-method from
+        # helper-context, import facets from ckan.common
+        if hasattr(h, 'facets'):
+            current_facets = h.facets()
+        else:
+            from ckan.common import g
+            current_facets = g.facets
+
+        for facet in current_facets:
+            if facet in default_facet_titles:
+                facets[facet] = default_facet_titles[facet]
+            else:
+                facets[facet] = facet
+
+        # Facet titles
+        for plugin in p.PluginImplementations(p.IFacets):
+            facets = plugin.dataset_facets(facets, package_type)
+
+        c.facet_titles = facets
+
+        data_dict = {
+            'q': q,
+            'fq': fq.strip(),
+            'facet.field': list(facets.keys()),
+            'rows': limit,
+            'start': (page - 1) * limit,
+            'sort': sort_by,
+            'extras': search_extras
+        }
+
+        query = tk.get_action('package_search')(context, data_dict)
+        c.sort_by_selected = query['sort']
+
+        c.page = h.Page(collection=query['results'],
+                        page=page,
+                        url=pager_url,
+                        item_count=query['count'],
+                        items_per_page=limit)
+        c.facets = query['facets']
+        c.search_facets = query['search_facets']
+        c.page.items = query['results']
+    except SearchError as se:
+        log.error('Dataset search error: %r', se.args)
+        c.query_error = True
+        c.facets = {}
+        c.search_facets = {}
+        c.page = h.Page(collection=[])
+    c.search_facets_limits = {}
+    for facet in c.search_facets.keys():
+        try:
+            limit = int(
+                tk.request.params.get(
+                    '_%s_limit' % facet,
+                    int(tk.config.get('search.facets.default', 10))))
+        except tk.ValueError:
+            abort(
+                400,
+                _("Parameter '{parameter_name}' is not an integer").format(
+                    parameter_name='_%s_limit' % facet))
+        c.search_facets_limits[facet] = limit
+
+
+def _search_url(params, name):
+    url = h.url_for('showcase_manage_datasets', id=name)
+    return url_with_params(url, params)
+
+
+def _encode_params(params):
+    return [(k, six.ensure_str(six.text_type(v))) for k, v in params]
+
+
+def url_with_params(url, params):
+    params = _encode_params(params)
+    return url + u'?' + urlencode(params)
+
+
+def delete_view(id):
+    if 'cancel' in tk.request.params:
+        tk.redirect_to('showcase_edit', id=id)
+
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': c.user or c.author,
+        'auth_user_obj': c.userobj
+    }
+
+    try:
+        tk.check_access('ckanext_showcase_delete', context, {'id': id})
+    except tk.NotAuthorized:
+        return tk.abort(401, _('Unauthorized to delete showcase'))
+
+    try:
+        if tk.request.method == 'POST':
+            tk.get_action('ckanext_showcase_delete')(context, {'id': id})
+            h.flash_notice(_('Showcase has been deleted.'))
+            return tk.redirect_to('showcase_index')
+        c.pkg_dict = tk.get_action('package_show')(context, {'id': id})
+    except tk.NotAuthorized:
+        tk.abort(401, _('Unauthorized to delete showcase'))
+    except tk.ObjectNotFound:
+        tk.abort(404, _('Showcase not found'))
+    return tk.render('showcase/confirm_delete.html',
+                     extra_vars={'dataset_type': DATASET_TYPE_NAME})
+
+
+def dataset_showcase_list(id):
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': c.user or c.author,
+        'for_view': True,
+        'auth_user_obj': c.userobj
+    }
+    data_dict = {'id': id}
+
+    try:
+        tk.check_access('package_show', context, data_dict)
+    except tk.ObjectNotFound:
+        return tk.abort(404, _('Dataset not found'))
+    except tk.NotAuthorized:
+        return tk.abort(401, _('Not authorized to see this page'))
+
+    try:
+        c.pkg_dict = tk.get_action('package_show')(context, data_dict)
+        c.showcase_list = tk.get_action('ckanext_package_showcase_list')(
+            context, {
+                'package_id': c.pkg_dict['id']
+            })
+    except tk.ObjectNotFound:
+        return tk.abort(404, _('Dataset not found'))
+    except tk.NotAuthorized:
+        return tk.abort(401, _('Unauthorized to read package'))
+
+    if tk.request.method == 'POST':
+        # Are we adding the dataset to a showcase?
+        form_data = tk.request.form if tk.check_ckan_version(
+            '2.9') else tk.request.params
+
+        new_showcase = form_data.get('showcase_added')
+        if new_showcase:
+            data_dict = {
+                "showcase_id": new_showcase,
+                "package_id": c.pkg_dict['id']
+            }
+            try:
+                tk.get_action('ckanext_showcase_package_association_create')(
+                    context, data_dict)
+            except tk.ObjectNotFound:
+                return tk.abort(404, _('Showcase not found'))
+            else:
+                h.flash_success(
+                    _("The dataset has been added to the showcase."))
+
+        # Are we removing a dataset from a showcase?
+        showcase_to_remove = form_data.get('remove_showcase_id')
+        if showcase_to_remove:
+            data_dict = {
+                "showcase_id": showcase_to_remove,
+                "package_id": c.pkg_dict['id']
+            }
+            try:
+                tk.get_action('ckanext_showcase_package_association_delete')(
+                    context, data_dict)
+            except tk.ObjectNotFound:
+                return tk.abort(404, _('Showcase not found'))
+            else:
+                h.flash_success(
+                    _("The dataset has been removed from the showcase."))
+        return h.redirect_to(
+            h.url_for('showcase_dataset_showcase_list', id=c.pkg_dict['name']))
+
+    pkg_showcase_ids = [showcase['id'] for showcase in c.showcase_list]
+    site_showcases = tk.get_action('ckanext_showcase_list')(context, {})
+
+    c.showcase_dropdown = [[showcase['id'], showcase['title']]
+                           for showcase in site_showcases
+                           if showcase['id'] not in pkg_showcase_ids]
+
+    return tk.render("package/dataset_showcase_list.html",
+                     extra_vars={'pkg_dict': c.pkg_dict})
+
+
+def manage_showcase_admins():
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': c.user or c.author
+    }
+
+    try:
+        tk.check_access('sysadmin', context, {})
+    except tk.NotAuthorized:
+        return tk.abort(401, _('User not authorized to view page'))
+
+    form_data = tk.request.form if tk.check_ckan_version(
+        '2.9') else tk.request.params
+
+    # We're trying to add a user to the showcase admins list.
+    if tk.request.method == 'POST' and form_data['username']:
+        username = form_data['username']
+        try:
+            tk.get_action('ckanext_showcase_admin_add')(data_dict={
+                'username': username
+            })
+        except tk.NotAuthorized:
+            abort(401, _('Unauthorized to perform that action'))
+        except tk.ObjectNotFound:
+            h.flash_error(
+                _("User '{user_name}' not found.").format(user_name=username))
+        except tk.ValidationError as e:
+            h.flash_notice(e.error_summary)
+        else:
+            h.flash_success(_("The user is now a Showcase Admin"))
+
+        return tk.redirect_to(h.url_for('showcase_admins'))
+
+    c.showcase_admins = tk.get_action('ckanext_showcase_admin_list')()
+
+    return tk.render('admin/manage_showcase_admins.html')
+
+
+def remove_showcase_admin():
+    '''
+    Remove a user from the Showcase Admin list.
+    '''
+    context = {
+        'model': model,
+        'session': model.Session,
+        'user': c.user or c.author
+    }
+
+    try:
+        tk.check_access('sysadmin', context, {})
+    except tk.NotAuthorized:
+        return tk.abort(401, _('User not authorized to view page'))
+
+    form_data = tk.request.form if tk.check_ckan_version(
+        '2.9') else tk.request.params
+
+    if 'cancel' in form_data:
+        return tk.redirect_to('showcase_admins')
+
+    user_id = tk.request.params['user']
+    if tk.request.method == 'POST' and user_id:
+        user_id = tk.request.params['user']
+        try:
+            tk.get_action('ckanext_showcase_admin_remove')(data_dict={
+                'username': user_id
+            })
+        except tk.NotAuthorized:
+            return tk.abort(401, _('Unauthorized to perform that action'))
+        except tk.ObjectNotFound:
+            h.flash_error(_('The user is not a Showcase Admin'))
+        else:
+            h.flash_success(_('The user is no longer a Showcase Admin'))
+
+        return tk.redirect_to(h.url_for('showcase_admins'))
+
+    c.user_dict = tk.get_action('user_show')(data_dict={'id': user_id})
+    c.user_id = user_id
+    return tk.render('admin/confirm_remove_showcase_admin.html')
+
+def markdown_to_html():
+    ''' Migrates the notes of all showcases from markdown to html.
+
+    When using CKEditor, notes on showcases are stored in html instead of
+    markdown, this command will migrate all nothes using CKAN's
+    render_markdown core helper.
+    '''
+    showcases = tk.get_action('ckanext_showcase_list')(data_dict={})
+
+    site_user = tk.get_action('get_site_user')({
+        'model': model,
+        'ignore_auth': True},
+        {}
+    )
+    context = {
+        'model': model,
+        'session': model.Session,
+        'ignore_auth': True,
+        'user': site_user['name'],
+    }
+
+    for showcase in showcases:
+        tk.get_action('package_patch')(
+            context,
+            {
+                'id': showcase['id'],
+                'notes': h.render_markdown(showcase['notes'])
+            }
+        )
+    log.info('All notes were migrated successfully.')
+
+
+def upload():
+    if not tk.request.method == 'POST':
+        tk.abort(409, _('Only Posting is availiable'))
+
+    try:
+        url = tk.get_action('ckanext_showcase_upload')(
+            None,
+            dict(tk.request.POST)
+            )
+    except tk.NotAuthorized:
+        tk.abort(401, _('Unauthorized to upload file %s') % id)
+
+    return json.dumps(url)
\ No newline at end of file
diff --git a/ckanext/showcase/views.py b/ckanext/showcase/views.py
new file mode 100644
index 00000000..e613a9cd
--- /dev/null
+++ b/ckanext/showcase/views.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+
+from flask import Blueprint
+
+import ckantoolkit as tk
+
+import ckan.lib.helpers as h
+import ckan.views.dataset as dataset
+
+import ckanext.showcase.utils as utils
+
+showcase = Blueprint(u'showcase_blueprint', __name__)
+
+
+def index():
+    return dataset.search(utils.DATASET_TYPE_NAME)
+
+
+class CreateView(dataset.CreateView):
+    def get(self, data=None, errors=None, error_summary=None):
+        utils.check_new_view_auth()
+        return super(CreateView, self).get(utils.DATASET_TYPE_NAME, data,
+                                           errors, error_summary)
+
+    def post(self):
+
+        data_dict = dataset.clean_dict(
+            dataset.dict_fns.unflatten(
+                dataset.tuplize_dict(dataset.parse_params(tk.request.form))))
+        data_dict.update(
+            dataset.clean_dict(
+                dataset.dict_fns.unflatten(
+                    dataset.tuplize_dict(dataset.parse_params(
+                        tk.request.files)))))
+        context = self._prepare()
+        data_dict['type'] = utils.DATASET_TYPE_NAME
+        context['message'] = data_dict.get('log_message', '')
+
+        try:
+            pkg_dict = tk.get_action('ckanext_showcase_create')(context,
+                                                                data_dict)
+
+        except tk.ValidationError as e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            data_dict['state'] = 'none'
+            return self.get(data_dict, errors, error_summary)
+
+        # redirect to manage datasets
+        url = h.url_for('showcase_blueprint.manage_datasets',
+                        id=pkg_dict['name'])
+        return h.redirect_to(url)
+
+
+def manage_datasets(id):
+    return utils.manage_datasets_view(id)
+
+
+def delete(id):
+    return utils.delete_view(id)
+
+
+def read(id):
+    return utils.read_view(id)
+
+
+class EditView(dataset.EditView):
+    def get(self, id, data=None, errors=None, error_summary=None):
+        utils.check_new_view_auth()
+        return super(EditView, self).get(utils.DATASET_TYPE_NAME, id, data,
+                                         errors, error_summary)
+
+    def post(self, id):
+        context = self._prepare(id)
+        utils.check_edit_view_auth(id)
+
+        data_dict = dataset.clean_dict(
+            dataset.dict_fns.unflatten(
+                dataset.tuplize_dict(dataset.parse_params(tk.request.form))))
+        data_dict.update(
+            dataset.clean_dict(
+                dataset.dict_fns.unflatten(
+                    dataset.tuplize_dict(dataset.parse_params(
+                        tk.request.files)))))
+
+        data_dict['id'] = id
+        try:
+            pkg = tk.get_action('ckanext_showcase_update')(context, data_dict)
+        except tk.ValidationError as e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.get(id, data_dict, errors, error_summary)
+
+        tk.c.pkg_dict = pkg
+
+        # redirect to showcase details page
+        url = h.url_for('showcase_read', id=pkg['name'])
+        return h.redirect_to(url)
+
+
+def dataset_showcase_list(id):
+    return utils.dataset_showcase_list(id)
+
+
+def admins():
+    return utils.manage_showcase_admins()
+
+
+def admin_remove():
+    return utils.remove_showcase_admin()
+
+def upload():
+    return utils.upload()
+
+
+showcase.add_url_rule('/showcase', view_func=index)
+showcase.add_url_rule('/showcase/new', view_func=CreateView.as_view('new'))
+showcase.add_url_rule('/showcase/delete/<id>',
+                      view_func=delete,
+                      methods=[u'GET', u'POST'])
+showcase.add_url_rule('/showcase/<id>', view_func=read)
+showcase.add_url_rule('/showcase/edit/<id>',
+                      view_func=EditView.as_view('edit'),
+                      methods=[u'GET', u'POST'])
+showcase.add_url_rule('/showcase/manage_datasets/<id>',
+                      view_func=manage_datasets,
+                      methods=[u'GET', u'POST'])
+showcase.add_url_rule('/dataset/showcases/<id>',
+                      view_func=dataset_showcase_list,
+                      methods=[u'GET', u'POST'])
+showcase.add_url_rule('/ckan-admin/showcase_admins',
+                      view_func=admins,
+                      methods=[u'GET', u'POST'])
+showcase.add_url_rule('/ckan-admin/showcase_admin_remove',
+                      view_func=admin_remove,
+                      methods=[u'GET', u'POST'])
+showcase.add_url_rule('/showcase/upload',
+                      view_func=upload,
+                      methods=[u'POST'])
+
+
+def get_blueprints():
+    return [showcase]
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 8f63371c..3eef46d7 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,1 +1,3 @@
-beautifulsoup4==4.5.1
+beautifulsoup4==4.8.2
+pytest==4.6.5
+pytest-cov==2.7.1
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..26a6edf1
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,1 @@
+ckantoolkit>=0.0.3
diff --git a/setup.cfg b/setup.cfg
index fa53fa01..5f5bdefc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,3 +19,9 @@ previous = true
 domain = ckanext-showcase
 directory = ckanext/showcase/i18n
 statistics = true
+
+[tool:pytest]
+filterwarnings =
+        ignore::sqlalchemy.exc.SADeprecationWarning
+        ignore::sqlalchemy.exc.SAWarning
+        ignore::DeprecationWarning
diff --git a/setup.py b/setup.py
index 15efcda6..390fa78b 100644
--- a/setup.py
+++ b/setup.py
@@ -49,6 +49,7 @@
     # You can just specify the packages manually here if your project is
     # simple. Or you can use find_packages().
     packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
+    namespace_packages=['ckanext'],
 
     # List run-time dependencies here.  These will be installed by pip when your
     # project is installed. For an analysis of "install_requires" vs pip's
@@ -80,7 +81,7 @@
         ckan = ckan.lib.extract:extract_ckan
 
         [paste.paster_command]
-        showcase=ckanext.showcase.commands.migrate:MigrationCommand
+        showcase=ckanext.showcase.commands.paster:MigrationCommand
     ''',
 
     message_extractors={


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..7e93a3b8
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,64 @@
+name: Tests
+on: [push, pull_request]
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: '3.6'
+      - name: Install requirements
+        run: pip install flake8 pycodestyle
+      - name: Check syntax
+        run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics --exclude ckan
+
+  test:
+    needs: lint
+    strategy:
+      matrix:
+        ckan-version: [2.9, 2.9-py2, 2.8, 2.7]
+      fail-fast: false
+
+    name: CKAN ${{ matrix.ckan-version }}
+    runs-on: ubuntu-latest
+    container:
+      image: openknowledge/ckan-dev:${{ matrix.ckan-version }}
+    services:
+      solr:
+        image: ckan/ckan-solr-dev:${{ matrix.ckan-version }}
+      postgres:
+        image: ckan/ckan-postgres-dev:${{ matrix.ckan-version }}
+        env:
+          POSTGRES_USER: postgres
+          POSTGRES_PASSWORD: postgres
+          POSTGRES_DB: postgres
+        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
+      redis:
+          image: redis:3
+    env:
+      CKAN_SQLALCHEMY_URL: postgresql://ckan_default:pass@postgres/ckan_test
+      CKAN_DATASTORE_WRITE_URL: postgresql://datastore_write:pass@postgres/datastore_test
+      CKAN_DATASTORE_READ_URL: postgresql://datastore_read:pass@postgres/datastore_test
+      CKAN_SOLR_URL: http://solr:8983/solr/ckan
+      CKAN_REDIS_URL: redis://redis:6379/1
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install requirements
+      run: |
+        pip install -r requirements.txt
+        pip install -r dev-requirements.txt
+        pip install -e .
+        # Replace default path to CKAN core config file with the one on the container
+        sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini
+    - name: Setup extension (CKAN >= 2.9)
+      if: ${{ matrix.ckan-version != '2.7' && matrix.ckan-version != '2.8' }}
+      run: |
+        ckan -c test.ini db init
+    - name: Setup extension (CKAN < 2.9)
+      if: ${{ matrix.ckan-version == '2.7' || matrix.ckan-version == '2.8' }}
+      run: |
+        paster --plugin=ckan db init -c test.ini
+    - name: Run tests
+      run: pytest --ckan-ini=test.ini --cov=ckanext.showcase --disable-warnings ckanext/showcase/tests
diff --git a/ckanext/showcase/tests/__init__.py b/ckanext/showcase/tests/__init__.py
index 8373a8a1..e69de29b 100644
--- a/ckanext/showcase/tests/__init__.py
+++ b/ckanext/showcase/tests/__init__.py
@@ -1,16 +0,0 @@
-from ckanext.showcase.model import setup as showcase_setup
-
-
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
-
-
-class ShowcaseFunctionalTestBase(helpers.FunctionalTestBase):
-
-    def setup(self):
-        '''Reset the database and clear the search indexes.'''
-        super(ShowcaseFunctionalTestBase, self).setup()
-        # set up showcase tables
-        showcase_setup()
diff --git a/ckanext/showcase/tests/action/test_create.py b/ckanext/showcase/tests/action/test_create.py
index 921aaf96..20d8c7aa 100644
--- a/ckanext/showcase/tests/action/test_create.py
+++ b/ckanext/showcase/tests/action/test_create.py
@@ -1,251 +1,305 @@
-from nose import tools as nosetools
+import pytest
 
 from ckan.model.package import Package
 import ckan.model as model
 import ckan.plugins.toolkit as toolkit
 
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
-
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
+from ckan.tests import factories, helpers
 
 
 from ckanext.showcase.model import ShowcasePackageAssociation, ShowcaseAdmin
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
-
 
-class TestCreateShowcase(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db")
+class TestCreateShowcase(object):
     def test_showcase_create_no_args(self):
-        '''
+        """
         Calling showcase create without args raises ValidationError.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
+        context = {"user": sysadmin["name"]}
 
         # no showcases exist.
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 0)
-
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_create',
-                                context=context)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 0
+        )
+
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_create", context=context,
+            )
 
         # no showcases (dataset of type 'showcase') created.
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 0)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 0
+        )
 
     def test_showcase_create_with_name_arg(self):
-        '''
+        """
         Calling showcase create with a name arg creates a showcase package.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
+        context = {"user": sysadmin["name"]}
 
         # no showcases exist.
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 0)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 0
+        )
 
-        helpers.call_action('ckanext_showcase_create',
-                            context=context, name='my-showcase')
+        helpers.call_action(
+            "ckanext_showcase_create", context=context, name="my-showcase"
+        )
 
         # a showcases (dataset of type 'showcase') created.
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 1)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 1
+        )
 
     def test_showcase_create_with_existing_name(self):
-        '''
+        """
         Calling showcase create with an existing name raises ValidationError.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        factories.Dataset(type='showcase', name='my-showcase')
+        context = {"user": sysadmin["name"]}
+        factories.Dataset(type="showcase", name="my-showcase")
 
         # a single showcases exist.
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 1)
-
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_create',
-                                context=context, name='my-showcase')
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 1
+        )
+
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_create", context=context, name="my-showcase",
+            )
 
         # still only one showcase exists.
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 1)
-
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 1
+        )
 
-class TestCreateShowcasePackageAssociation(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db")
+class TestCreateShowcasePackageAssociation(object):
     def test_association_create_no_args(self):
-        '''
+        """
         Calling sc/pkg association create with no args raises
         ValidationError.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_association_create',
-                                context=context)
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_association_create", context=context,
+            )
 
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
 
     def test_association_create_missing_arg(self):
-        '''
+        """
         Calling sc/pkg association create with a missing arg raises
         ValidationError.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
+        package_id = factories.Dataset()["id"]
 
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_association_create',
-                                context=context, package_id=package_id)
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_association_create",
+                context=context,
+                package_id=package_id,
+            )
 
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
 
     def test_association_create_by_id(self):
-        '''
+        """
         Calling sc/pkg association create with correct args (package ids)
         creates an association.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
-        showcase_id = factories.Dataset(type='showcase')['id']
+        package_id = factories.Dataset()["id"]
+        showcase_id = factories.Dataset(type="showcase")["id"]
 
-        context = {'user': sysadmin['name']}
-        association_dict = helpers.call_action('ckanext_showcase_package_association_create',
-                                               context=context, package_id=package_id,
-                                               showcase_id=showcase_id)
+        context = {"user": sysadmin["name"]}
+        association_dict = helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_id,
+            showcase_id=showcase_id,
+        )
 
         # One association object created
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 1)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 1
         # Association properties are correct
-        nosetools.assert_equal(association_dict.get('showcase_id'), showcase_id)
-        nosetools.assert_equal(association_dict.get('package_id'), package_id)
+        assert association_dict.get("showcase_id") == showcase_id
+        assert association_dict.get("package_id") == package_id
 
     def test_association_create_by_name(self):
-        '''
+        """
         Calling sc/pkg association create with correct args (package names)
         creates an association.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
         package = factories.Dataset()
-        package_name = package['name']
-        showcase = factories.Dataset(type='showcase')
-        showcase_name = showcase['name']
-
-        context = {'user': sysadmin['name']}
-        association_dict = helpers.call_action('ckanext_showcase_package_association_create',
-                                               context=context, package_id=package_name,
-                                               showcase_id=showcase_name)
-
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 1)
-        nosetools.assert_equal(association_dict.get('showcase_id'), showcase['id'])
-        nosetools.assert_equal(association_dict.get('package_id'), package['id'])
+        package_name = package["name"]
+        showcase = factories.Dataset(type="showcase")
+        showcase_name = showcase["name"]
+
+        context = {"user": sysadmin["name"]}
+        association_dict = helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_name,
+            showcase_id=showcase_name,
+        )
+
+        assert model.Session.query(ShowcasePackageAssociation).count() == 1
+        assert association_dict.get("showcase_id") == showcase["id"]
+        assert association_dict.get("package_id") == package["id"]
 
     def test_association_create_existing(self):
-        '''
+        """
         Attempt to create association with existing details returns Validation
         Error.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
-        showcase_id = factories.Dataset(type='showcase')['id']
+        package_id = factories.Dataset()["id"]
+        showcase_id = factories.Dataset(type="showcase")["id"]
 
-        context = {'user': sysadmin['name']}
+        context = {"user": sysadmin["name"]}
         # Create association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_id,
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_id,
+            showcase_id=showcase_id,
+        )
         # Attempted duplicate creation results in ValidationError
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_association_create',
-                                context=context, package_id=package_id,
-                                showcase_id=showcase_id)
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_association_create",
+                context=context,
+                package_id=package_id,
+                showcase_id=showcase_id,
+            )
 
 
-class TestCreateShowcaseAdmin(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db")
+class TestCreateShowcaseAdmin(object):
     def test_showcase_admin_add_creates_showcase_admin_user(self):
-        '''
+        """
         Calling ckanext_showcase_admin_add adds user to showcase admin list.
-        '''
+        """
         user_to_add = factories.User()
 
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 0)
+        assert model.Session.query(ShowcaseAdmin).count() == 0
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_to_add['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=user_to_add["name"],
+        )
 
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 1)
-        nosetools.assert_true(user_to_add['id'] in ShowcaseAdmin.get_showcase_admin_ids())
+        assert model.Session.query(ShowcaseAdmin).count() == 1
+        assert user_to_add["id"] in ShowcaseAdmin.get_showcase_admin_ids()
 
     def test_showcase_admin_add_multiple_users(self):
-        '''
+        """
         Calling ckanext_showcase_admin_add for multiple users correctly adds
         them to showcase admin list.
-        '''
+        """
         user_to_add = factories.User()
         second_user_to_add = factories.User()
 
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 0)
+        assert model.Session.query(ShowcaseAdmin).count() == 0
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_to_add['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=user_to_add["name"],
+        )
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=second_user_to_add['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=second_user_to_add["name"],
+        )
 
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 2)
-        nosetools.assert_true(user_to_add['id'] in ShowcaseAdmin.get_showcase_admin_ids())
-        nosetools.assert_true(second_user_to_add['id'] in ShowcaseAdmin.get_showcase_admin_ids())
+        assert model.Session.query(ShowcaseAdmin).count() == 2
+        assert user_to_add["id"] in ShowcaseAdmin.get_showcase_admin_ids()
+        assert (
+            second_user_to_add["id"] in ShowcaseAdmin.get_showcase_admin_ids()
+        )
 
     def test_showcase_admin_add_existing_user(self):
-        '''
+        """
         Calling ckanext_showcase_admin_add twice for same user raises a
         ValidationError.
-        '''
+        """
         user_to_add = factories.User()
 
         # Add once
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_to_add['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=user_to_add["name"],
+        )
 
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 1)
+        assert model.Session.query(ShowcaseAdmin).count() == 1
 
         # Attempt second add
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_admin_add', context={},
-                                username=user_to_add['name'])
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_admin_add",
+                context={},
+                username=user_to_add["name"],
+            )
 
         # Still only one ShowcaseAdmin object.
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 1)
+        assert model.Session.query(ShowcaseAdmin).count() == 1
 
     def test_showcase_admin_add_username_doesnot_exist(self):
-        '''
+        """
         Calling ckanext_showcase_admin_add with non-existent username raises
         ValidationError and no ShowcaseAdmin object is created.
-        '''
-        nosetools.assert_raises(toolkit.ObjectNotFound, helpers.call_action,
-                                'ckanext_showcase_admin_add', context={},
-                                username='missing')
+        """
+        with pytest.raises(toolkit.ObjectNotFound):
+            helpers.call_action(
+                "ckanext_showcase_admin_add", context={}, username="missing",
+            )
 
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 0)
-        nosetools.assert_equal(ShowcaseAdmin.get_showcase_admin_ids(), [])
+        assert model.Session.query(ShowcaseAdmin).count() == 0
+        assert ShowcaseAdmin.get_showcase_admin_ids() == []
 
     def test_showcase_admin_add_no_args(self):
-        '''
+        """
         Calling ckanext_showcase_admin_add with no args raises ValidationError
         and no ShowcaseAdmin object is created.
-        '''
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_admin_add', context={})
-
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 0)
-        nosetools.assert_equal(ShowcaseAdmin.get_showcase_admin_ids(), [])
+        """
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_admin_add", context={},
+            )
+
+        assert model.Session.query(ShowcaseAdmin).count() == 0
+        assert ShowcaseAdmin.get_showcase_admin_ids() == []
diff --git a/ckanext/showcase/tests/action/test_delete.py b/ckanext/showcase/tests/action/test_delete.py
index 6c19b4aa..cc2ec8b2 100644
--- a/ckanext/showcase/tests/action/test_delete.py
+++ b/ckanext/showcase/tests/action/test_delete.py
@@ -1,349 +1,422 @@
-from nose import tools as nosetools
+import pytest
 
 import ckan.model as model
 import ckan.plugins.toolkit as toolkit
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
 
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
+from ckan.tests import factories, helpers
 
 from ckanext.showcase.model import ShowcasePackageAssociation, ShowcaseAdmin
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
 from ckan.model.package import Package
 
 
-class TestDeleteShowcase(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db")
+class TestDeleteShowcase(object):
     def test_showcase_delete_no_args(self):
-        '''
+        """
         Calling showcase delete with no args raises a ValidationError.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_delete', context=context)
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_delete", context=context,
+            )
 
     def test_showcase_delete_incorrect_args(self):
-        '''
+        """
         Calling showcase delete with incorrect args raises ObjectNotFound.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        factories.Dataset(type='showcase')
-        nosetools.assert_raises(toolkit.ObjectNotFound, helpers.call_action,
-                                'ckanext_showcase_delete', context=context,
-                                id='blah-blah')
+        context = {"user": sysadmin["name"]}
+        factories.Dataset(type="showcase")
+        with pytest.raises(toolkit.ObjectNotFound):
+            helpers.call_action(
+                "ckanext_showcase_delete", context=context, id="blah-blah",
+            )
 
     def test_showcase_delete_by_id(self):
-        '''
+        """
         Calling showcase delete with showcase id.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        showcase = factories.Dataset(type='showcase')
+        context = {"user": sysadmin["name"]}
+        showcase = factories.Dataset(type="showcase")
 
         # One showcase object created
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 1)
-
-        helpers.call_action('ckanext_showcase_delete',
-                            context=context, id=showcase['id'])
-
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 0)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 1
+        )
+
+        helpers.call_action(
+            "ckanext_showcase_delete", context=context, id=showcase["id"]
+        )
+
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 0
+        )
 
     def test_showcase_delete_by_name(self):
-        '''
+        """
         Calling showcase delete with showcase name.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        showcase = factories.Dataset(type='showcase')
+        context = {"user": sysadmin["name"]}
+        showcase = factories.Dataset(type="showcase")
 
         # One showcase object created
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 1)
-
-        helpers.call_action('ckanext_showcase_delete',
-                            context=context, id=showcase['name'])
-
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 0)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 1
+        )
+
+        helpers.call_action(
+            "ckanext_showcase_delete", context=context, id=showcase["name"]
+        )
+
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 0
+        )
 
     def test_showcase_delete_removes_associations(self):
-        '''
+        """
         Deleting a showcase also deletes associated ShowcasePackageAssociation
         objects.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        showcase = factories.Dataset(type='showcase', name='my-showcase')
-        dataset_one = factories.Dataset(name='dataset-one')
-        dataset_two = factories.Dataset(name='dataset-two')
-
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset_one['id'],
-                            showcase_id=showcase['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset_two['id'],
-                            showcase_id=showcase['id'])
-
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 2)
-
-        helpers.call_action('ckanext_showcase_delete',
-                            context=context, id=showcase['id'])
-
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
-
-
-class TestDeletePackage(ShowcaseFunctionalTestBase):
-
+        context = {"user": sysadmin["name"]}
+        showcase = factories.Dataset(type="showcase", name="my-showcase")
+        dataset_one = factories.Dataset(name="dataset-one")
+        dataset_two = factories.Dataset(name="dataset-two")
+
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset_one["id"],
+            showcase_id=showcase["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset_two["id"],
+            showcase_id=showcase["id"],
+        )
+
+        assert model.Session.query(ShowcasePackageAssociation).count() == 2
+
+        helpers.call_action(
+            "ckanext_showcase_delete", context=context, id=showcase["id"]
+        )
+
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
+
+
+@pytest.mark.usefixtures("clean_db")
+class TestDeletePackage(object):
     def test_package_delete_retains_associations(self):
-        '''
+        """
         Deleting a package (setting its status to 'delete') retains associated
         ShowcasePackageAssociation objects.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        showcase = factories.Dataset(type='showcase', name='my-showcase')
-        dataset_one = factories.Dataset(name='dataset-one')
-        dataset_two = factories.Dataset(name='dataset-two')
-
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset_one['id'],
-                            showcase_id=showcase['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset_two['id'],
-                            showcase_id=showcase['id'])
-
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 2)
+        context = {"user": sysadmin["name"]}
+        showcase = factories.Dataset(type="showcase", name="my-showcase")
+        dataset_one = factories.Dataset(name="dataset-one")
+        dataset_two = factories.Dataset(name="dataset-two")
+
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset_one["id"],
+            showcase_id=showcase["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset_two["id"],
+            showcase_id=showcase["id"],
+        )
+
+        assert model.Session.query(ShowcasePackageAssociation).count() == 2
 
         # delete the first package, should also delete the
         # ShowcasePackageAssociation associated with it.
-        helpers.call_action('package_delete',
-                            context=context, id=dataset_one['id'])
+        helpers.call_action(
+            "package_delete", context=context, id=dataset_one["id"]
+        )
 
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 2)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 2
 
     def test_package_purge_deletes_associations(self):
-        '''
+        """
         Purging a package (actually deleting it from the database) deletes
         associated ShowcasePackageAssociation objects.
-        '''
+        """
         sysadmin = factories.Sysadmin()
-        context = {'user': sysadmin['name']}
-        showcase = factories.Dataset(type='showcase', name='my-showcase')
-        dataset_one = factories.Dataset(name='dataset-one')
-        dataset_two = factories.Dataset(name='dataset-two')
-
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset_one['id'],
-                            showcase_id=showcase['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset_two['id'],
-                            showcase_id=showcase['id'])
-
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 2)
+        context = {"user": sysadmin["name"]}
+        showcase = factories.Dataset(type="showcase", name="my-showcase")
+        dataset_one = factories.Dataset(name="dataset-one")
+        dataset_two = factories.Dataset(name="dataset-two")
+
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset_one["id"],
+            showcase_id=showcase["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset_two["id"],
+            showcase_id=showcase["id"],
+        )
+
+        assert model.Session.query(ShowcasePackageAssociation).count() == 2
 
         # purge the first package, should also delete the
         # ShowcasePackageAssociation associated with it.
-        pkg = model.Session.query(model.Package).get(dataset_one['id'])
+        pkg = model.Session.query(model.Package).get(dataset_one["id"])
         pkg.purge()
         model.repo.commit_and_remove()
 
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 1)
-
+        assert model.Session.query(ShowcasePackageAssociation).count() == 1
 
-class TestDeleteShowcasePackageAssociation(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db")
+class TestDeleteShowcasePackageAssociation(object):
     def test_association_delete_no_args(self):
-        '''
+        """
         Calling sc/pkg association delete with no args raises ValidationError.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_association_delete',
-                                context=context)
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_association_delete", context=context,
+            )
 
     def test_association_delete_missing_arg(self):
-        '''
+        """
         Calling sc/pkg association delete with a missing arg raises
         ValidationError.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
+        package_id = factories.Dataset()["id"]
 
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_association_delete',
-                                context=context, package_id=package_id)
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_association_delete",
+                context=context,
+                package_id=package_id,
+            )
 
     def test_association_delete_by_id(self):
-        '''
+        """
         Calling sc/pkg association delete with correct args (package ids)
         correctly deletes an association.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
-        showcase_id = factories.Dataset(type='showcase')['id']
+        package_id = factories.Dataset()["id"]
+        showcase_id = factories.Dataset(type="showcase")["id"]
 
-        context = {'user': sysadmin['name']}
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_id,
-                            showcase_id=showcase_id)
+        context = {"user": sysadmin["name"]}
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_id,
+            showcase_id=showcase_id,
+        )
 
         # One association object created
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 1)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 1
 
-        helpers.call_action('ckanext_showcase_package_association_delete',
-                            context=context, package_id=package_id,
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_delete",
+            context=context,
+            package_id=package_id,
+            showcase_id=showcase_id,
+        )
 
     def test_association_delete_attempt_with_non_existent_association(self):
-        '''
+        """
         Attempting to delete a non-existent association (package ids exist,
         but aren't associated with each other), will cause a NotFound error.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
-        showcase_id = factories.Dataset(type='showcase')['id']
+        package_id = factories.Dataset()["id"]
+        showcase_id = factories.Dataset(type="showcase")["id"]
 
         # No existing associations
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
 
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ObjectNotFound, helpers.call_action,
-                                'ckanext_showcase_package_association_delete',
-                                context=context, package_id=package_id,
-                                showcase_id=showcase_id)
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ObjectNotFound):
+            helpers.call_action(
+                "ckanext_showcase_package_association_delete",
+                context=context,
+                package_id=package_id,
+                showcase_id=showcase_id,
+            )
 
     def test_association_delete_attempt_with_bad_package_ids(self):
-        '''
+        """
         Attempting to delete an association by passing non-existent package
         ids will cause a ValidationError.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         # No existing associations
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
 
-        context = {'user': sysadmin['name']}
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_association_delete',
-                                context=context, package_id="my-bad-package-id",
-                                showcase_id="my-bad-showcase-id")
+        context = {"user": sysadmin["name"]}
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_association_delete",
+                context=context,
+                package_id="my-bad-package-id",
+                showcase_id="my-bad-showcase-id",
+            )
 
     def test_association_delete_retains_packages(self):
-        '''
+        """
         Deleting a sc/pkg association doesn't delete the associated packages.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
-        package_id = factories.Dataset()['id']
-        showcase_id = factories.Dataset(type='showcase')['id']
-
-        context = {'user': sysadmin['name']}
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_id,
-                            showcase_id=showcase_id)
-
-        helpers.call_action('ckanext_showcase_package_association_delete',
-                            context=context, package_id=package_id,
-                            showcase_id=showcase_id)
+        package_id = factories.Dataset()["id"]
+        showcase_id = factories.Dataset(type="showcase")["id"]
+
+        context = {"user": sysadmin["name"]}
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_id,
+            showcase_id=showcase_id,
+        )
+
+        helpers.call_action(
+            "ckanext_showcase_package_association_delete",
+            context=context,
+            package_id=package_id,
+            showcase_id=showcase_id,
+        )
 
         # package still exist
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'dataset').count(), 1)
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "dataset")
+            .count()
+            == 1
+        )
 
         # showcase still exist
-        nosetools.assert_equal(model.Session.query(Package)
-                               .filter(Package.type == 'showcase').count(), 1)
-
+        assert (
+            model.Session.query(Package)
+            .filter(Package.type == "showcase")
+            .count()
+            == 1
+        )
 
-class TestRemoveShowcaseAdmin(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db")
+class TestRemoveShowcaseAdmin(object):
     def test_showcase_admin_remove_deletes_showcase_admin_user(self):
-        '''
+        """
         Calling ckanext_showcase_admin_remove deletes ShowcaseAdmin object.
-        '''
+        """
         user = factories.User()
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
 
         # There's a ShowcaseAdmin obj
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 1)
+        assert model.Session.query(ShowcaseAdmin).count() == 1
 
-        helpers.call_action('ckanext_showcase_admin_remove', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_remove", context={}, username=user["name"]
+        )
 
         # There's no ShowcaseAdmin obj
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 0)
-        nosetools.assert_equal(ShowcaseAdmin.get_showcase_admin_ids(), [])
+        assert model.Session.query(ShowcaseAdmin).count() == 0
+        assert ShowcaseAdmin.get_showcase_admin_ids() == []
 
     def test_showcase_admin_delete_user_removes_showcase_admin_object(self):
-        '''
+        """
         Deleting a user also deletes the corresponding ShowcaseAdmin object.
-        '''
+        """
         user = factories.User()
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
 
         # There's a ShowcaseAdmin object
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 1)
-        nosetools.assert_true(user['id'] in ShowcaseAdmin.get_showcase_admin_ids())
+        assert model.Session.query(ShowcaseAdmin).count() == 1
+        assert user["id"] in ShowcaseAdmin.get_showcase_admin_ids()
 
         # purge the user, should also delete the ShowcaseAdmin object
         # associated with it.
-        user_obj = model.Session.query(model.User).get(user['id'])
+        user_obj = model.Session.query(model.User).get(user["id"])
         user_obj.purge()
         model.repo.commit_and_remove()
 
         # The ShowcaseAdmin has also been removed
-        nosetools.assert_equal(model.Session.query(ShowcaseAdmin).count(), 0)
-        nosetools.assert_equal(ShowcaseAdmin.get_showcase_admin_ids(), [])
+        assert model.Session.query(ShowcaseAdmin).count() == 0
+        assert ShowcaseAdmin.get_showcase_admin_ids() == []
 
     def test_showcase_admin_remove_retains_user(self):
-        '''
+        """
         Deleting a ShowcaseAdmin object doesn't delete the corresponding user.
-        '''
+        """
 
         user = factories.User()
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
 
         # We have a user
-        user_obj = model.Session.query(model.User).get(user['id'])
-        nosetools.assert_true(user_obj is not None)
+        user_obj = model.Session.query(model.User).get(user["id"])
+        assert user_obj is not None
 
-        helpers.call_action('ckanext_showcase_admin_remove', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_remove", context={}, username=user["name"]
+        )
 
         # We still have a user
-        user_obj = model.Session.query(model.User).get(user['id'])
-        nosetools.assert_true(user_obj is not None)
+        user_obj = model.Session.query(model.User).get(user["id"])
+        assert user_obj is not None
 
     def test_showcase_admin_remove_with_bad_username(self):
-        '''
+        """
         Calling showcase admin remove with a non-existent user raises
         ValidationError.
-        '''
+        """
 
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_admin_remove', context={},
-                                username='no-one-here')
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_admin_remove",
+                context={},
+                username="no-one-here",
+            )
 
     def test_showcase_admin_remove_with_no_args(self):
-        '''
+        """
         Calling showcase admin remove with no arg raises ValidationError.
-        '''
+        """
 
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_admin_remove', context={})
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_admin_remove", context={},
+            )
diff --git a/ckanext/showcase/tests/action/test_get.py b/ckanext/showcase/tests/action/test_get.py
index fd02bca7..b4748abc 100644
--- a/ckanext/showcase/tests/action/test_get.py
+++ b/ckanext/showcase/tests/action/test_get.py
@@ -1,599 +1,724 @@
-from nose import tools as nosetools
-from nose import SkipTest
+# -*- coding: utf-8 -*-
 
-import ckan.plugins.toolkit as toolkit
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
-
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
-
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
+import pytest
 
+from ckan.tests import factories, helpers
+import ckan.plugins.toolkit as toolkit
 
-class TestShowcaseShow(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseShow(object):
     def test_showcase_show_no_args(self):
-        '''
+        """
         Calling showcase show with no args raises a ValidationError.
-        '''
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_show')
+        """
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action("ckanext_showcase_show")
 
     def test_showcase_show_with_id(self):
-        '''
+        """
         Calling showcase show with id arg returns showcase dict.
-        '''
-        my_showcase = factories.Dataset(type='showcase', name='my-showcase')
+        """
+        my_showcase = factories.Dataset(type="showcase", name="my-showcase")
 
-        showcase_shown = helpers.call_action('ckanext_showcase_show', id=my_showcase['id'])
+        showcase_shown = helpers.call_action(
+            "ckanext_showcase_show", id=my_showcase["id"]
+        )
 
-        nosetools.assert_equal(my_showcase['name'], showcase_shown['name'])
+        assert my_showcase["name"] == showcase_shown["name"]
 
     def test_showcase_show_with_name(self):
-        '''
+        """
         Calling showcase show with name arg returns showcase dict.
-        '''
-        my_showcase = factories.Dataset(type='showcase', name='my-showcase')
+        """
+        my_showcase = factories.Dataset(type="showcase", name="my-showcase")
 
-        showcase_shown = helpers.call_action('ckanext_showcase_show', id=my_showcase['name'])
+        showcase_shown = helpers.call_action(
+            "ckanext_showcase_show", id=my_showcase["name"]
+        )
 
-        nosetools.assert_equal(my_showcase['id'], showcase_shown['id'])
+        assert my_showcase["id"] == showcase_shown["id"]
 
     def test_showcase_show_with_nonexisting_name(self):
-        '''
+        """
         Calling showcase show with bad name arg returns ObjectNotFound.
-        '''
-        factories.Dataset(type='showcase', name='my-showcase')
+        """
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        nosetools.assert_raises(toolkit.ObjectNotFound, helpers.call_action,
-                                'ckanext_showcase_show', id='my-bad-name')
+        with pytest.raises(toolkit.ObjectNotFound):
+            helpers.call_action(
+                "ckanext_showcase_show", id="my-bad-name",
+            )
 
     def test_showcase_show_num_datasets_added(self):
-        '''
+        """
         num_datasets property returned with showcase dict.
-        '''
-        my_showcase = factories.Dataset(type='showcase', name='my-showcase')
+        """
+        my_showcase = factories.Dataset(type="showcase", name="my-showcase")
 
-        showcase_shown = helpers.call_action('ckanext_showcase_show', id=my_showcase['name'])
+        showcase_shown = helpers.call_action(
+            "ckanext_showcase_show", id=my_showcase["name"]
+        )
 
-        nosetools.assert_true('num_datasets' in showcase_shown)
-        nosetools.assert_equal(showcase_shown['num_datasets'], 0)
+        assert "num_datasets" in showcase_shown
+        assert showcase_shown["num_datasets"] == 0
 
     def test_showcase_show_num_datasets_correct_value(self):
-        '''
+        """
         num_datasets property has correct value.
-        '''
+        """
 
         sysadmin = factories.User(sysadmin=True)
 
-        my_showcase = factories.Dataset(type='showcase', name='my-showcase')
+        my_showcase = factories.Dataset(type="showcase", name="my-showcase")
         package_one = factories.Dataset()
         package_two = factories.Dataset()
 
-        context = {'user': sysadmin['name']}
+        context = {"user": sysadmin["name"]}
         # create an association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_one['id'],
-                            showcase_id=my_showcase['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_two['id'],
-                            showcase_id=my_showcase['id'])
-
-        showcase_shown = helpers.call_action('ckanext_showcase_show', id=my_showcase['name'])
-
-        nosetools.assert_equal(showcase_shown['num_datasets'], 2)
-
-    def test_showcase_show_num_datasets_correct_only_count_active_datasets(self):
-        '''
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_one["id"],
+            showcase_id=my_showcase["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_two["id"],
+            showcase_id=my_showcase["id"],
+        )
+
+        showcase_shown = helpers.call_action(
+            "ckanext_showcase_show", id=my_showcase["name"]
+        )
+
+        assert showcase_shown["num_datasets"] == 2
+
+    def test_showcase_show_num_datasets_correct_only_count_active_datasets(
+        self,
+    ):
+        """
         num_datasets property has correct value when some previously
         associated datasets have been datasets.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
-        my_showcase = factories.Dataset(type='showcase', name='my-showcase')
+        my_showcase = factories.Dataset(type="showcase", name="my-showcase")
         package_one = factories.Dataset()
         package_two = factories.Dataset()
         package_three = factories.Dataset()
 
-        context = {'user': sysadmin['name']}
+        context = {"user": sysadmin["name"]}
         # create the associations
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_one['id'],
-                            showcase_id=my_showcase['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_two['id'],
-                            showcase_id=my_showcase['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_three['id'],
-                            showcase_id=my_showcase['id'])
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_one["id"],
+            showcase_id=my_showcase["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_two["id"],
+            showcase_id=my_showcase["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_three["id"],
+            showcase_id=my_showcase["id"],
+        )
 
         # delete the first package
-        helpers.call_action('package_delete', context=context, id=package_one['id'])
+        helpers.call_action(
+            "package_delete", context=context, id=package_one["id"]
+        )
 
-        showcase_shown = helpers.call_action('ckanext_showcase_show', id=my_showcase['name'])
+        showcase_shown = helpers.call_action(
+            "ckanext_showcase_show", id=my_showcase["name"]
+        )
 
         # the num_datasets should only include active datasets
-        nosetools.assert_equal(showcase_shown['num_datasets'], 2)
+        assert showcase_shown["num_datasets"] == 2
 
-    def test_showcase_anon_user_can_see_package_list_when_showcase_association_was_deleted(self):
-        '''
+    def test_showcase_anon_user_can_see_package_list_when_showcase_association_was_deleted(
+        self, app
+    ):
+        """
         When a showcase is deleted, the remaining associations with formerly associated
         packages or showcases can still be displayed.
-        '''
-        app = self._get_test_app()
+        """
 
         sysadmin = factories.User(sysadmin=True)
 
-        showcase_one = factories.Dataset(type='showcase', name='showcase-one')
-        showcase_two = factories.Dataset(type='showcase', name='showcase-two')
+        showcase_one = factories.Dataset(type="showcase", name="showcase-one")
+        showcase_two = factories.Dataset(type="showcase", name="showcase-two")
         package_one = factories.Dataset()
         package_two = factories.Dataset()
 
-        admin_context = {'user': sysadmin['name']}
+        admin_context = {"user": sysadmin["name"]}
 
         # create the associations
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=admin_context, package_id=package_one['id'],
-                            showcase_id=showcase_one['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=admin_context, package_id=package_one['id'],
-                            showcase_id=showcase_two['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=admin_context, package_id=package_two['id'],
-                            showcase_id=showcase_one['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=admin_context, package_id=package_two['id'],
-                            showcase_id=showcase_two['id'])
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=admin_context,
+            package_id=package_one["id"],
+            showcase_id=showcase_one["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=admin_context,
+            package_id=package_one["id"],
+            showcase_id=showcase_two["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=admin_context,
+            package_id=package_two["id"],
+            showcase_id=showcase_one["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=admin_context,
+            package_id=package_two["id"],
+            showcase_id=showcase_two["id"],
+        )
 
         # delete one of the associated showcases
-        helpers.call_action('package_delete', context=admin_context,
-                            id=showcase_two['id'])
+        helpers.call_action(
+            "package_delete", context=admin_context, id=showcase_two["id"]
+        )
 
         # the anon user can still see the associated packages of remaining showcase
         associated_packages = helpers.call_action(
-            'ckanext_showcase_package_list',
-            showcase_id=showcase_one['id'])
+            "ckanext_showcase_package_list", showcase_id=showcase_one["id"]
+        )
 
-        nosetools.assert_equal(len(associated_packages), 2)
+        assert len(associated_packages) == 2
 
         # overview of packages can still be seen
         app.get("/dataset", status=200)
 
 
-class TestShowcaseList(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseList(object):
     def test_showcase_list(self):
-        '''Showcase list action returns names of showcases in site.'''
+        """Showcase list action returns names of showcases in site."""
 
-        showcase_one = factories.Dataset(type='showcase')
-        showcase_two = factories.Dataset(type='showcase')
-        showcase_three = factories.Dataset(type='showcase')
+        showcase_one = factories.Dataset(type="showcase")
+        showcase_two = factories.Dataset(type="showcase")
+        showcase_three = factories.Dataset(type="showcase")
 
-        showcase_list = helpers.call_action('ckanext_showcase_list')
+        showcase_list = helpers.call_action("ckanext_showcase_list")
 
-        showcase_list_name_id = [(sc['name'], sc['id']) for sc in showcase_list]
+        showcase_list_name_id = [
+            (sc["name"], sc["id"]) for sc in showcase_list
+        ]
 
-        nosetools.assert_equal(len(showcase_list), 3)
-        nosetools.assert_true(sorted(showcase_list_name_id) ==
-                              sorted([(showcase['name'], showcase['id'])
-                                     for showcase in [showcase_one,
-                                                      showcase_two,
-                                                      showcase_three]]))
+        assert len(showcase_list) == 3
+        assert sorted(showcase_list_name_id) == sorted(
+            [
+                (showcase["name"], showcase["id"])
+                for showcase in [showcase_one, showcase_two, showcase_three,]
+            ]
+        )
 
     def test_showcase_list_no_datasets(self):
-        '''
+        """
         Showcase list action doesn't return normal datasets (of type
         'dataset').
-        '''
-        showcase_one = factories.Dataset(type='showcase')
+        """
+        showcase_one = factories.Dataset(type="showcase")
         dataset_one = factories.Dataset()
         dataset_two = factories.Dataset()
 
-        showcase_list = helpers.call_action('ckanext_showcase_list')
+        showcase_list = helpers.call_action("ckanext_showcase_list")
 
-        showcase_list_name_id = [(sc['name'], sc['id']) for sc in showcase_list]
+        showcase_list_name_id = [
+            (sc["name"], sc["id"]) for sc in showcase_list
+        ]
 
-        nosetools.assert_equal(len(showcase_list), 1)
-        nosetools.assert_true((showcase_one['name'], showcase_one['id']) in showcase_list_name_id)
-        nosetools.assert_true((dataset_one['name'], dataset_one['id']) not in showcase_list_name_id)
-        nosetools.assert_true((dataset_two['name'], dataset_two['id']) not in showcase_list_name_id)
+        assert len(showcase_list) == 1
+        assert (
+            showcase_one["name"],
+            showcase_one["id"],
+        ) in showcase_list_name_id
+        assert (
+            dataset_one["name"],
+            dataset_one["id"],
+        ) not in showcase_list_name_id
+        assert (
+            dataset_two["name"],
+            dataset_two["id"],
+        ) not in showcase_list_name_id
 
 
-class TestShowcasePackageList(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcasePackageList(object):
 
-    '''Tests for ckanext_showcase_package_list'''
+    """Tests for ckanext_showcase_package_list"""
 
     def test_showcase_package_list_no_packages(self):
-        '''
+        """
         Calling ckanext_showcase_package_list with a showcase that has no
         packages returns an empty list.
-        '''
-        showcase_id = factories.Dataset(type='showcase')['id']
+        """
+        showcase_id = factories.Dataset(type="showcase")["id"]
 
-        pkg_list = helpers.call_action('ckanext_showcase_package_list',
-                                       showcase_id=showcase_id)
+        pkg_list = helpers.call_action(
+            "ckanext_showcase_package_list", showcase_id=showcase_id
+        )
 
-        nosetools.assert_equal(pkg_list, [])
+        assert pkg_list == []
 
     def test_showcase_package_list_works_with_name(self):
-        '''
+        """
         Calling ckanext_showcase_package_list with a showcase name doesn't
         raise a ValidationError.
-        '''
-        showcase_name = factories.Dataset(type='showcase')['name']
+        """
+        showcase_name = factories.Dataset(type="showcase")["name"]
 
-        pkg_list = helpers.call_action('ckanext_showcase_package_list',
-                                       showcase_id=showcase_name)
+        pkg_list = helpers.call_action(
+            "ckanext_showcase_package_list", showcase_id=showcase_name
+        )
 
-        nosetools.assert_equal(pkg_list, [])
+        assert pkg_list == []
 
     def test_showcase_package_list_wrong_showcase_id(self):
-        '''
+        """
         Calling ckanext_showcase_package_list with a bad showcase id raises a
         ValidationError.
-        '''
-        factories.Dataset(type='showcase')['id']
+        """
+        factories.Dataset(type="showcase")["id"]
 
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_list',
-                                showcase_id='a-bad-id')
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_list", showcase_id="a-bad-id",
+            )
 
     def test_showcase_package_list_showcase_has_package(self):
-        '''
+        """
         Calling ckanext_showcase_package_list with a showcase that has a
         package should return that package.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package = factories.Dataset()
-        showcase_id = factories.Dataset(type='showcase')['id']
-        context = {'user': sysadmin['name']}
+        showcase_id = factories.Dataset(type="showcase")["id"]
+        context = {"user": sysadmin["name"]}
         # create an association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package["id"],
+            showcase_id=showcase_id,
+        )
 
-        pkg_list = helpers.call_action('ckanext_showcase_package_list',
-                                       showcase_id=showcase_id)
+        pkg_list = helpers.call_action(
+            "ckanext_showcase_package_list", showcase_id=showcase_id
+        )
 
         # We've got an item in the pkg_list
-        nosetools.assert_equal(len(pkg_list), 1)
+        assert len(pkg_list) == 1
         # The list item should have the correct name property
-        nosetools.assert_equal(pkg_list[0]['name'], package['name'])
+        assert pkg_list[0]["name"] == package["name"]
 
     def test_showcase_package_list_showcase_has_two_packages(self):
-        '''
+        """
         Calling ckanext_showcase_package_list with a showcase that has two
         packages should return the packages.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package_one = factories.Dataset()
         package_two = factories.Dataset()
-        showcase_id = factories.Dataset(type='showcase')['id']
-        context = {'user': sysadmin['name']}
+        showcase_id = factories.Dataset(type="showcase")["id"]
+        context = {"user": sysadmin["name"]}
         # create first association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_one['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_one["id"],
+            showcase_id=showcase_id,
+        )
         # create second association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_two['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_two["id"],
+            showcase_id=showcase_id,
+        )
 
-        pkg_list = helpers.call_action('ckanext_showcase_package_list',
-                                       showcase_id=showcase_id)
+        pkg_list = helpers.call_action(
+            "ckanext_showcase_package_list", showcase_id=showcase_id
+        )
 
         # We've got two items in the pkg_list
-        nosetools.assert_equal(len(pkg_list), 2)
+        assert len(pkg_list) == 2
 
-    def test_showcase_package_list_showcase_only_contains_active_datasets(self):
-        '''
+    def test_showcase_package_list_showcase_only_contains_active_datasets(
+        self,
+    ):
+        """
         Calling ckanext_showcase_package_list will only return active datasets
         (not deleted ones).
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package_one = factories.Dataset()
         package_two = factories.Dataset()
         package_three = factories.Dataset()
-        showcase_id = factories.Dataset(type='showcase')['id']
-        context = {'user': sysadmin['name']}
+        showcase_id = factories.Dataset(type="showcase")["id"]
+        context = {"user": sysadmin["name"]}
         # create first association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_one['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_one["id"],
+            showcase_id=showcase_id,
+        )
         # create second association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_two['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_two["id"],
+            showcase_id=showcase_id,
+        )
         # create third association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package_three['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package_three["id"],
+            showcase_id=showcase_id,
+        )
 
         # delete the first package
-        helpers.call_action('package_delete', context=context, id=package_one['id'])
+        helpers.call_action(
+            "package_delete", context=context, id=package_one["id"]
+        )
 
-        pkg_list = helpers.call_action('ckanext_showcase_package_list',
-                                       showcase_id=showcase_id)
+        pkg_list = helpers.call_action(
+            "ckanext_showcase_package_list", showcase_id=showcase_id
+        )
 
         # We've got two items in the pkg_list
-        nosetools.assert_equal(len(pkg_list), 2)
+        assert len(pkg_list) == 2
 
-        pkg_list_ids = [pkg['id'] for pkg in pkg_list]
-        nosetools.assert_true(package_two['id'] in pkg_list_ids)
-        nosetools.assert_true(package_three['id'] in pkg_list_ids)
-        nosetools.assert_false(package_one['id'] in pkg_list_ids)
+        pkg_list_ids = [pkg["id"] for pkg in pkg_list]
+        assert package_two["id"] in pkg_list_ids
+        assert package_three["id"] in pkg_list_ids
+        assert package_one["id"] not in pkg_list_ids
 
     def test_showcase_package_list_package_isnot_a_showcase(self):
-        '''
+        """
         Calling ckanext_showcase_package_list with a package id should raise a
         ValidationError.
 
         Since Showcases are Packages under the hood, make sure we treat them
         differently.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package = factories.Dataset()
-        showcase_id = factories.Dataset(type='showcase')['id']
-        context = {'user': sysadmin['name']}
+        showcase_id = factories.Dataset(type="showcase")["id"]
+        context = {"user": sysadmin["name"]}
         # create an association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package['id'],
-                            showcase_id=showcase_id)
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package["id"],
+            showcase_id=showcase_id,
+        )
 
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_showcase_package_list',
-                                showcase_id=package['id'])
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_showcase_package_list", showcase_id=package["id"],
+            )
 
 
-class TestPackageShowcaseList(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestPackageShowcaseList(object):
 
-    '''Tests for ckanext_package_showcase_list'''
+    """Tests for ckanext_package_showcase_list"""
 
     def test_package_showcase_list_no_showcases(self):
-        '''
+        """
         Calling ckanext_package_showcase_list with a package that has no
         showcases returns an empty list.
-        '''
-        package_id = factories.Dataset()['id']
+        """
+        package_id = factories.Dataset()["id"]
 
-        showcase_list = helpers.call_action('ckanext_package_showcase_list',
-                                            package_id=package_id)
+        showcase_list = helpers.call_action(
+            "ckanext_package_showcase_list", package_id=package_id
+        )
 
-        nosetools.assert_equal(showcase_list, [])
+        assert showcase_list == []
 
     def test_package_showcase_list_works_with_name(self):
-        '''
+        """
         Calling ckanext_package_showcase_list with a package name doesn't
         raise a ValidationError.
-        '''
-        package_name = factories.Dataset()['name']
+        """
+        package_name = factories.Dataset()["name"]
 
-        showcase_list = helpers.call_action('ckanext_package_showcase_list',
-                                            package_id=package_name)
+        showcase_list = helpers.call_action(
+            "ckanext_package_showcase_list", package_id=package_name
+        )
 
-        nosetools.assert_equal(showcase_list, [])
+        assert showcase_list == []
 
     def test_package_showcase_list_wrong_showcase_id(self):
-        '''
+        """
         Calling ckanext_package_showcase_list with a bad package id raises a
         ValidationError.
-        '''
-        factories.Dataset()['id']
+        """
+        factories.Dataset()["id"]
 
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_package_showcase_list',
-                                showcase_id='a-bad-id')
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_package_showcase_list", showcase_id="a-bad-id",
+            )
 
     def test_package_showcase_list_showcase_has_package(self):
-        '''
+        """
         Calling ckanext_package_showcase_list with a package that has a
         showcase should return that showcase.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package = factories.Dataset()
-        showcase = factories.Dataset(type='showcase')
-        context = {'user': sysadmin['name']}
+        showcase = factories.Dataset(type="showcase")
+        context = {"user": sysadmin["name"]}
         # create an association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package['id'],
-                            showcase_id=showcase['id'])
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package["id"],
+            showcase_id=showcase["id"],
+        )
 
-        showcase_list = helpers.call_action('ckanext_package_showcase_list',
-                                            package_id=package['id'])
+        showcase_list = helpers.call_action(
+            "ckanext_package_showcase_list", package_id=package["id"]
+        )
 
         # We've got an item in the showcase_list
-        nosetools.assert_equal(len(showcase_list), 1)
+        assert len(showcase_list) == 1
         # The list item should have the correct name property
-        nosetools.assert_equal(showcase_list[0]['name'], showcase['name'])
+        assert showcase_list[0]["name"] == showcase["name"]
 
     def test_package_showcase_list_showcase_has_two_packages(self):
-        '''
+        """
         Calling ckanext_package_showcase_list with a package that has two
         showcases should return the showcases.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package = factories.Dataset()
-        showcase_one = factories.Dataset(type='showcase')
-        showcase_two = factories.Dataset(type='showcase')
-        context = {'user': sysadmin['name']}
+        showcase_one = factories.Dataset(type="showcase")
+        showcase_two = factories.Dataset(type="showcase")
+        context = {"user": sysadmin["name"]}
         # create first association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package['id'],
-                            showcase_id=showcase_one['id'])
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package["id"],
+            showcase_id=showcase_one["id"],
+        )
         # create second association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package['id'],
-                            showcase_id=showcase_two['id'])
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package["id"],
+            showcase_id=showcase_two["id"],
+        )
 
-        showcase_list = helpers.call_action('ckanext_package_showcase_list',
-                                            package_id=package['id'])
+        showcase_list = helpers.call_action(
+            "ckanext_package_showcase_list", package_id=package["id"]
+        )
 
         # We've got two items in the showcase_list
-        nosetools.assert_equal(len(showcase_list), 2)
+        assert len(showcase_list) == 2
 
     def test_package_showcase_list_package_isnot_a_showcase(self):
-        '''
+        """
         Calling ckanext_package_showcase_list with a showcase id should raise a
         ValidationError.
 
         Since Showcases are Packages under the hood, make sure we treat them
         differently.
-        '''
+        """
         sysadmin = factories.User(sysadmin=True)
 
         package = factories.Dataset()
-        showcase = factories.Dataset(type='showcase')
-        context = {'user': sysadmin['name']}
+        showcase = factories.Dataset(type="showcase")
+        context = {"user": sysadmin["name"]}
         # create an association
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=package['id'],
-                            showcase_id=showcase['id'])
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=package["id"],
+            showcase_id=showcase["id"],
+        )
 
-        nosetools.assert_raises(toolkit.ValidationError, helpers.call_action,
-                                'ckanext_package_showcase_list',
-                                package_id=showcase['id'])
+        with pytest.raises(toolkit.ValidationError):
+            helpers.call_action(
+                "ckanext_package_showcase_list", package_id=showcase["id"],
+            )
 
 
-class TestShowcaseAdminList(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAdminList(object):
 
-    '''Tests for ckanext_showcase_admin_list'''
+    """Tests for ckanext_showcase_admin_list"""
 
     def test_showcase_admin_list_no_showcase_admins(self):
-        '''
+        """
         Calling ckanext_showcase_admin_list on a site that has no showcases
         admins returns an empty list.
-        '''
+        """
 
-        showcase_admin_list = helpers.call_action('ckanext_showcase_admin_list')
+        showcase_admin_list = helpers.call_action(
+            "ckanext_showcase_admin_list"
+        )
 
-        nosetools.assert_equal(showcase_admin_list, [])
+        assert showcase_admin_list == []
 
     def test_showcase_admin_list_users(self):
-        '''
+        """
         Calling ckanext_showcase_admin_list will return users who are showcase
         admins.
-        '''
+        """
         user_one = factories.User()
         user_two = factories.User()
         user_three = factories.User()
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_one['name'])
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_two['name'])
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_three['name'])
-
-        showcase_admin_list = helpers.call_action('ckanext_showcase_admin_list', context={})
-
-        nosetools.assert_equal(len(showcase_admin_list), 3)
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user_one["name"]
+        )
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user_two["name"]
+        )
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=user_three["name"],
+        )
+
+        showcase_admin_list = helpers.call_action(
+            "ckanext_showcase_admin_list", context={}
+        )
+
+        assert len(showcase_admin_list) == 3
         for user in [user_one, user_two, user_three]:
-            nosetools.assert_true({'name': user['name'], 'id': user['id']} in showcase_admin_list)
+            assert {
+                "name": user["name"],
+                "id": user["id"],
+            } in showcase_admin_list
 
     def test_showcase_admin_only_lists_admin_users(self):
-        '''
+        """
         Calling ckanext_showcase_admin_list will only return users who are
         showcase admins.
-        '''
+        """
         user_one = factories.User()
         user_two = factories.User()
         user_three = factories.User()
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_one['name'])
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_two['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user_one["name"]
+        )
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user_two["name"]
+        )
 
-        showcase_admin_list = helpers.call_action('ckanext_showcase_admin_list', context={})
+        showcase_admin_list = helpers.call_action(
+            "ckanext_showcase_admin_list", context={}
+        )
 
-        nosetools.assert_equal(len(showcase_admin_list), 2)
+        assert len(showcase_admin_list) == 2
         # user three isn't in list
-        nosetools.assert_true({'name': user_three['name'], 'id': user_three['id']} not in showcase_admin_list)
+        assert {
+            "name": user_three["name"],
+            "id": user_three["id"],
+        } not in showcase_admin_list
 
 
-class TestPackageSearchBeforeSearch(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestPackageSearchBeforeSearch(object):
 
-    '''
+    """
     Extension uses the `before_search` method to alter search parameters.
-    '''
+    """
 
     def test_package_search_no_additional_filters(self):
-        '''
+        """
         Perform package_search with no additional filters should not include
         showcases.
-        '''
+        """
         factories.Dataset()
         factories.Dataset()
-        factories.Dataset(type='showcase')
-        factories.Dataset(type='custom')
+        factories.Dataset(type="showcase")
+        factories.Dataset(type="custom")
 
-        search_results = helpers.call_action('package_search', context={})['results']
+        search_results = helpers.call_action("package_search", context={})[
+            "results"
+        ]
 
-        types = [result['type'] for result in search_results]
+        types = [result["type"] for result in search_results]
 
-        nosetools.assert_equal(len(search_results), 3)
-        nosetools.assert_true('showcase' not in types)
-        nosetools.assert_true('custom' in types)
+        assert len(search_results) == 3
+        assert "showcase" not in types
+        assert "custom" in types
 
     def test_package_search_filter_include_showcase(self):
-        '''
+        """
         package_search filtered to include datasets of type showcase should
         only include showcases.
-        '''
+        """
         factories.Dataset()
         factories.Dataset()
-        factories.Dataset(type='showcase')
-        factories.Dataset(type='custom')
+        factories.Dataset(type="showcase")
+        factories.Dataset(type="custom")
 
-        search_results = helpers.call_action('package_search', context={},
-                                             fq='dataset_type:showcase')['results']
+        search_results = helpers.call_action(
+            "package_search", context={}, fq="dataset_type:showcase"
+        )["results"]
 
-        types = [result['type'] for result in search_results]
+        types = [result["type"] for result in search_results]
 
-        nosetools.assert_equal(len(search_results), 1)
-        nosetools.assert_true('showcase' in types)
-        nosetools.assert_true('custom' not in types)
-        nosetools.assert_true('dataset' not in types)
+        assert len(search_results) == 1
+        assert "showcase" in types
+        assert "custom" not in types
+        assert "dataset" not in types
 
 
-class TestUserShowBeforeSearch(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestUserShowBeforeSearch(object):
 
-    '''
+    """
     Extension uses the `before_search` method to alter results of user_show
     (via package_search).
-    '''
+    """
 
     def test_user_show_no_additional_filters(self):
-        '''
+        """
         Perform package_search with no additional filters should not include
         showcases.
-        '''
-        if not toolkit.check_ckan_version(min_version='2.4'):
-            raise SkipTest('Filtering out showcases requires CKAN 2.4+ (ckan/ckan/issues/2380)')
+        """
 
         user = factories.User()
         factories.Dataset(user=user)
         factories.Dataset(user=user)
-        factories.Dataset(user=user, type='showcase')
-        factories.Dataset(user=user, type='custom')
+        factories.Dataset(user=user, type="showcase")
+        factories.Dataset(user=user, type="custom")
 
-        search_results = helpers.call_action('user_show', context={},
-                                             include_datasets=True,
-                                             id=user['name'])['datasets']
+        search_results = helpers.call_action(
+            "user_show", context={}, include_datasets=True, id=user["name"]
+        )["datasets"]
 
-        types = [result['type'] for result in search_results]
+        types = [result["type"] for result in search_results]
 
-        nosetools.assert_equal(len(search_results), 3)
-        nosetools.assert_true('showcase' not in types)
-        nosetools.assert_true('custom' in types)
+        assert len(search_results) == 3
+        assert "showcase" not in types
+        assert "custom" in types
diff --git a/ckanext/showcase/tests/ckan_setup.py b/ckanext/showcase/tests/ckan_setup.py
new file mode 100644
index 00000000..a953c22f
--- /dev/null
+++ b/ckanext/showcase/tests/ckan_setup.py
@@ -0,0 +1,38 @@
+try:
+    from ckan.tests.pytest_ckan.ckan_setup import *
+except ImportError:
+    from ckan.config.middleware import make_app
+    from ckan.common import config
+
+    import pkg_resources
+    from paste.deploy import loadapp
+    import sys
+    import os
+
+    import pylons
+    from pylons.i18n.translation import _get_translator
+
+    def pytest_addoption(parser):
+        """Allow using custom config file during tests.
+        """
+        parser.addoption(u"--ckan-ini", action=u"store")
+
+    def pytest_sessionstart(session):
+        """Initialize CKAN environment.
+        """
+        global pylonsapp
+        path = os.getcwd()
+        sys.path.insert(0, path)
+        pkg_resources.working_set.add_entry(path)
+        pylonsapp = loadapp(
+            "config:" + session.config.option.ckan_ini, relative_to=path,
+        )
+
+        # Initialize a translator for tests that utilize i18n
+        translator = _get_translator(pylons.config.get("lang"))
+        pylons.translator._push_object(translator)
+
+        class FakeResponse:
+            headers = {}  # because render wants to delete Pragma
+
+        pylons.response._push_object(FakeResponse)
diff --git a/ckanext/showcase/tests/fixtures.py b/ckanext/showcase/tests/fixtures.py
new file mode 100644
index 00000000..53071972
--- /dev/null
+++ b/ckanext/showcase/tests/fixtures.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+
+try:
+    from ckan.tests.pytest_ckan.fixtures import *
+
+except ImportError:
+    import pytest
+
+    import ckan.tests.helpers as test_helpers
+    import ckan.plugins
+    import ckan.lib.search as search
+
+    from ckan.common import config
+
+    from ckanext.showcase.model import setup as showcase_setup
+
+    @pytest.fixture
+    def ckan_config(request, monkeypatch):
+        """Allows to override the configuration object used by tests
+
+        Takes into account config patches introduced by the ``ckan_config``
+        mark.
+
+        If you just want to set one or more configuration options for the
+        scope of a test (or a test class), use the ``ckan_config`` mark::
+
+            @pytest.mark.ckan_config('ckan.auth.create_unowned_dataset', True)
+            def test_auth_create_unowned_dataset():
+
+                # ...
+
+        To use the custom config inside a test, apply the
+        ``ckan_config`` mark to it and inject the ``ckan_config`` fixture:
+
+        .. literalinclude:: /../ckan/tests/pytest_ckan/test_fixtures.py
+           :start-after: # START-CONFIG-OVERRIDE
+           :end-before: # END-CONFIG-OVERRIDE
+
+        If the change only needs to be applied locally, use the
+        ``monkeypatch`` fixture
+
+        .. literalinclude:: /../ckan/tests/test_common.py
+           :start-after: # START-CONFIG-OVERRIDE
+           :end-before: # END-CONFIG-OVERRIDE
+
+        """
+        _original = config.copy()
+        for mark in request.node.iter_markers(u"ckan_config"):
+            monkeypatch.setitem(config, *mark.args)
+        yield config
+        config.clear()
+        config.update(_original)
+
+    @pytest.fixture
+    def make_app(ckan_config):
+        """Factory for client app instances.
+
+        Unless you need to create app instances lazily for some reason,
+        use the ``app`` fixture instead.
+        """
+        return test_helpers._get_test_app
+
+    @pytest.fixture
+    def app(make_app):
+        """Returns a client app instance to use in functional tests
+
+        To use it, just add the ``app`` parameter to your test function signature::
+
+            def test_dataset_search(self, app):
+
+                url = h.url_for('dataset.search')
+
+                response = app.get(url)
+
+
+        """
+        return make_app()
+
+    @pytest.fixture(scope=u"session")
+    def reset_db():
+        """Callable for resetting the database to the initial state.
+
+        If possible use the ``clean_db`` fixture instead.
+
+        """
+        return test_helpers.reset_db
+
+    @pytest.fixture(scope=u"session")
+    def reset_index():
+        """Callable for cleaning search index.
+
+        If possible use the ``clean_index`` fixture instead.
+        """
+        return search.clear_all
+
+    @pytest.fixture
+    def clean_db(reset_db):
+        """Resets the database to the initial state.
+
+        This can be used either for all tests in a class::
+
+            @pytest.mark.usefixtures("clean_db")
+            class TestExample(object):
+
+                def test_example(self):
+
+        or for a single test::
+
+            class TestExample(object):
+
+                @pytest.mark.usefixtures("clean_db")
+                def test_example(self):
+
+        """
+        reset_db()
+        showcase_setup()
+
+    @pytest.fixture
+    def clean_index(reset_index):
+        """Clear search index before starting the test.
+        """
+        reset_index()
diff --git a/ckanext/showcase/tests/test_auth.py b/ckanext/showcase/tests/test_auth.py
index 86a23470..bfa4cc83 100644
--- a/ckanext/showcase/tests/test_auth.py
+++ b/ckanext/showcase/tests/test_auth.py
@@ -1,711 +1,818 @@
+import pytest
 import json
-from nose import tools as nosetools
 
 import ckan.plugins.toolkit as toolkit
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
 
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
+from ckan.tests import factories, helpers
 
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
 
+def _get_request(app, url, status):
+    ''' Wrapper around app.get() for compatibility between CKAN versions.
 
-class TestShowcaseAuthIndex(ShowcaseFunctionalTestBase):
+    CKAN 2.9's app.get() forces a redirect and 2.8's doesn't. Also
+    follow_redirects parameter is not supported in CKAN 2.8.
 
-    def test_auth_anon_user_can_view_showcase_index(self):
-        '''An anon (not logged in) user can view the Showcases index.'''
-        app = self._get_test_app()
+    Can be removed when CKAN 2.8 is no longer supported.
+    '''
+    try:
+        # CKAN 2.9
+        app.get(url, status=status, follow_redirects=False)
+    except TypeError:
+        # CKAN 2.8
+        app.get(url, status=status)
+
+
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAuthIndex(object):
+    def test_auth_anon_user_can_view_showcase_index(self, app):
+        """An anon (not logged in) user can view the Showcases index."""
 
         app.get("/showcase", status=200)
 
-    def test_auth_logged_in_user_can_view_showcase_index(self):
-        '''
+    def test_auth_logged_in_user_can_view_showcase_index(self, app):
+        """
         A logged in user can view the Showcase index.
-        '''
-        app = self._get_test_app()
+        """
 
         user = factories.User()
 
-        app.get("/showcase", status=200,
-                extra_environ={'REMOTE_USER': str(user["name"])})
+        app.get(
+            "/showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_anon_user_cant_see_add_showcase_button(self):
-        '''
+    def test_auth_anon_user_cant_see_add_showcase_button(self, app):
+        """
         An anon (not logged in) user can't see the Add Showcase button on the
         showcase index page.
-        '''
-        app = self._get_test_app()
+        """
 
         response = app.get("/showcase", status=200)
 
         # test for new showcase link in response
-        response.mustcontain(no="/showcase/new")
+        assert "/showcase/new" not in response.body
 
-    def test_auth_logged_in_user_cant_see_add_showcase_button(self):
-        '''
+    def test_auth_logged_in_user_cant_see_add_showcase_button(self, app):
+        """
         A logged in user can't see the Add Showcase button on the showcase
         index page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
-        response = app.get("/showcase", status=200,
-                           extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         # test for new showcase link in response
-        response.mustcontain(no="/showcase/new")
+        assert "/showcase/new" not in response.body
 
-    def test_auth_sysadmin_can_see_add_showcase_button(self):
-        '''
+    def test_auth_sysadmin_can_see_add_showcase_button(self, app):
+        """
         A sysadmin can see the Add Showcase button on the showcase index
         page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.Sysadmin()
 
-        response = app.get("/showcase", status=200,
-                           extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         # test for new showcase link in response
-        response.mustcontain("/showcase/new")
+        assert "/showcase/new" in response.body
 
 
-class TestShowcaseAuthDetails(ShowcaseFunctionalTestBase):
-    def test_auth_anon_user_can_view_showcase_details(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAuthDetails(object):
+    def test_auth_anon_user_can_view_showcase_details(self, app):
+        """
         An anon (not logged in) user can view an individual Showcase details page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/my-showcase', status=200)
+        app.get("/showcase/my-showcase", status=200)
 
-    def test_auth_logged_in_user_can_view_showcase_details(self):
-        '''
+    def test_auth_logged_in_user_can_view_showcase_details(self, app):
+        """
         A logged in user can view an individual Showcase details page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_anon_user_cant_see_manage_button(self):
-        '''
+    def test_auth_anon_user_cant_see_manage_button(self, app):
+        """
         An anon (not logged in) user can't see the Manage button on an individual
         showcase details page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/showcase/my-showcase', status=200)
+        response = app.get("/showcase/my-showcase", status=200)
 
-        # test for url to edit page
-        response.mustcontain(no="/showcase/edit/my-showcase")
+        assert "/showcase/edit/my-showcase" not in response.body
 
-    def test_auth_logged_in_user_can_see_manage_button(self):
-        '''
+    def test_auth_logged_in_user_can_see_manage_button(self, app):
+        """
         A logged in user can't see the Manage button on an individual showcase
         details page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/showcase/my-showcase', status=200,
-                           extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/showcase/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         # test for url to edit page
-        response.mustcontain(no="/showcase/edit/my-showcase")
+        assert "/showcase/edit/my-showcase" not in response.body
 
-    def test_auth_sysadmin_can_see_manage_button(self):
-        '''
+    def test_auth_sysadmin_can_see_manage_button(self, app):
+        """
         A sysadmin can see the Manage button on an individual showcase details
         page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.Sysadmin()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/showcase/my-showcase', status=200,
-                           extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/showcase/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         # test for url to edit page
-        response.mustcontain("/showcase/edit/my-showcase")
+        assert "/showcase/edit/my-showcase" in response.body
 
-    def test_auth_showcase_show_anon_can_access(self):
-        '''
+    def test_auth_showcase_show_anon_can_access(self, app):
+        """
         Anon user can request showcase show.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/api/3/action/ckanext_showcase_show?id=my-showcase',
-                           status=200)
+        response = app.get(
+            "/api/3/action/ckanext_showcase_show?id=my-showcase", status=200
+        )
 
         json_response = json.loads(response.body)
 
-        nosetools.assert_true(json_response['success'])
+        assert json_response["success"]
 
-    def test_auth_showcase_show_normal_user_can_access(self):
-        '''
+    def test_auth_showcase_show_normal_user_can_access(self, app):
+        """
         Normal logged in user can request showcase show.
-        '''
+        """
         user = factories.User()
-        app = self._get_test_app()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/api/3/action/ckanext_showcase_show?id=my-showcase',
-                           status=200, extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/api/3/action/ckanext_showcase_show?id=my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         json_response = json.loads(response.body)
 
-        nosetools.assert_true(json_response['success'])
+        assert json_response["success"]
 
-    def test_auth_showcase_show_sysadmin_can_access(self):
-        '''
+    def test_auth_showcase_show_sysadmin_can_access(self, app):
+        """
         Normal logged in user can request showcase show.
-        '''
+        """
         user = factories.Sysadmin()
-        app = self._get_test_app()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/api/3/action/ckanext_showcase_show?id=my-showcase',
-                           status=200, extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/api/3/action/ckanext_showcase_show?id=my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         json_response = json.loads(response.body)
 
-        nosetools.assert_true(json_response['success'])
-
+        assert json_response["success"]
 
-class TestShowcaseAuthCreate(ShowcaseFunctionalTestBase):
 
-    def test_auth_anon_user_cant_view_create_showcase(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAuthCreate(object):
+    def test_auth_anon_user_cant_view_create_showcase(self, app):
+        """
         An anon (not logged in) user can't access the create showcase page.
-        '''
-        app = self._get_test_app()
-        app.get("/showcase/new", status=302)
+        """
+        _get_request(app, "/showcase/new", status=302)
 
-    def test_auth_logged_in_user_cant_view_create_showcase_page(self):
-        '''
+    def test_auth_logged_in_user_cant_view_create_showcase_page(self, app):
+        """
         A logged in user can't access the create showcase page.
-        '''
-        app = self._get_test_app()
+        """
         user = factories.User()
-        app.get("/showcase/new", status=401,
-                extra_environ={'REMOTE_USER': str(user['name'])})
-
-    def test_auth_sysadmin_can_view_create_showcase_page(self):
-        '''
+        app.get(
+            "/showcase/new",
+            status=401,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
+
+    def test_auth_sysadmin_can_view_create_showcase_page(self, app):
+        """
         A sysadmin can access the create showcase page.
-        '''
-        app = self._get_test_app()
+        """
         user = factories.Sysadmin()
-        app.get("/showcase/new", status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
-
+        app.get(
+            "/showcase/new",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-class TestShowcaseAuthList(ShowcaseFunctionalTestBase):
 
-    def test_auth_showcase_list_anon_can_access(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAuthList(object):
+    def test_auth_showcase_list_anon_can_access(self, app):
+        """
         Anon user can request showcase list.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/api/3/action/ckanext_showcase_list',
-                           status=200)
+        response = app.get("/api/3/action/ckanext_showcase_list", status=200)
 
         json_response = json.loads(response.body)
 
-        nosetools.assert_true(json_response['success'])
+        assert json_response["success"]
 
-    def test_auth_showcase_list_normal_user_can_access(self):
-        '''
+    def test_auth_showcase_list_normal_user_can_access(self, app):
+        """
         Normal logged in user can request showcase list.
-        '''
+        """
         user = factories.User()
-        app = self._get_test_app()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/api/3/action/ckanext_showcase_list',
-                           status=200, extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/api/3/action/ckanext_showcase_list",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         json_response = json.loads(response.body)
 
-        nosetools.assert_true(json_response['success'])
+        assert json_response["success"]
 
-    def test_auth_showcase_list_sysadmin_can_access(self):
-        '''
+    def test_auth_showcase_list_sysadmin_can_access(self, app):
+        """
         Normal logged in user can request showcase list.
-        '''
+        """
         user = factories.Sysadmin()
-        app = self._get_test_app()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        response = app.get('/api/3/action/ckanext_showcase_list',
-                           status=200, extra_environ={'REMOTE_USER': str(user['name'])})
+        response = app.get(
+            "/api/3/action/ckanext_showcase_list",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
         json_response = json.loads(response.body)
 
-        nosetools.assert_true(json_response['success'])
-
+        assert json_response["success"]
 
-class TestShowcaseAuthEdit(ShowcaseFunctionalTestBase):
 
-    def test_auth_anon_user_cant_view_edit_showcase_page(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAuthEdit(object):
+    def test_auth_anon_user_cant_view_edit_showcase_page(self, app):
+        """
         An anon (not logged in) user can't access the showcase edit page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/edit/my-showcase', status=302)
+        _get_request(app, "/showcase/edit/my-showcase", status=302)
 
-    def test_auth_logged_in_user_cant_view_edit_showcase_page(self):
-        '''
+    def test_auth_logged_in_user_cant_view_edit_showcase_page(self, app):
+        """
         A logged in user can't access the showcase edit page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/edit/my-showcase', status=401,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/edit/my-showcase",
+            status=401,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_sysadmin_can_view_edit_showcase_page(self):
-        '''
+    def test_auth_sysadmin_can_view_edit_showcase_page(self, app):
+        """
         A sysadmin can access the showcase edit page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.Sysadmin()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/edit/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/edit/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_showcase_admin_can_view_edit_showcase_page(self):
-        '''
+    def test_auth_showcase_admin_can_view_edit_showcase_page(self, app):
+        """
         A showcase admin can access the showcase edit page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
         # Make user a showcase admin
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/edit/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/edit/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_anon_user_cant_view_manage_datasets(self):
-        '''
+    def test_auth_anon_user_cant_view_manage_datasets(self, app):
+        """
         An anon (not logged in) user can't access the showcase manage datasets page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/manage_datasets/my-showcase', status=302)
+        _get_request(app, "/showcase/manage_datasets/my-showcase", status=302)
 
-    def test_auth_logged_in_user_cant_view_manage_datasets(self):
-        '''
+    def test_auth_logged_in_user_cant_view_manage_datasets(self, app):
+        """
         A logged in user (not sysadmin) can't access the showcase manage datasets page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/manage_datasets/my-showcase', status=401,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/manage_datasets/my-showcase",
+            status=401,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_sysadmin_can_view_manage_datasets(self):
-        '''
+    def test_auth_sysadmin_can_view_manage_datasets(self, app):
+        """
         A sysadmin can access the showcase manage datasets page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.Sysadmin()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/manage_datasets/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/manage_datasets/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_showcase_admin_can_view_manage_datasets(self):
-        '''
+    def test_auth_showcase_admin_can_view_manage_datasets(self, app):
+        """
         A showcase admin can access the showcase manage datasets page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
         # Make user a showcase admin
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/manage_datasets/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/manage_datasets/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_anon_user_cant_view_delete_showcase_page(self):
-        '''
+    def test_auth_anon_user_cant_view_delete_showcase_page(self, app):
+        """
         An anon (not logged in) user can't access the showcase delete page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/delete/my-showcase', status=302)
+        _get_request(app, "/showcase/delete/my-showcase", status=302)
 
-    def test_auth_logged_in_user_cant_view_delete_showcase_page(self):
-        '''
+    def test_auth_logged_in_user_cant_view_delete_showcase_page(self, app):
+        """
         A logged in user can't access the showcase delete page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/delete/my-showcase', status=401,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/delete/my-showcase",
+            status=401,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_sysadmin_can_view_delete_showcase_page(self):
-        '''
+    def test_auth_sysadmin_can_view_delete_showcase_page(self, app):
+        """
         A sysadmin can access the showcase delete page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.Sysadmin()
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
-        app.get('/showcase/delete/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/delete/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-    def test_auth_showcase_admin_can_view_delete_showcase_page(self):
-        '''
+    def test_auth_showcase_admin_can_view_delete_showcase_page(self, app):
+        """
         A showcase admin can access the showcase delete page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
         # Make user a showcase admin
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
-
-        factories.Dataset(type='showcase', name='my-showcase')
-
-        app.get('/showcase/delete/my-showcase', status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
-
-    def test_auth_anon_user_cant_view_addtoshowcase_dropdown_dataset_showcase_list(self):
-        '''
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
+
+        factories.Dataset(type="showcase", name="my-showcase")
+
+        app.get(
+            "/showcase/delete/my-showcase",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
+
+    def test_auth_anon_user_cant_view_addtoshowcase_dropdown_dataset_showcase_list(
+        self, app
+    ):
+        """
         An anonymous user can't view the 'Add to showcase' dropdown selector
         from a datasets showcase list page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(name='my-showcase', type='showcase')
-        factories.Dataset(name='my-dataset')
+        factories.Dataset(name="my-showcase", type="showcase")
+        factories.Dataset(name="my-dataset")
 
-        showcase_list_response = app.get('/dataset/showcases/my-dataset', status=200)
+        showcase_list_response = app.get(
+            "/dataset/showcases/my-dataset", status=200
+        )
 
-        nosetools.assert_false('showcase-add' in showcase_list_response.forms)
+        assert "showcase-add" not in showcase_list_response.body
 
-    def test_auth_normal_user_cant_view_addtoshowcase_dropdown_dataset_showcase_list(self):
-        '''
+    def test_auth_normal_user_cant_view_addtoshowcase_dropdown_dataset_showcase_list(
+        self, app
+    ):
+        """
         A normal (logged in) user can't view the 'Add to showcase' dropdown
         selector from a datasets showcase list page.
-        '''
+        """
         user = factories.User()
-        app = self._get_test_app()
 
-        factories.Dataset(name='my-showcase', type='showcase')
-        factories.Dataset(name='my-dataset')
+        factories.Dataset(name="my-showcase", type="showcase")
+        factories.Dataset(name="my-dataset")
 
-        showcase_list_response = app.get('/dataset/showcases/my-dataset', status=200,
-                                         extra_environ={'REMOTE_USER': str(user['name'])})
+        showcase_list_response = app.get(
+            "/dataset/showcases/my-dataset",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-        nosetools.assert_false('showcase-add' in showcase_list_response.forms)
+        assert "showcase-add" not in showcase_list_response.body
 
-    def test_auth_sysadmin_can_view_addtoshowcase_dropdown_dataset_showcase_list(self):
-        '''
+    def test_auth_sysadmin_can_view_addtoshowcase_dropdown_dataset_showcase_list(
+        self, app
+    ):
+        """
         A sysadmin can view the 'Add to showcase' dropdown selector from a
         datasets showcase list page.
-        '''
+        """
         user = factories.Sysadmin()
-        app = self._get_test_app()
 
-        factories.Dataset(name='my-showcase', type='showcase')
-        factories.Dataset(name='my-dataset')
+        factories.Dataset(name="my-showcase", type="showcase")
+        factories.Dataset(name="my-dataset")
 
-        showcase_list_response = app.get('/dataset/showcases/my-dataset', status=200,
-                                         extra_environ={'REMOTE_USER': str(user['name'])})
+        showcase_list_response = app.get(
+            "/dataset/showcases/my-dataset",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-        nosetools.assert_true('showcase-add' in showcase_list_response.forms)
+        assert "showcase-add" in showcase_list_response.body
 
-    def test_auth_showcase_admin_can_view_addtoshowcase_dropdown_dataset_showcase_list(self):
-        '''
+    def test_auth_showcase_admin_can_view_addtoshowcase_dropdown_dataset_showcase_list(
+        self, app
+    ):
+        """
         A showcase admin can view the 'Add to showcase' dropdown selector from
         a datasets showcase list page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.User()
 
         # Make user a showcase admin
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user["name"]
+        )
 
-        factories.Dataset(name='my-showcase', type='showcase')
-        factories.Dataset(name='my-dataset')
+        factories.Dataset(name="my-showcase", type="showcase")
+        factories.Dataset(name="my-dataset")
 
-        showcase_list_response = app.get('/dataset/showcases/my-dataset', status=200,
-                                         extra_environ={'REMOTE_USER': str(user['name'])})
+        showcase_list_response = app.get(
+            "/dataset/showcases/my-dataset",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
 
-        nosetools.assert_true('showcase-add' in showcase_list_response.forms)
+        assert "showcase-add" in showcase_list_response.body
 
 
-class TestShowcasePackageAssociationCreate(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcasePackageAssociationCreate(object):
     def test_showcase_package_association_create_no_user(self):
-        '''
+        """
         Calling showcase package association create with no user raises
         NotAuthorized.
-        '''
+        """
 
-        context = {'user': None, 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_package_association_create',
-                                context=context)
+        context = {"user": None, "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_package_association_create", context=context,
+            )
 
     def test_showcase_package_association_create_sysadmin(self):
-        '''
+        """
         Calling showcase package association create by a sysadmin doesn't
         raise NotAuthorized.
-        '''
+        """
         a_sysadmin = factories.Sysadmin()
-        context = {'user': a_sysadmin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_package_association_create',
-                          context=context)
+        context = {"user": a_sysadmin["name"], "model": None}
+        helpers.call_auth(
+            "ckanext_showcase_package_association_create", context=context
+        )
 
     def test_showcase_package_association_create_showcase_admin(self):
-        '''
+        """
         Calling showcase package association create by a showcase admin
         doesn't raise NotAuthorized.
-        '''
+        """
         showcase_admin = factories.User()
 
         # Make user a showcase admin
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=showcase_admin['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=showcase_admin["name"],
+        )
 
-        context = {'user': showcase_admin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_package_association_create',
-                          context=context)
+        context = {"user": showcase_admin["name"], "model": None}
+        helpers.call_auth(
+            "ckanext_showcase_package_association_create", context=context
+        )
 
     def test_showcase_package_association_create_unauthorized_creds(self):
-        '''
+        """
         Calling showcase package association create with unauthorized user
         raises NotAuthorized.
-        '''
+        """
         not_a_sysadmin = factories.User()
-        context = {'user': not_a_sysadmin['name'], 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_package_association_create',
-                                context=context)
-
+        context = {"user": not_a_sysadmin["name"], "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_package_association_create", context=context,
+            )
 
-class TestShowcasePackageAssociationDelete(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcasePackageAssociationDelete(object):
     def test_showcase_package_association_delete_no_user(self):
-        '''
+        """
         Calling showcase package association create with no user raises
         NotAuthorized.
-        '''
+        """
 
-        context = {'user': None, 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_package_association_delete',
-                                context=context)
+        context = {"user": None, "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_package_association_delete", context=context,
+            )
 
     def test_showcase_package_association_delete_sysadmin(self):
-        '''
+        """
         Calling showcase package association create by a sysadmin doesn't
         raise NotAuthorized.
-        '''
+        """
         a_sysadmin = factories.Sysadmin()
-        context = {'user': a_sysadmin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_package_association_delete',
-                          context=context)
+        context = {"user": a_sysadmin["name"], "model": None}
+        helpers.call_auth(
+            "ckanext_showcase_package_association_delete", context=context
+        )
 
     def test_showcase_package_association_delete_showcase_admin(self):
-        '''
+        """
         Calling showcase package association create by a showcase admin
         doesn't raise NotAuthorized.
-        '''
+        """
         showcase_admin = factories.User()
 
         # Make user a showcase admin
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=showcase_admin['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add",
+            context={},
+            username=showcase_admin["name"],
+        )
 
-        context = {'user': showcase_admin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_package_association_delete',
-                          context=context)
+        context = {"user": showcase_admin["name"], "model": None}
+        helpers.call_auth(
+            "ckanext_showcase_package_association_delete", context=context
+        )
 
     def test_showcase_package_association_delete_unauthorized_creds(self):
-        '''
+        """
         Calling showcase package association create with unauthorized user
         raises NotAuthorized.
-        '''
+        """
         not_a_sysadmin = factories.User()
-        context = {'user': not_a_sysadmin['name'], 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_package_association_delete',
-                                context=context)
+        context = {"user": not_a_sysadmin["name"], "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_package_association_delete", context=context,
+            )
 
 
-class TestShowcaseAdminAddAuth(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAdminAddAuth(object):
     def test_showcase_admin_add_no_user(self):
-        '''
+        """
         Calling showcase admin add with no user raises NotAuthorized.
-        '''
+        """
 
-        context = {'user': None, 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_admin_add', context=context)
+        context = {"user": None, "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_admin_add", context=context,
+            )
 
     def test_showcase_admin_add_correct_creds(self):
-        '''
+        """
         Calling showcase admin add by a sysadmin doesn't raise
         NotAuthorized.
-        '''
+        """
         a_sysadmin = factories.Sysadmin()
-        context = {'user': a_sysadmin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_admin_add', context=context)
+        context = {"user": a_sysadmin["name"], "model": None}
+        helpers.call_auth("ckanext_showcase_admin_add", context=context)
 
     def test_showcase_admin_add_unauthorized_creds(self):
-        '''
+        """
         Calling showcase admin add with unauthorized user raises
         NotAuthorized.
-        '''
+        """
         not_a_sysadmin = factories.User()
-        context = {'user': not_a_sysadmin['name'], 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_admin_add', context=context)
-
+        context = {"user": not_a_sysadmin["name"], "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_admin_add", context=context,
+            )
 
-class TestShowcaseAdminRemoveAuth(ShowcaseFunctionalTestBase):
 
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAdminRemoveAuth(object):
     def test_showcase_admin_remove_no_user(self):
-        '''
+        """
         Calling showcase admin remove with no user raises NotAuthorized.
-        '''
+        """
 
-        context = {'user': None, 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_admin_remove', context=context)
+        context = {"user": None, "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_admin_remove", context=context,
+            )
 
     def test_showcase_admin_remove_correct_creds(self):
-        '''
+        """
         Calling showcase admin remove by a sysadmin doesn't raise
         NotAuthorized.
-        '''
+        """
         a_sysadmin = factories.Sysadmin()
-        context = {'user': a_sysadmin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_admin_remove', context=context)
+        context = {"user": a_sysadmin["name"], "model": None}
+        helpers.call_auth("ckanext_showcase_admin_remove", context=context)
 
     def test_showcase_admin_remove_unauthorized_creds(self):
-        '''
+        """
         Calling showcase admin remove with unauthorized user raises
         NotAuthorized.
-        '''
+        """
         not_a_sysadmin = factories.User()
-        context = {'user': not_a_sysadmin['name'], 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_admin_remove', context=context)
+        context = {"user": not_a_sysadmin["name"], "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_admin_remove", context=context,
+            )
 
 
-class TestShowcaseAdminListAuth(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAdminListAuth(object):
     def test_showcase_admin_list_no_user(self):
-        '''
+        """
         Calling showcase admin list with no user raises NotAuthorized.
-        '''
+        """
 
-        context = {'user': None, 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_admin_list', context=context)
+        context = {"user": None, "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_admin_list", context=context,
+            )
 
     def test_showcase_admin_list_correct_creds(self):
-        '''
+        """
         Calling showcase admin list by a sysadmin doesn't raise
         NotAuthorized.
-        '''
+        """
         a_sysadmin = factories.Sysadmin()
-        context = {'user': a_sysadmin['name'], 'model': None}
-        helpers.call_auth('ckanext_showcase_admin_list', context=context)
+        context = {"user": a_sysadmin["name"], "model": None}
+        helpers.call_auth("ckanext_showcase_admin_list", context=context)
 
     def test_showcase_admin_list_unauthorized_creds(self):
-        '''
+        """
         Calling showcase admin list with unauthorized user raises
         NotAuthorized.
-        '''
+        """
         not_a_sysadmin = factories.User()
-        context = {'user': not_a_sysadmin['name'], 'model': None}
-        nosetools.assert_raises(toolkit.NotAuthorized, helpers.call_auth,
-                                'ckanext_showcase_admin_list', context=context)
-
+        context = {"user": not_a_sysadmin["name"], "model": None}
+        with pytest.raises(toolkit.NotAuthorized):
+            helpers.call_auth(
+                "ckanext_showcase_admin_list", context=context,
+            )
 
-class TestShowcaseAuthManageShowcaseAdmins(ShowcaseFunctionalTestBase):
 
-    def test_auth_anon_user_cant_view_showcase_admin_manage_page(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseAuthManageShowcaseAdmins(object):
+    def test_auth_anon_user_cant_view_showcase_admin_manage_page(self, app):
+        """
         An anon (not logged in) user can't access the manage showcase admin
         page.
-        '''
-        app = self._get_test_app()
-        app.get("/showcase/new", status=302)
+        """
 
-    def test_auth_logged_in_user_cant_view_showcase_admin_manage_page(self):
-        '''
+        _get_request(app, "/showcase/new", status=302)
+
+    def test_auth_logged_in_user_cant_view_showcase_admin_manage_page(
+        self, app
+    ):
+        """
         A logged in user can't access the manage showcase admin page.
-        '''
-        app = self._get_test_app()
-        user = factories.User()
-        app.get("/showcase/new", status=401,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        """
 
-    def test_auth_sysadmin_can_view_showcase_admin_manage_page(self):
-        '''
+        user = factories.User()
+        app.get(
+            "/showcase/new",
+            status=401,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
+
+    def test_auth_sysadmin_can_view_showcase_admin_manage_page(self, app):
+        """
         A sysadmin can access the manage showcase admin page.
-        '''
-        app = self._get_test_app()
+        """
+
         user = factories.Sysadmin()
-        app.get("/showcase/new", status=200,
-                extra_environ={'REMOTE_USER': str(user['name'])})
+        app.get(
+            "/showcase/new",
+            status=200,
+            extra_environ={"REMOTE_USER": str(user["name"])},
+        )
diff --git a/ckanext/showcase/tests/test_cli.py b/ckanext/showcase/tests/test_cli.py
deleted file mode 100644
index bd56da6e..00000000
--- a/ckanext/showcase/tests/test_cli.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import ckan.tests.helpers as helpers
-
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
-
-from ckan.lib.helpers import render_markdown
-from ckanext.showcase.commands.migrate import MigrationCommand
-
-
-class TestMigrationCommand(object):
-    '''Tests for MigrationCommand.markdown_to_html'''
-
-    @classmethod
-    def setup_class(cls):
-        cls.migration_cmd = MigrationCommand('migration-command')
-
-    def setup(self):
-        helpers.reset_db()
-
-    def test_notes_are_migrated_from_markdown_to_html(self):
-        showcase1 = factories.Dataset(
-            type='showcase',
-            name='my-showcase',
-            notes='# Title')
-
-        showcase2 = factories.Dataset(
-            type='showcase',
-            name='my-showcase-2',
-            notes='# Title 2')
-
-        self.migration_cmd.args = ['markdown_to_html']
-        self.migration_cmd.markdown_to_html()
-
-        migrated_showcase1 = helpers.call_action(
-            'package_show',
-            context={'ignore_auth': True},
-            id=showcase1['id']
-        )
-        assert(
-            migrated_showcase1['notes'] == render_markdown(showcase1['notes'])
-            )
-
-        migrated_showcase2 = helpers.call_action(
-            'package_show',
-            context={'ignore_auth': True},
-            id=showcase2['id']
-        )
-        assert(
-            migrated_showcase2['notes'] == render_markdown(showcase2['notes'])
-            )
diff --git a/ckanext/showcase/tests/test_converters.py b/ckanext/showcase/tests/test_converters.py
index 5208e3a9..acec18e0 100644
--- a/ckanext/showcase/tests/test_converters.py
+++ b/ckanext/showcase/tests/test_converters.py
@@ -1,69 +1,68 @@
-from nose import tools as nosetools
+import pytest
 
 import ckan.model as model
 import ckan.plugins.toolkit as toolkit
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
 
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
+from ckan.tests import factories
 
-from ckanext.showcase.logic.converters import convert_package_name_or_id_to_title_or_name
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
+from ckanext.showcase.logic.converters import (
+    convert_package_name_or_id_to_title_or_name,
+)
 
 
-class TestNameOrIdToTitleConverter(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestNameOrIdToTitleConverter(object):
     def test_name_to_title(self):
-        '''
+        """
         Package correctly returns title.
-        '''
-        context = {'session': model.Session}
-        factories.Dataset(id='my-id', title='My Title', name='my-name')
+        """
+        context = {"session": model.Session}
+        factories.Dataset(id="my-id", title="My Title", name="my-name")
 
-        result = convert_package_name_or_id_to_title_or_name('my-name', context)
-        nosetools.assert_equals('My Title', result)
+        result = convert_package_name_or_id_to_title_or_name(
+            "my-name", context
+        )
+        assert "My Title" == result
 
     def test_name_to_name(self):
-        '''
+        """
         Package with no title correctly returns name.
-        '''
-        context = {'session': model.Session}
-        factories.Dataset(id='my-id', title='', name='my-name')
+        """
+        context = {"session": model.Session}
+        factories.Dataset(id="my-id", title="", name="my-name")
 
-        result = convert_package_name_or_id_to_title_or_name('my-name', context)
-        nosetools.assert_equals('my-name', result)
+        result = convert_package_name_or_id_to_title_or_name(
+            "my-name", context
+        )
+        assert "my-name" == result
 
     def test_id_to_title(self):
-        '''
+        """
         Package correctly returns title.
-        '''
-        context = {'session': model.Session}
-        factories.Dataset(id='my-id', title='My Title', name='my-name')
+        """
+        context = {"session": model.Session}
+        factories.Dataset(id="my-id", title="My Title", name="my-name")
 
-        result = convert_package_name_or_id_to_title_or_name('my-id', context)
-        nosetools.assert_equals('My Title', result)
+        result = convert_package_name_or_id_to_title_or_name("my-id", context)
+        assert "My Title" == result
 
     def test_id_to_name(self):
-        '''
+        """
         Package with no title correctly returns name.
-        '''
-        context = {'session': model.Session}
-        factories.Dataset(id='my-id', title='', name='my-name')
+        """
+        context = {"session": model.Session}
+        factories.Dataset(id="my-id", title="", name="my-name")
 
-        result = convert_package_name_or_id_to_title_or_name('my-id', context)
-        nosetools.assert_equals('my-name', result)
+        result = convert_package_name_or_id_to_title_or_name("my-id", context)
+        assert "my-name" == result
 
     def test_with_no_package_id_exists(self):
-        '''
+        """
         No package with id exists. Raises error.
-        '''
-        context = {'session': model.Session}
+        """
+        context = {"session": model.Session}
 
-        nosetools.assert_raises(toolkit.Invalid, convert_package_name_or_id_to_title_or_name,
-                                'my-non-existent-id',
-                                context=context)
+        with pytest.raises(toolkit.Invalid):
+            convert_package_name_or_id_to_title_or_name(
+                "my-non-existent-id", context=context,
+            )
diff --git a/ckanext/showcase/tests/test_helpers.py b/ckanext/showcase/tests/test_helpers.py
index 0c057816..98a41917 100644
--- a/ckanext/showcase/tests/test_helpers.py
+++ b/ckanext/showcase/tests/test_helpers.py
@@ -1,78 +1,72 @@
-from nose import tools as nosetools
-from nose import SkipTest
+import pytest
 
 from ckan.plugins import toolkit as tk
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
 
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
+from ckan.tests import factories
 
 import ckanext.showcase.logic.helpers as showcase_helpers
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
 
 
-class TestGetSiteStatistics(ShowcaseFunctionalTestBase):
-
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestGetSiteStatistics(object):
     def test_dataset_count_no_datasets(self):
-        '''
+        """
         Dataset and showcase count is 0 when no datasets, and no showcases.
-        '''
-        if not tk.check_ckan_version(min_version='2.5'):
-            raise SkipTest('get_site_statistics without user broken in CKAN 2.4')
+        """
+        if not tk.check_ckan_version(min_version="2.5"):
+            pytest.skip(
+                reason="get_site_statistics without user broken in CKAN 2.4"
+            )
         stats = showcase_helpers.get_site_statistics()
-        nosetools.assert_equal(stats['dataset_count'], 0)
-        nosetools.assert_equal(stats['showcase_count'], 0)
+        assert stats["dataset_count"] == 0
+        assert stats["showcase_count"] == 0
 
     def test_dataset_count_no_datasets_some_showcases(self):
-        '''
+        """
         Dataset and showcase count is 0 when no datasets, but some showcases.
-        '''
-        if not tk.check_ckan_version(min_version='2.5'):
-            raise SkipTest('get_site_statistics without user broken in CKAN 2.4')
-        for i in xrange(0, 10):
-            factories.Dataset(type='showcase')
+        """
+        if not tk.check_ckan_version(min_version="2.5"):
+            pytest.skip(
+                reason="get_site_statistics without user broken in CKAN 2.4"
+            )
+        for i in range(0, 10):
+            factories.Dataset(type="showcase")
 
         stats = showcase_helpers.get_site_statistics()
-        nosetools.assert_equal(stats['dataset_count'], 0)
-        nosetools.assert_equal(stats['showcase_count'], 10)
+        assert stats["dataset_count"] == 0
+        assert stats["showcase_count"] == 10
 
     def test_dataset_count_some_datasets_no_showcases(self):
-        '''
+        """
         Dataset and showcase count is correct when there are datasets, but no
         showcases.
-        '''
-        if not tk.check_ckan_version(min_version='2.5'):
-            raise SkipTest('get_site_statistics without user broken in CKAN 2.4')
-        for i in xrange(0, 10):
+        """
+        if not tk.check_ckan_version(min_version="2.5"):
+            pytest.skip(
+                reason="get_site_statistics without user broken in CKAN 2.4"
+            )
+        for i in range(0, 10):
             factories.Dataset()
 
         stats = showcase_helpers.get_site_statistics()
-        nosetools.assert_equal(stats['dataset_count'], 10)
-        nosetools.assert_equal(stats['showcase_count'], 0)
+        assert stats["dataset_count"] == 10
+        assert stats["showcase_count"] == 0
 
     def test_dataset_count_some_datasets_some_showcases(self):
-        '''
+        """
         Dataset and showcase count is correct when there are datasets and some
         showcases.
-        '''
-        if not tk.check_ckan_version(min_version='2.5'):
-            raise SkipTest('get_site_statistics without user broken in CKAN 2.4')
-        for i in xrange(0, 10):
+        """
+        if not tk.check_ckan_version(min_version="2.5"):
+            pytest.skip(
+                reason="get_site_statistics without user broken in CKAN 2.4"
+            )
+        for i in range(0, 10):
             factories.Dataset()
 
-        for i in xrange(0, 5):
-            factories.Dataset(type='showcase')
+        for i in range(0, 5):
+            factories.Dataset(type="showcase")
 
         stats = showcase_helpers.get_site_statistics()
-        nosetools.assert_equal(stats['dataset_count'], 10)
-        nosetools.assert_equal(stats['showcase_count'], 5)
-
-
-@helpers.change_config('ckanext.showcase.editor', 'custom-editor')
-def test_get_wysiwyg_editor():
-    nosetools.assert_equals(showcase_helpers.get_wysiwyg_editor(), 'custom-editor')
+        assert stats["dataset_count"] == 10
+        assert stats["showcase_count"] == 5
diff --git a/ckanext/showcase/tests/test_plugin.py b/ckanext/showcase/tests/test_plugin.py
index 0ca5284e..495ec5e7 100644
--- a/ckanext/showcase/tests/test_plugin.py
+++ b/ckanext/showcase/tests/test_plugin.py
@@ -1,428 +1,435 @@
+import pytest
+from bs4 import BeautifulSoup
+
 from ckan.lib.helpers import url_for
-from nose import tools as nosetools
-from nose import SkipTest
+
 
 from ckan.plugins import toolkit as tk
 import ckan.model as model
-try:
-    import ckan.tests.factories as factories
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.factories as factories
 
-try:
-    import ckan.tests.helpers as helpers
-except ImportError:  # for ckan <= 2.3
-    import ckan.new_tests.helpers as helpers
+from ckan.tests import factories, helpers
 
 from ckanext.showcase.model import ShowcasePackageAssociation
-from ckanext.showcase.tests import ShowcaseFunctionalTestBase
 
 import logging
-log = logging.getLogger(__name__)
-
-submit_and_follow = helpers.submit_and_follow
-
 
-class TestShowcaseIndex(ShowcaseFunctionalTestBase):
-
-    def test_showcases_redirects_to_showcase(self):
-        '''/showcases redirects to /showcase.'''
-        app = self._get_test_app()
-        response = app.get('/showcases', status=302)
-        nosetools.assert_equal(response.location, 'http://localhost/showcase')
-
-    def test_showcases_redirects_to_showcase_for_item(self):
-        '''/showcases/ redirects to /showcase.'''
-        app = self._get_test_app()
-
-        factories.Dataset(type='showcase', name='my-showcase')
+log = logging.getLogger(__name__)
 
-        response = app.get('/showcases/my-showcase', status=302)
-        nosetools.assert_equal(response.location, 'http://localhost/showcase/my-showcase')
 
-    def test_showcase_listed_on_index(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestShowcaseIndex(object):
+    def test_showcase_listed_on_index(self, app):
+        """
         An added Showcase will appear on the Showcase index page.
-        '''
-        app = self._get_test_app()
+        """
 
-        factories.Dataset(type='showcase', name='my-showcase')
+        factories.Dataset(type="showcase", name="my-showcase")
 
         response = app.get("/showcase", status=200)
-        response.mustcontain("1 showcase found")
-        response.mustcontain("my-showcase")
+        assert "1 showcase found" in response.body
+        assert "my-showcase" in response.body
 
 
-class TestShowcaseNewView(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db")
+class TestShowcaseNewView(object):
+    def test_showcase_create_form_renders(self, app):
 
-    def test_showcase_create_form_renders(self):
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
-        response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController', action='new'),
-            extra_environ=env,
-        )
-        nosetools.assert_true('dataset-edit' in response.forms)
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
+        response = app.get(url=url_for("showcase_new"), extra_environ=env,)
+        assert "dataset-edit" in response
+
+    def test_showcase_new_redirects_to_manage_datasets(self, app):
+        """Creating a new showcase redirects to the manage datasets form."""
+        if tk.check_ckan_version("2.9"):
+            pytest.skip("submit_and_follow not supported")
 
-    def test_showcase_new_redirects_to_manage_datasets(self):
-        '''Creating a new showcase redirects to the manage datasets form.'''
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
         # need a dataset for the 'bulk_action.showcase_add' button to show
         factories.Dataset()
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
-        response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController', action='new'),
-            extra_environ=env,
-        )
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
+        response = app.get(url=url_for("showcase_new"), extra_environ=env,)
 
         # create showcase
-        form = response.forms['dataset-edit']
-        form['name'] = u'my-showcase'
-        create_response = submit_and_follow(app, form, env, 'save')
+        form = response.forms["dataset-edit"]
+        form["name"] = u"my-showcase"
+        create_response = helpers.submit_and_follow(app, form, env, "save")
 
         # Unique to manage_datasets page
-        nosetools.assert_true('bulk_action.showcase_add' in create_response)
+        assert "bulk_action.showcase_add" in create_response
         # Requested page is the manage_datasets url.
-        nosetools.assert_equal(url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                                       action='manage_datasets', id='my-showcase'), create_response.request.path)
-
+        assert (
+            url_for("showcase_manage_datasets", id="my-showcase")
+            == create_response.request.path
+        )
 
-class TestShowcaseEditView(ShowcaseFunctionalTestBase):
 
-    def test_showcase_edit_form_renders(self):
-        '''
+@pytest.mark.usefixtures("clean_db")
+class TestShowcaseEditView(object):
+    def test_showcase_edit_form_renders(self, app):
+        """
         Edit form renders in response for ShowcaseController edit action.
-        '''
-        app = self._get_test_app()
+        """
+
         sysadmin = factories.Sysadmin()
-        factories.Dataset(name='my-showcase', type='showcase')
+        factories.Dataset(name="my-showcase", type="showcase")
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='edit',
-                        id='my-showcase'),
-            extra_environ=env,
+            url=url_for("showcase_edit", id="my-showcase"), extra_environ=env,
         )
-        nosetools.assert_true('dataset-edit' in response.forms)
+        assert "dataset-edit" in response
+
+    def test_showcase_edit_redirects_to_showcase_details(self, app):
+        """Editing a showcase redirects to the showcase details page."""
+        if tk.check_ckan_version("2.9"):
+            pytest.skip("submit_and_follow not supported")
 
-    def test_showcase_edit_redirects_to_showcase_details(self):
-        '''Editing a showcase redirects to the showcase details page.'''
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
-        factories.Dataset(name='my-showcase', type='showcase')
+        factories.Dataset(name="my-showcase", type="showcase")
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='edit', id='my-showcase'),
-            extra_environ=env,
+            url=url_for("showcase_edit", id="my-showcase"), extra_environ=env,
         )
 
         # edit showcase
-        form = response.forms['dataset-edit']
-        form['name'] = u'my-changed-showcase'
-        edit_response = submit_and_follow(app, form, env, 'save')
+        form = response.forms["dataset-edit"]
+        form["name"] = u"my-changed-showcase"
+        edit_response = helpers.submit_and_follow(app, form, env, "save")
 
         # Requested page is the showcase read url.
-        nosetools.assert_equal(url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                                       action='read', id='my-changed-showcase'), edit_response.request.path)
+        assert (
+            url_for("showcase_read", id="my-changed-showcase")
+            == edit_response.request.path
+        )
 
 
-class TestDatasetView(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db")
+class TestDatasetView(object):
 
-    '''Plugin adds a new showcases view for datasets.'''
+    """Plugin adds a new showcases view for datasets."""
 
-    def test_dataset_read_has_showcases_tab(self):
-        '''
+    def test_dataset_read_has_showcases_tab(self, app):
+        """
         Dataset view page has a new Showcases tab linked to the correct place.
-        '''
-        app = self._get_test_app()
-        dataset = factories.Dataset(name='my-dataset')
+        """
 
-        response = app.get(
-            url=url_for(controller='package', action='read', id=dataset['id'])
-        )
+        dataset = factories.Dataset(name="my-dataset")
+
+        if tk.check_ckan_version("2.9"):
+            url = url = url_for("dataset.read", id=dataset["id"])
+        else:
+            url = url_for(
+                controller="package", action="read", id=dataset["id"]
+            )
+        response = app.get(url)
         # response contains link to dataset's showcase list
-        nosetools.assert_true('/dataset/showcases/{0}'.format(dataset['name']) in response)
+        assert "/dataset/showcases/{0}".format(dataset["name"]) in response
 
-    def test_dataset_showcase_page_lists_showcases_no_associations(self):
-        '''
+    def test_dataset_showcase_page_lists_showcases_no_associations(self, app):
+        """
         No showcases are listed if dataset has no showcase associations.
-        '''
+        """
 
-        app = self._get_test_app()
-        dataset = factories.Dataset(name='my-dataset')
+        dataset = factories.Dataset(name="my-dataset")
 
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='dataset_showcase_list', id=dataset['id'])
+            url=url_for("showcase_dataset_showcase_list", id=dataset["id"])
         )
 
-        nosetools.assert_equal(len(response.html.select('ul.media-grid li.media-item')), 0)
+        assert (
+            len(
+                BeautifulSoup(response.body).select(
+                    "ul.media-grid li.media-item"
+                )
+            )
+            == 0
+        )
 
-    def test_dataset_showcase_page_lists_showcases_two_associations(self):
-        '''
+    def test_dataset_showcase_page_lists_showcases_two_associations(self, app):
+        """
         Two showcases are listed for dataset with two showcase associations.
-        '''
+        """
 
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
-        dataset = factories.Dataset(name='my-dataset')
-        showcase_one = factories.Dataset(name='my-first-showcase', type='showcase')
-        showcase_two = factories.Dataset(name='my-second-showcase', type='showcase')
-        factories.Dataset(name='my-third-showcase', type='showcase')
-
-        context = {'user': sysadmin['name']}
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset['id'],
-                            showcase_id=showcase_one['id'])
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset['id'],
-                            showcase_id=showcase_two['id'])
+        dataset = factories.Dataset(name="my-dataset")
+        showcase_one = factories.Dataset(
+            name="my-first-showcase", type="showcase"
+        )
+        showcase_two = factories.Dataset(
+            name="my-second-showcase", type="showcase"
+        )
+        factories.Dataset(name="my-third-showcase", type="showcase")
+
+        context = {"user": sysadmin["name"]}
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset["id"],
+            showcase_id=showcase_one["id"],
+        )
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset["id"],
+            showcase_id=showcase_two["id"],
+        )
 
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='dataset_showcase_list', id=dataset['id'])
+            url=url_for("showcase_dataset_showcase_list", id=dataset["id"])
         )
 
-        nosetools.assert_equal(len(response.html.select('li.media-item')), 2)
-        nosetools.assert_true('my-first-showcase' in response)
-        nosetools.assert_true('my-second-showcase' in response)
-        nosetools.assert_true('my-third-showcase' not in response)
+        assert len(BeautifulSoup(response.body).select("li.media-item")) == 2
+        assert "my-first-showcase" in response
+        assert "my-second-showcase" in response
+        assert "my-third-showcase" not in response
 
-    def test_dataset_showcase_page_add_to_showcase_dropdown_list(self):
-        '''
+    def test_dataset_showcase_page_add_to_showcase_dropdown_list(self, app):
+        """
         Add to showcase dropdown only lists showcases that aren't already
         associated with dataset.
-        '''
-        app = self._get_test_app()
+        """
+        if tk.check_ckan_version("2.9"):
+            pytest.skip("submit_and_follow not supported")
+
         sysadmin = factories.Sysadmin()
-        dataset = factories.Dataset(name='my-dataset')
-        showcase_one = factories.Dataset(name='my-first-showcase', type='showcase')
-        showcase_two = factories.Dataset(name='my-second-showcase', type='showcase')
-        showcase_three = factories.Dataset(name='my-third-showcase', type='showcase')
+        dataset = factories.Dataset(name="my-dataset")
+        showcase_one = factories.Dataset(
+            name="my-first-showcase", type="showcase"
+        )
+        showcase_two = factories.Dataset(
+            name="my-second-showcase", type="showcase"
+        )
+        showcase_three = factories.Dataset(
+            name="my-third-showcase", type="showcase"
+        )
 
-        context = {'user': sysadmin['name']}
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset['id'],
-                            showcase_id=showcase_one['id'])
+        context = {"user": sysadmin["name"]}
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset["id"],
+            showcase_id=showcase_one["id"],
+        )
 
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='dataset_showcase_list', id=dataset['id']),
-            extra_environ={'REMOTE_USER': str(sysadmin['name'])}
+            url=url_for("showcase_dataset_showcase_list", id=dataset["id"]),
+            extra_environ={"REMOTE_USER": str(sysadmin["name"])},
         )
 
-        showcase_add_form = response.forms['showcase-add']
-        showcase_added_options = [value for (value, _) in showcase_add_form['showcase_added'].options]
-        nosetools.assert_true(showcase_one['id'] not in showcase_added_options)
-        nosetools.assert_true(showcase_two['id'] in showcase_added_options)
-        nosetools.assert_true(showcase_three['id'] in showcase_added_options)
+        showcase_add_form = response.forms["showcase-add"]
+        showcase_added_options = [
+            value for (value, _) in showcase_add_form["showcase_added"].options
+        ]
+        assert showcase_one["id"] not in showcase_added_options
+        assert showcase_two["id"] in showcase_added_options
+        assert showcase_three["id"] in showcase_added_options
 
-    def test_dataset_showcase_page_add_to_showcase_dropdown_submit(self):
-        '''
+    def test_dataset_showcase_page_add_to_showcase_dropdown_submit(self, app):
+        """
         Submitting 'Add to showcase' form with selected showcase value creates
         a sc/pkg association.
-        '''
-        app = self._get_test_app()
+        """
+
         sysadmin = factories.Sysadmin()
-        dataset = factories.Dataset(name='my-dataset')
-        showcase_one = factories.Dataset(name='my-first-showcase', type='showcase')
-        factories.Dataset(name='my-second-showcase', type='showcase')
-        factories.Dataset(name='my-third-showcase', type='showcase')
+        dataset = factories.Dataset(name="my-dataset")
+        showcase_one = factories.Dataset(
+            name="my-first-showcase", type="showcase"
+        )
+        factories.Dataset(name="my-second-showcase", type="showcase")
+        factories.Dataset(name="my-third-showcase", type="showcase")
 
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
+        if tk.check_ckan_version("2.9"):
+            pytest.skip("submit_and_follow not supported")
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
 
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='dataset_showcase_list', id=dataset['id']),
-            extra_environ=env
+            url=url_for("showcase_dataset_showcase_list", id=dataset["id"]),
+            extra_environ=env,
         )
 
-        form = response.forms['showcase-add']
-        form['showcase_added'] = showcase_one['id']
-        showcase_add_response = submit_and_follow(app, form, env)
+        form = response.forms["showcase-add"]
+        form["showcase_added"] = showcase_one["id"]
+        showcase_add_response = helpers.submit_and_follow(app, form, env)
 
         # returns to the correct page
-        nosetools.assert_equal(showcase_add_response.request.path, "/dataset/showcases/my-dataset")
+        assert (
+            showcase_add_response.request.path
+            == "/dataset/showcases/my-dataset"
+        )
         # an association is created
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 1)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 1
 
-    def test_dataset_showcase_page_remove_showcase_button_submit(self):
-        '''
+    def test_dataset_showcase_page_remove_showcase_button_submit(self, app):
+        """
         Submitting 'Remove' form with selected showcase value deletes a sc/pkg
         association.
-        '''
-        app = self._get_test_app()
+        """
+
         sysadmin = factories.Sysadmin()
-        dataset = factories.Dataset(name='my-dataset')
-        showcase_one = factories.Dataset(name='my-first-showcase', type='showcase')
+        dataset = factories.Dataset(name="my-dataset")
+        showcase_one = factories.Dataset(
+            name="my-first-showcase", type="showcase"
+        )
 
-        context = {'user': sysadmin['name']}
-        helpers.call_action('ckanext_showcase_package_association_create',
-                            context=context, package_id=dataset['id'],
-                            showcase_id=showcase_one['id'])
+        context = {"user": sysadmin["name"]}
+        helpers.call_action(
+            "ckanext_showcase_package_association_create",
+            context=context,
+            package_id=dataset["id"],
+            showcase_id=showcase_one["id"],
+        )
 
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 1)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 1
+        if tk.check_ckan_version("2.9"):
+            pytest.skip("submit_and_follow not supported")
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='dataset_showcase_list', id=dataset['id']),
-            extra_environ=env
+            url=url_for("showcase_dataset_showcase_list", id=dataset["id"]),
+            extra_environ=env,
         )
-
         # Submit the remove form.
         form = response.forms[1]
-        nosetools.assert_equal(form['remove_showcase_id'].value, showcase_one['id'])
-        showcase_remove_response = submit_and_follow(app, form, env)
+        assert form["remove_showcase_id"].value == showcase_one["id"]
+        showcase_remove_response = helpers.submit_and_follow(app, form, env)
 
         # returns to the correct page
-        nosetools.assert_equal(showcase_remove_response.request.path, "/dataset/showcases/my-dataset")
+        assert (
+            showcase_remove_response.request.path
+            == "/dataset/showcases/my-dataset"
+        )
         # the association is deleted
-        nosetools.assert_equal(model.Session.query(ShowcasePackageAssociation).count(), 0)
+        assert model.Session.query(ShowcasePackageAssociation).count() == 0
 
 
-class TestShowcaseAdminManageView(ShowcaseFunctionalTestBase):
+@pytest.mark.usefixtures("clean_db")
+class TestShowcaseAdminManageView(object):
 
-    '''Plugin adds a showcase admin management page to ckan-admin section.'''
+    """Plugin adds a showcase admin management page to ckan-admin section."""
 
-    def test_ckan_admin_has_showcase_config_tab(self):
-        '''
+    def test_ckan_admin_has_showcase_config_tab(self, app):
+        """
         ckan-admin index page has a showcase config tab.
-        '''
-        if not tk.check_ckan_version(min_version='2.4'):
-            raise SkipTest('Showcase config tab only available for CKAN 2.4+')
+        """
+        if not tk.check_ckan_version(min_version="2.4"):
+            pytest.skip(
+                "Showcase config tab only available for CKAN 2.4+"
+            )
 
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
         response = app.get(
-            url=url_for(controller='admin', action='index'),
-            extra_environ=env
+            url=url_for(controller="admin", action="index"), extra_environ=env
         )
         # response contains link to dataset's showcase list
-        nosetools.assert_true('/ckan-admin/showcase_admins' in response)
+        assert "/ckan-admin/showcase_admins" in response
 
-    def test_showcase_admin_manage_page_returns_correct_status(self):
-        '''
+    def test_showcase_admin_manage_page_returns_correct_status(self, app):
+        """
         /ckan-admin/showcase_admins can be successfully accessed.
-        '''
-        app = self._get_test_app()
+        """
+
         sysadmin = factories.Sysadmin()
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
-        app.get(url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                            action='manage_showcase_admins'),
-                status=200, extra_environ=env)
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
+        app.get(url=url_for("showcase_admins"), status=200, extra_environ=env)
 
-    def test_showcase_admin_manage_page_lists_showcase_admins(self):
-        '''
+    def test_showcase_admin_manage_page_lists_showcase_admins(self, app):
+        """
         Showcase admins are listed on the showcase admin page.
-        '''
-        app = self._get_test_app()
+        """
+
         user_one = factories.User()
         user_two = factories.User()
         user_three = factories.User()
 
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_one['name'])
-        helpers.call_action('ckanext_showcase_admin_add', context={},
-                            username=user_two['name'])
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user_one["name"]
+        )
+        helpers.call_action(
+            "ckanext_showcase_admin_add", context={}, username=user_two["name"]
+        )
 
         sysadmin = factories.Sysadmin()
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
-        response = app.get(url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                                       action='manage_showcase_admins'),
-                           status=200, extra_environ=env)
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
+        response = app.get(
+            url=url_for("showcase_admins"), status=200, extra_environ=env
+        )
 
-        nosetools.assert_true('/user/{0}'.format(user_one['name']) in response)
-        nosetools.assert_true('/user/{0}'.format(user_two['name']) in response)
-        nosetools.assert_true('/user/{0}'.format(user_three['name']) not in response)
+        assert "/user/{0}".format(user_one["name"]) in response
+        assert "/user/{0}".format(user_two["name"]) in response
+        assert "/user/{0}".format(user_three["name"]) not in response
 
-    def test_showcase_admin_manage_page_no_admins_message(self):
-        '''
+    def test_showcase_admin_manage_page_no_admins_message(self, app):
+        """
         Showcase admins page displays message if no showcase admins present.
-        '''
-        app = self._get_test_app()
+        """
 
         sysadmin = factories.Sysadmin()
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
-        response = app.get(url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                                       action='manage_showcase_admins'),
-                           status=200, extra_environ=env)
-
-        nosetools.assert_true('There are currently no Showcase Admins' in response)
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
+        response = app.get(
+            url=url_for("showcase_admins"), status=200, extra_environ=env
+        )
 
+        assert "There are currently no Showcase Admins" in response
 
-class TestSearch(helpers.FunctionalTestBase):
 
-    def test_search_with_nonascii_filter_query(self):
-        '''
+@pytest.mark.usefixtures("clean_db", "clean_index")
+class TestSearch(object):
+    def test_search_with_nonascii_filter_query(self, app):
+        """
         Searching with non-ASCII filter queries works.
 
         See https://github.com/ckan/ckanext-showcase/issues/34.
-        '''
-        app = self._get_test_app()
-        tag = u'\xe4\xf6\xfc'
-        dataset = factories.Dataset(tags=[{'name': tag, 'state': 'active'}])
-        result = helpers.call_action('package_search', fq='tags:' + tag)
-        nosetools.assert_equals(result['count'], 1)
+        """
+
+        tag = u"\xe4\xf6\xfc"
+        factories.Dataset(tags=[{"name": tag, "state": "active"}])
+        result = helpers.call_action("package_search", fq="tags:" + tag)
+        assert result["count"] == 1
 
 
-class TestCKEditor(helpers.FunctionalTestBase):
+@pytest.mark.usefixtures('clean_db')
+class TestCKEditor(object):
+    @pytest.mark.ckan_config("ckanext.showcase.editor", "ckeditor")
+    def test_rich_text_editor_is_shown_when_configured(self, app):
 
-    @helpers.change_config('ckanext.showcase.editor', 'ckeditor')
-    def test_rich_text_editor_is_shown_when_configured(self):
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
-        factories.Dataset(name='my-showcase', type='showcase')
+        factories.Dataset(name="my-showcase", type="showcase")
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='edit',
-                        id='my-showcase'),
-            extra_environ=env,
+            url=url_for("showcase_edit", id="my-showcase",), extra_environ=env,
         )
-        nosetools.assert_in('<textarea id="editor"', response.ubody)
+        assert '<textarea id="editor"' in response.body
 
+    def test_rich_text_editor_is_not_shown_when_not_configured(self, app):
 
-    def test_rich_text_editor_is_not_shown_when_not_configured(self):
-        app = self._get_test_app()
         sysadmin = factories.Sysadmin()
-        factories.Dataset(name='my-showcase', type='showcase')
+        factories.Dataset(name="my-showcase", type="showcase")
 
-        env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
+        env = {"REMOTE_USER": sysadmin["name"].encode("ascii")}
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='edit',
-                        id='my-showcase'),
-            extra_environ=env,
+            url=url_for("showcase_edit", id="my-showcase",), extra_environ=env,
         )
-        nosetools.assert_not_in('<textarea id="editor"', response.ubody)
+        assert '<textarea id="editor"' not in response.body
 
-    @helpers.change_config('ckanext.showcase.editor', 'ckeditor')
-    def test_custom_div_content_is_used_with_ckeditor(self):
-        app = self._get_test_app()
+    @pytest.mark.ckan_config("ckanext.showcase.editor", "ckeditor")
+    def test_custom_div_content_is_used_with_ckeditor(self, app):
         sysadmin = factories.Sysadmin()
         factories.Dataset(name='my-showcase', type='showcase')
 
         env = {'REMOTE_USER': sysadmin['name'].encode('ascii')}
         response = app.get(
-            url=url_for(controller='ckanext.showcase.controller:ShowcaseController',
-                        action='read',
-                        id='my-showcase'),
-            extra_environ=env,
+            url=url_for("showcase_read", id="my-showcase",), extra_environ=env,
         )
-        nosetools.assert_in('<div class="ck-content">', response.ubody)
\ No newline at end of file
+        assert '<div class="ck-content">' in response.body
diff --git a/ckanext/showcase/tests/test_utils.py b/ckanext/showcase/tests/test_utils.py
new file mode 100644
index 00000000..89e4f897
--- /dev/null
+++ b/ckanext/showcase/tests/test_utils.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from ckan.lib import helpers
+from ckan.tests import factories, helpers as test_helpers
+
+from ckanext.showcase.utils import markdown_to_html
+
+
+@pytest.mark.usefixtures("clean_db")
+class TestUtils(object):
+
+    def test_markdown_to_html(self):
+        showcase1 = factories.Dataset(
+                type='showcase',
+                name='my-showcase',
+                notes='# Title')
+
+        showcase2 = factories.Dataset(
+            type='showcase',
+            name='my-showcase-2',
+            notes='# Title 2')
+
+        markdown_to_html()
+
+        migrated_showcase1 = test_helpers.call_action(
+                'package_show',
+                context={'ignore_auth': True},
+                id=showcase1['id']
+            )
+
+        assert migrated_showcase1['notes'] == helpers.render_markdown(showcase1['notes'])
+
+        migrated_showcase2 = test_helpers.call_action(
+                'package_show',
+                context={'ignore_auth': True},
+                id=showcase2['id']
+            )
+
+        assert migrated_showcase2['notes'] == helpers.render_markdown(showcase2['notes'])
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 00000000..368d79d7
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+
+pytest_plugins = [
+    u'ckanext.showcase.tests.ckan_setup',
+    u'ckanext.showcase.tests.fixtures',
+]
diff --git a/test-travis.ini b/test-travis.ini
deleted file mode 100644
index 92655389..00000000
--- a/test-travis.ini
+++ /dev/null
@@ -1,20 +0,0 @@
-[DEFAULT]
-debug = false
-smtp_server = localhost
-error_email_from = paste@localhost
-
-[server:main]
-use = egg:Paste#http
-host = 0.0.0.0
-port = 5000
-
-[app:main]
-use = config:../ckan/test-core.ini
-
-# Insert any custom config settings to be used when running your extension's
-# tests here.
-
-ckan.legacy_templates = false
-ckan.plugins = showcase
-
-solr_url = http://127.0.0.1:8983/solr
diff --git a/test.ini b/test.ini
index a22bba67..1f127b2a 100644
--- a/test.ini
+++ b/test.ini
@@ -17,7 +17,6 @@ use = config:../ckan/test-core.ini
 ckan.legacy_templates = false
 ckan.plugins = showcase
 
-solr_url = http://127.0.0.1:8983/solr/ckan
 
 # Logging configuration
 [loggers]