# Kanbanara Kanban Component
# Written by Rebecca Shalfield between 2013 and 2017
# Copyright (c) Rebecca Shalfield 2013-2017
# Released under the GNU AGPL v3

import ast
import datetime
import logging
import os
import re
import subprocess
import time
import urllib.parse

from bson import ObjectId
import cherrypy
from kanbanara import Kanbanara
import pymongo
from pymongo import MongoClient


class Kanban(Kanbanara):

    @cherrypy.expose
    def step_buffer_blocked(self, swimlane_no, step_no):
        """Populates the blocked section for a given column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_blocked(['updatable'], ['updatable'], column_name, state,
                                                   priorities, projection)

    @cherrypy.expose
    def step_buffer_deferred(self, swimlane_no, step_no):
        """Populates the deferrals section for a given column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_deferred(['updatable'], ['updatable'], column_name,
                                                    state, priorities, projection)

    @cherrypy.expose
    def step_buffer_ghosted(self, swimlane_no, step_no):
        """Populates the ghosted section for a given column via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        centric = buffer_column['centric']
        if centric == 'Owner':
            opposite_centric = 'Reviewer'
        elif centric == 'Reviewer':
            opposite_centric = 'Owner'

        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_ghosted(['ghosted'], ['ghosted'], column_name, state,
                                                   opposite_centric, priorities)

    @cherrypy.expose
    def step_buffer_waiting(self, swimlane_no, step_no):
        """Populates the waiting section for a given column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        return self.populate_kanban_column_waiting(['updatable'], column_name, state, projection)

    @cherrypy.expose
    def step_counterpart_blocked(self, swimlane_no, step_no):
        """Populates the blocked section for a given counterpart column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_blocked(['updatable'], ['updatable'], column_name, state,
                                                   priorities, projection)

    @cherrypy.expose
    def step_counterpart_deferred(self, swimlane_no, step_no):
        """Populates the deferrals section for a given counterpart column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_deferred(['updatable'], ['updatable'], column_name,
                                                    state, priorities, projection)

    @cherrypy.expose
    def step_counterpart_ghosted(self, swimlane_no, step_no):
        """Populates the ghosted section for a given counterpart column via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        if centric == 'Owner':
            opposite_centric = 'Reviewer'
        elif centric == 'Reviewer':
            opposite_centric = 'Owner'

        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_ghosted(['ghosted'], ['ghosted'], column_name, state,
                                                   opposite_centric, priorities)

    @cherrypy.expose
    def step_counterpart_waiting(self, swimlane_no, step_no):
        """Populates the waiting section for a given counterpart column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        return self.populate_kanban_column_waiting(['updatable'], column_name, state, projection)

    @cherrypy.expose
    def step_main_blocked(self, swimlane_no, step_no):
        """Populates the blocked section for a given column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_blocked(['updatable'], ['updatable'], column_name, state,
                                                   priorities, projection)

    @cherrypy.expose
    def step_main_deferred(self, swimlane_no, step_no):
        """Populates the deferrals section for a given column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_deferred(['updatable'], ['updatable'], column_name,
                                                    state, priorities, projection)

    @cherrypy.expose
    def step_main_ghosted(self, swimlane_no, step_no):
        """Populates the ghosted section for a given column via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        if centric == 'Owner':
            opposite_centric = 'Reviewer'
        elif centric == 'Reviewer':
            opposite_centric = 'Owner'

        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        priorities = self.displayable_priorities()
        return self.populate_kanban_column_ghosted(['ghosted'], ['ghosted'], column_name, state,
                                                   opposite_centric, priorities)

    @cherrypy.expose
    def step_main_waiting(self, swimlane_no, step_no):
        """Populates the waiting section for a given column via AJAX"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        projection = 0
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)

        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        return self.populate_kanban_column_waiting(['updatable'], column_name, state, projection)

    def automatically_pull_card_to_reach_min_wip_limit(self, buffer_state, next_step_state):
        """Automatically pull a card from previous buffer state to reach minimum WIP limit"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        next_step_state_owner_count, next_step_state_reviewer_count, next_step_state_min_wip_limit, _ = self.get_document_count(next_step_state, [])
        if next_step_state_min_wip_limit:
            _, _, centric, _, _ = self.get_associated_state_information(next_step_state)
            if centric == 'Reviewer':
                spare_capacity = next_step_state_min_wip_limit - next_step_state_reviewer_count
            else:
                spare_capacity = next_step_state_min_wip_limit - next_step_state_owner_count

            if spare_capacity > 0:
                if centric == 'Reviewer':
                    _, search_criteria, _ = self.get_filtered_search_criteria(session_document, [buffer_state])
                else:
                    search_criteria, _, _ = self.get_filtered_search_criteria(session_document, [buffer_state])

                search_criteria["expedite"] = False
                for card_document in self.cards_collection.find(search_criteria):
                    card_document['state'] = next_step_state
                    self.cards_collection.save(card_document)
                    break

    def automatically_pull_expedite_card_from_buffer_state(self, buffer_state, next_step_state):
        """Automatically pull an expedited card from previous buffer state"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [buffer_state])
        owner_reviewer_search_criteria["expedite"] = True
        for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
            card_document['state'] = next_step_state
            self.cards_collection.save(card_document)

    @cherrypy.expose
    def step_counterpart_expedite(self, swimlane_no, step_no):
        """Shows expedited cards in the given state on the kanban board"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            state = custom_states[state]

        content = []
        owner_search_criteria, reviewer_search_criteria, _ = self.get_filtered_search_criteria(session_document, [state])
        if centric == 'Reviewer':
            search_criteria = reviewer_search_criteria
        else:
            search_criteria = owner_search_criteria

        search_criteria['expedite'] = True
        content.append(f'<div id="step{step_no}counterpartexpedite"><table><tr><td>')
        if self.cards_collection.find(search_criteria).count():
            content.append('<b class="expedite">Expedite</b></td><td>')
            for card_document in self.cards_collection.find(search_criteria):
                # TODO - Find a way to set projection correctly here
                if not self.card_waiting_for_other_owner_or_reviewer(card_document, 0):
                    content.append(self.assemble_kanban_card(['owner', 'coowner'], ['updatable'], swimlane_no,
                                                             card_document['_id'], False, 0))

        else:
            member_document = Kanbanara.get_member_document(self, session_document)
            projects = self.get_member_projects(member_document)
            search_criteria['project'] = {'$in': projects}
            if self.cards_collection.find(search_criteria).count():
                content.append('<span class="ui-icon ui-icon-info" title="An expedite entry in the '+state+' state exists for one of your other projects!" />')
            else:
                content.append('<span class="ui-icon ui-icon-info" title="There is no expedite entry in the '+state+' state!" />')

        content.append('</td></tr></table></div>')
        dummy = self.generate_drop_js_script(swimlane_no, step_no, 'counterpart', 'expedite')
        content.append(f'<script type="text/javascript" src="/kanban/scripts/autogenerated/swimlane-1step{step_no}counterpartexpedite.js"></script>')
        return "".join(content)

    @cherrypy.expose
    def step_buffer_high(self, swimlane_no, step_no):
        """Populates the high priority section of a given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        centric = buffer_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('high'):
            if metastate in ['unittestingaccepted', 'integrationtestingaccepted',
                             'systemtestingaccepted', 'acceptancetestingaccepted']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'buffer', column_name, state, metastate,
                                                   centric, ['high'])
            else:
                return self.populate_kanban_column(['updatable', 'minimised'],
                                                   ['updatable', 'minimised'], swimlane_no, step_no,
                                                   'buffer', column_name, state, metastate, centric,
                                                   ['high'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All high cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_buffer_all(self, swimlane_no, step_no):
        """Populates the disowned step with any card of a state no longer in workflow via AJAX"""
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        for disowned_card_doc_id in self.cards_collection.find({'project': project,
                                                                'state': {'$nin': condensed_column_states}
                                                               }).distinct('_id'):
            content.append(self.assemble_disowned_kanban_card(swimlane_no, disowned_card_doc_id))

        return ''.join(content)

    @cherrypy.expose
    def step_counterpart_all(self, swimlane_no, step_no):
        """Populates a section with all priorities for the given counterpart state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        priorities = self.displayable_priorities()
        if metastate in ['defined', 'analysed', 'closed']:
            return self.populate_kanban_column(['updatable', 'minimised'],
                                               ['updatable', 'minimised'], swimlane_no, step_no,
                                               'counterpart', column_name, state, metastate,
                                               centric, priorities)
        elif metastate in ['analysis', 'unittesting', 'integrationtesting', 'systemtesting',
                           'acceptancetesting']:
            return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                               swimlane_no, step_no, 'counterpart', column_name,
                                               state, metastate, centric, priorities)
        else:
            return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no, step_no,
                                               'counterpart', column_name, state, metastate,
                                               centric, priorities)

    @cherrypy.expose
    def step_counterpart_high(self, swimlane_no, step_no):
        """Populates the high priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('high'):
            if metastate in ['analysis', 'unittesting', 'integrationtesting', 'systemtesting',
                             'acceptancetesting']:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'counterpart', column_name,
                                                   state, metastate, centric, ['high'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'counterpart', column_name, state,
                                                   metastate, centric, ['high'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All high cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_main_all(self, swimlane_no, step_no):
        """Populates a section with all priorities for the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        priorities = self.displayable_priorities()
        if metastate in ['defined', 'analysed', 'closed']:
            return self.populate_kanban_column(['updatable', 'minimised'],
                                               ['updatable', 'minimised'], swimlane_no, step_no,
                                               'main', column_name, state, metastate, centric,
                                               priorities)
        elif metastate in ['analysis', 'unittesting', 'integrationtesting', 'systemtesting',
                           'acceptancetesting']:
            return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                               swimlane_no, step_no, 'main', column_name, state,
                                               metastate, centric, priorities)
        else:
            return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no, step_no,
                                               'main', column_name, state, metastate, centric,
                                               priorities)

    @cherrypy.expose
    def step_main_high(self, swimlane_no, step_no):
        """Populates the high priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('high'):
            if metastate in ['analysis', 'unittesting', 'integrationtesting', 'systemtesting',
                             'acceptancetesting']:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'main', column_name, state,
                                                   metastate, centric, ['high'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'main', column_name, state, metastate,
                                                   centric, ['high'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All high cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_buffer_low(self, swimlane_no, step_no):
        """Populates the low priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        centric = buffer_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('low'):
            if metastate in ['unittestingaccepted', 'integrationtestingaccepted',
                             'systemtestingaccepted', 'acceptancetestingaccepted']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'buffer', column_name, state, metastate,
                                                   centric, ['low'])
            else:
                return self.populate_kanban_column(['updatable', 'minimised'],
                                                   ['updatable', 'minimised'], swimlane_no, step_no,
                                                   'buffer', column_name, state, metastate, centric,
                                                   ['low'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All low cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_counterpart_low(self, swimlane_no, step_no):
        """Populates the low priority section of the given counterpart state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('low'):
            if metastate in ['untriaged', 'triaged', 'backlog']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'counterpart', column_name, state,
                                                   metastate, centric, ['low'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'counterpart', column_name,
                                                   state, metastate, centric, ['low'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All low cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_counterpart_medium(self, swimlane_no, step_no):
        """Populates the medium priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('medium'):
            if metastate in ['untriaged', 'triaged', 'backlog']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'counterpart', column_name, state,
                                                   metastate, centric, ['medium'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'counterpart', column_name,
                                                   state, metastate, centric, ['medium'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All medium cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_main_low(self, swimlane_no, step_no):
        """Populates the low priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('low'):
            if metastate in ['untriaged', 'triaged', 'backlog']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'main', column_name, state, metastate,
                                                   centric, ['low'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'main', column_name, state,
                                                   metastate, centric, ['low'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All low cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_main_medium(self, swimlane_no, step_no):
        """Populates the medium priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('medium'):
            if metastate in ['untriaged', 'triaged', 'backlog']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'main', column_name, state, metastate,
                                                   centric, ['medium'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'main', column_name, state,
                                                   metastate, centric, ['medium'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All medium cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_buffer_medium(self, swimlane_no, step_no):
        """Populates the medium priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        centric = buffer_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('medium'):
            if metastate in ['unittestingaccepted', 'integrationtestingaccepted',
                             'systemtestingaccepted', 'acceptancetestingaccepted']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'buffer', column_name, state, metastate,
                                                   centric, ['medium'])
            else:
                return self.populate_kanban_column(['updatable', 'minimised'],
                                                   ['updatable', 'minimised'], swimlane_no, step_no,
                                                   'buffer', column_name, state, metastate, centric,
                                                   ['medium'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All medium cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def ajax_loader(self):
        return '<img src="/images/ajax-loader.gif">'

    @cherrypy.expose
    def step_counterpart_critical(self, swimlane_no, step_no):
        """Populates the critical priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('critical'):
            if metastate in ['untriaged', 'triaged', 'backlog']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'counterpart', column_name, state,
                                                   metastate, centric, ['critical'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'counterpart', column_name,
                                                   state, metastate, centric, ['critical'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All critical cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_main_critical(self, swimlane_no, step_no):
        """Populates the critical priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('critical'):
            if metastate in ['untriaged', 'triaged', 'backlog']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'main', column_name, state, metastate,
                                                   centric, ['critical'])
            else:
                return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'],
                                                   swimlane_no, step_no, 'main', column_name, state,
                                                   metastate, centric, ['critical'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All critical cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_buffer_critical(self, swimlane_no, step_no):
        """Populates the critical priority section of the given state via AJAX"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        state = buffer_column['state']
        centric = buffer_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        if self.displayable_priority('critical'):
            if metastate in ['unittestingaccepted', 'integrationtestingaccepted',
                             'systemtestingaccepted', 'acceptancetestingaccepted']:
                return self.populate_kanban_column(['updatable'], ['updatable'], swimlane_no,
                                                   step_no, 'buffer', column_name, state, metastate,
                                                   centric, ['critical'])
            else:
                return self.populate_kanban_column(['updatable', 'minimised'],
                                                   ['updatable', 'minimised'], swimlane_no, step_no,
                                                   'buffer', column_name, state, metastate, centric,
                                                   ['critical'])

        else:
            return '<span class="ui-icon ui-icon-info" title="All critical cards are being suppressed by your filter!" />'

    @cherrypy.expose
    def step_buffer_header(self, step_no=-1):
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        workflow_index = project_document.get('workflow_index', {})
        global_wips = project_document.get('global_wips', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        step_document = workflow[int(step_no)]
        buffer_column = step_document['buffercolumn']
        column_name = buffer_column['name']
        centric = buffer_column['centric']
        state = buffer_column['state']
        description = buffer_column.get('description', '')
        _, step_role, _, _, next_state = self.get_associated_state_information(state)
        if step_role == 'buffer':
            self.automatically_pull_expedite_card_from_buffer_state(state, next_state)
            self.automatically_pull_card_to_reach_min_wip_limit(state, next_state)

        if 'swimlanes' in member_document and member_document['swimlanes']:
            toggle_button_code = ''
        else:
            toggle_button_code = '<input class="togglecolumn" type="button" id="togglestep'+str(step_no)+'buffer" value="Hide" />'

        owner_count, reviewer_count, min_wip_limit, max_wip_limit = self.get_document_count(state, [])
        return self.get_kanban_column_heading(state, column_name, step_no, 'buffer', centric, description, owner_count,
                                              reviewer_count, min_wip_limit, max_wip_limit, toggle_button_code,
                                              condensed_column_states, global_wips)

    @cherrypy.expose
    def step_counterpart_header(self, step_no=-1):
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        workflow_index = project_document.get('workflow_index', {})
        global_wips = project_document.get('global_wips', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        step_document = workflow[int(step_no)]
        counterpart_column = step_document['counterpartcolumn']
        column_name = counterpart_column['name']
        state = counterpart_column['state']
        centric = counterpart_column['centric']
        description = counterpart_column.get('description', '')
        owner_count, reviewer_count, min_wip_limit, max_wip_limit = self.get_document_count(state, [])
        if 'swimlanes' in member_document and member_document['swimlanes']:
            toggle_button_code = ''
        else:
            toggle_button_code = '<input class="togglecolumn" type="button" id="togglestep'+str(step_no)+'counterpart" value="Hide" />'

        return self.get_kanban_column_heading(state, column_name, step_no, 'counterpart', centric, description, owner_count, reviewer_count,
                                              min_wip_limit, max_wip_limit, toggle_button_code, condensed_column_states,
                                              global_wips)

    @cherrypy.expose
    def step_main_header(self, step_no=-1):
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        workflow_index = project_document.get('workflow_index', {})
        global_wips = project_document.get('global_wips', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        column_name = main_column['name']
        state = main_column['state']
        centric = main_column['centric']
        description = main_column.get('description', '')
        owner_count, reviewer_count, min_wip_limit, max_wip_limit = self.get_document_count(state, [])
        if 'swimlanes' in member_document and member_document['swimlanes']:
            toggle_button_code = ''
        else:
            toggle_button_code = '<input class="togglecolumn" type="button" id="togglestep'+str(step_no)+'main" value="Hide" />'

        return self.get_kanban_column_heading(state, column_name, step_no, 'main', centric, description, owner_count, reviewer_count,
                                              min_wip_limit, max_wip_limit, toggle_button_code, condensed_column_states,
                                              global_wips)

    def bottleneck_condition(self, project, state, metastate):
        """Checks whether the state following a given state has reached its WIP limit, thereby causing a bottleneck"""
        content = ""
        bottleneck = False
        _, _, _, _, next_state = self.get_associated_state_information(state)
        owner_count, reviewer_count, _, max_wip_limit = self.get_document_count(next_state, [])
        if isinstance(max_wip_limit, int) and max_wip_limit != -1:
            _, _, next_centric, _, _ = self.get_associated_state_information(next_state)
            if next_centric == 'Reviewer':
                if reviewer_count >= max_wip_limit:
                    bottleneck = True

            else:
                if owner_count >= max_wip_limit:
                    bottleneck = True

        if bottleneck:
            content = '<div class="wiplimitexceeded" title="The '+next_state+' column has reached its maximum WIP limit!">Bottleneck!</div>'

        return content

    def card_blocked_by_any_means(self, card_document, projection):
        """Returns True if given card is explicitly blocked, blocked by one of its children or subject to a before/after blockage"""
        blocked = card_document.get('blocked', '')
        if blocked:
            if projection:
                blockeduntil = card_document.get('blockeduntil', 0)
                if blockeduntil:
                    epoch = datetime.datetime.utcnow()
                    projected_epoch = epoch + (self.timedelta_day * projection)
                    if blockeduntil > projected_epoch:
                        return True

                else:
                    return True

            else:
                return True

        if self.card_blocked_by_child(card_document):
            return True

        if self.card_blocked_by_before_card(card_document):
            return True

        if self.card_blocked_by_after_card(card_document):
            return True

        return False

    def card_hidden(self, card_document, projection):
        """Returns True if given card is explicitly hidden"""
        hiddenuntil = card_document.get('hiddenuntil', '')
        if hiddenuntil:
            if projection:
                epoch = datetime.datetime.utcnow()
                projected_epoch = epoch + (self.timedelta_day * projection)
                if hiddenuntil > projected_epoch:
                    return True

            else:
                return True

        return False

    def column_required(self, column_name):
        """Ascertain whether a particular column is to be displayed"""
        required = False
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if member_document and 'columns' in member_document and member_document['columns']:
            project_document = self.projects_collection.find_one({'project': member_document['project']})
            workflow_index = project_document.get('workflow_index', {})
            uncondensed_column_names = workflow_index['uncondensed_column_names']
            (start_column, end_column) = member_document['columns'].split('-')
            try:
                start_column_pos = uncondensed_column_names.index(start_column)
                end_column_pos   = uncondensed_column_names.index(end_column)
                if column_name:
                    if column_name in uncondensed_column_names[start_column_pos:end_column_pos+1]:
                        return True

            except:
                return True

        else:
            return True

        return required

    def displayable_priorities(self):
        """Return the list of priorities that are currently displayable"""
        priorities = [priority for priority in self.priorities if self.displayable_priority(priority)]
        return priorities

    def displayable_priority(self, priority):
        """Checks whether a card of this priority is currently displayable"""
        displayable_priority = False
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if member_document:
            if 'priority' in member_document:
               if not member_document['priority'] or member_document['priority'] == priority:
                   return True

            else:
                return True

        else:
            return True

        return displayable_priority

    def displayable_severity(self, severity):
        """Checks whether a card of this severity is currently displayable"""
        displayable_severity = False
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if member_document:
            if 'severity' in member_document:
               if not member_document['severity'] or member_document['severity'] == severity:
                   return True

            else:
                return True

        else:
            return True

        return displayable_severity

    def increment_number_of_countable_cards_displayed(self, state, centric, card_document, no_of_countable_cards_displayed, projection):
        """
            Increments the number of cards so far displayed that count towards your maximum WIP limit.
            Deferred, blocked and ghosted cards are ignored.
        """
        if not self.card_deferred(card_document, projection) and not self.card_hidden(card_document, projection) and not self.card_blocked_by_any_means(card_document, projection) and not self.card_waiting_for_other_owner_or_reviewer(card_document, projection):
            session_id = Kanbanara.cookie_handling(self)
            session_document = self.sessions_collection.find_one({"session_id": session_id})
            member_document = Kanbanara.get_member_document(self, session_document)
            owner      = ""
            coowner    = ""
            reviewer   = ""
            coreviewer = ""
            if member_document and 'teammember' in member_document:
                selected_username = member_document["teammember"]
                if centric == 'Reviewer':
                    reviewer   = card_document.get('reviewer',   '')
                    coreviewer = card_document.get('coreviewer', '')
                    if (reviewer and selected_username == reviewer) or (coreviewer and selected_username == coreviewer):
                        no_of_countable_cards_displayed += 1

                else:
                    owner   = card_document.get('owner',   '')
                    coowner = card_document.get('coowner', '')
                    if (owner and selected_username == owner) or (coowner and selected_username == coowner):
                        no_of_countable_cards_displayed += 1

        return no_of_countable_cards_displayed

    @staticmethod
    def max_wip_limit_reached():
        """Indicates that you have reached your WIP limit for a given state"""
        return '<div class="wiplimitreached">WIP Limit Reached!</div>'

    def get_state_max_limit(self, member_document, state):
        state_max_limit = -1
        if member_document:
            if 'project_wips' in member_document:
                project_wips = member_document['project_wips']
            else:
                project_wips = {}

            project = member_document.get('project', '')
            if project in project_wips:
                personal_wips = project_wips[project]
            else:
                personal_wips = {}

            step_no, step_role, centric, preceding_state, next_state = self.get_associated_state_information(state)
            if 'step'+str(step_no)+step_role+'maxwip' in personal_wips and personal_wips['step'+str(step_no)+step_role+'maxwip']:
                state_max_limit = personal_wips['step'+str(step_no)+step_role+'maxwip']
            elif project:
                for project_document in self.projects_collection.find({'project': project}):
                    if 'global_wips' in project_document:
                        global_wips = project_document['global_wips']
                        if 'step'+str(step_no)+step_role+'maxwip' in global_wips and global_wips['step'+str(step_no)+step_role+'maxwip']:
                            state_max_limit = global_wips['step'+str(step_no)+step_role+'maxwip']
                            break

        return state_max_limit

    @cherrypy.expose
    def pull_card_from_buffer_state(self, preceding_state, state):
        """Pulls the next card from the preceding buffer state"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        next_step_state_max_limit = self.get_state_max_limit(member_document, state)
        _, _, centric, _, _ = self.get_associated_state_information(state)
        owner_search_criteria, reviewer_search_criteria, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])
        if centric == 'Reviewer':
            count = self.cards_collection.find(reviewer_search_criteria).count()
        else:
            count = self.cards_collection.find(owner_search_criteria).count()

        if next_step_state_max_limit == -1 or count < next_step_state_max_limit:
            owner_reviewer_search_criteria["state"] = preceding_state
            for priority in self.priorities:
                owner_reviewer_search_criteria["priority"] = priority
                for severity in self.severities:
                    owner_reviewer_search_criteria["severity"] = severity
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        card_document['state'] = state
                        self.cards_collection.save(card_document)
                        raise cherrypy.HTTPRedirect("/kanban/index", 302)

        raise cherrypy.HTTPRedirect("/kanban/index", 302)

    @cherrypy.expose
    def pull_card_from_buffer_state_button(self, swimlane_no, step_no):
        """Pulls the next card from the preceding buffer state"""
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        main_column = step_document['maincolumn']
        state = main_column['state']
        _, _, centric, preceding_state, _ = self.get_associated_state_information(state)
        preceding_metastate = self.get_corresponding_metastate(project_document, preceding_state)
        if preceding_metastate in self.metastates_buffer_list:
            owner_search_criteria, reviewer_search_criteria, _ = self.get_filtered_search_criteria(session_document, [])
            owner_search_criteria["state"] = preceding_state
            if self.cards_collection.find(owner_search_criteria).count():
                state_max_limit = self.get_state_max_limit(member_document, state)
                if centric == 'Reviewer':
                    reviewer_search_criteria["state"] = state
                    next_step_state_count = self.cards_collection.find(reviewer_search_criteria).count()
                else:
                    owner_search_criteria["state"] = state
                    next_step_state_count = self.cards_collection.find(owner_search_criteria).count()

                if state_max_limit == -1 or next_step_state_count < state_max_limit:
                    content.append('<div align="center">')
                    content.append('<form class="pull" action="/kanban/pull_card_from_buffer_state" method="post">')
                    content.append('<input type="hidden" name="preceding_state" value="'+preceding_state+'">')
                    content.append('<input type="hidden" name="state" value="'+state+'">')
                    content.append('<input class="button" type="submit" value="Pull" title="Pull Next Card From '+preceding_state.capitalize()+' State">')
                    content.append('</form></div>')

        return "".join(content)

    def remove_duplicate_member_documents(self):
        """Forces unwanted member documents sharing a common username to be deleted"""
        for username in self.members_collection.find({}).distinct('username'):
            if self.members_collection.find({'username': username}).count() > 1:
                doc_ids = self.members_collection.find({'username': username}).distinct('_id')
                for doc_id in doc_ids[1:]:
                    self.members_collection.remove({'_id': ObjectId(doc_id)})

    def remove_expired_hiddenuntil_dates(self):
        """Removes any expired hiddenuntil dates from cards"""
        for card_document in self.cards_collection.find({'hiddenuntil': {'$lte': datetime.datetime.utcnow()}}):
            del card_document['hiddenuntil']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

    def remove_expired_blockeduntil_dates(self):
        """Removes any expired blockeduntil dates from cards"""
        for card_document in self.cards_collection.find({'blockeduntil': {'$lte': datetime.datetime.utcnow()}}):
            del card_document['blockeduntil']
            del card_document['blocked']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

    def remove_expired_deferreduntil_dates(self):
        """Removes any expired deferreduntil dates from cards"""
        for card_document in self.cards_collection.find({'deferreduntil': {'$lte': datetime.datetime.utcnow()}}):
            del card_document['deferreduntil']
            del card_document['deferred']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

    @cherrypy.expose
    def execute_card_rules_wrapper(self, doc_id):
        """Executes a given card's rules"""
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            self.execute_card_rules(card_document, 'forced')
            break

        raise cherrypy.HTTPRedirect("/kanban/index", 302)

    def tidy_svgcharts_folder(self):
        """Automatically removes .svg charts that are over a day old"""
        prog = subprocess.Popen(['python', os.path.join(self.current_dir, '..', 'subprocesses', 'tidy_svgcharts_folder.py'), ''])

    def __init__(self):
        """Initialisation"""
        self.component = 'kanban'

        Kanbanara.__init__(self)

        self.current_dir = os.path.dirname(os.path.abspath(__file__))

        logging.basicConfig(level=logging.DEBUG,
                            format='%(asctime)s - %(levelname)s - %(message)s',
                            filename=os.path.join(self.current_dir, '..', 'logs', 'kanbanara.log'),
                            filemode='w'
                           )

        self.housekeeping_last_done = 0

        self.next_card_numbers = {}

        self.plurals = {'bug':         'bugs',
                        'defect':      'defects',
                        'enhancement': 'enhancements',
                        'epic':        'epics',
                        'feature':     'features',
                        'story':       'stories',
                        'task':        'tasks',
                        'test':        'tests',
                        'transient':   'transient'}

        self.filter_bar_component_statistics = {'card':           ('Card',             'dynamic'),
                                                'category':       ('Category',         'dynamic'),
                                                'classofservice': ('Class Of Service', 'dynamic'),
                                                'columns':        ('Columns',          'static'),
                                                'customer':       ('Customer',         'dynamic'),
                                                'fontsize':       ('Font Size',        'dynamic'),
                                                'hashtag':        ('Hashtag',          'dynamic'),
                                                'iteration':      ('Iteration',        'dynamic'),
                                                'kanbanboard':    ('Kanban Board',     'static'),
                                                'priority':       ('Priority',         'dynamic'),
                                                'project':        ('Project',          'static'),
                                                'release':        ('Release',          'dynamic'),
                                                'severity':       ('Severity',         'dynamic'),
                                                'subteam':        ('Subteam',          'dynamic'),
                                                'swimlanes':      ('Swim Lanes',       'dynamic'),
                                                'teammember':     ('Team Member',      'static'),
                                                'type':           ('Type',             'static')}

        # Initialisation Settings
        self.mongodb_host, self.mongodb_port, self.mongodb_username, self.mongodb_password, self.mongodb_bindir = Kanbanara.read_mongodb_ini_file(self)

        # Connect to MongoDB on given host and port
        if self.mongodb_username and self.mongodb_password:
            modified_username = urllib.parse.quote_plus(self.mongodb_username)
            modified_password = urllib.parse.quote_plus(self.mongodb_password)
            connection = MongoClient('mongodb://' + modified_username + ':' + modified_password + '@' +
                                     self.mongodb_host + ':' + str(self.mongodb_port))
        else:
            connection = MongoClient(self.mongodb_host, self.mongodb_port)

        # Connect to 'kanbanara' database, creating if not already exists
        db = connection['kanbanara']

        # Connect to 'projects' collection
        self.projects_collection = db['projects']
        for attribute in ['project']:
            self.projects_collection.create_index(attribute, unique=True, background=True)

        # Connect to 'sessions' collection
        self.sessions_collection = db['sessions']
        for attribute in ['session_id']:
            self.sessions_collection.create_index(attribute, unique=True, background=True)

        for attribute in ['lastaccess']:
            self.sessions_collection.create_index(attribute, unique=False, background=True)

        # Connect to 'members' collection
        self.members_collection = db['members']
        for attribute in ['username']:
            # TODO - username attribute should be unique but get error when unique=true set
            self.members_collection.create_index(attribute, unique=False, background=True)

        # Connect to 'cards' collection
        self.cards_collection = db['cards']
        for attribute in ['id']:
            self.cards_collection.create_index(attribute, unique=True, background=True)

        for attribute in ['description', 'iteration', 'project', 'release', 'state']:
            self.cards_collection.create_index(attribute, unique=False, background=True)

        # TODO - Could I use an ordered dictionary here?
        self.closed_periods = {'1 hour':   datetime.timedelta(hours=1),
                               '3 hours':  datetime.timedelta(hours=3),
                               '6 hours':  datetime.timedelta(hours=6),
                               '9 hours':  datetime.timedelta(hours=9),
                               '12 hours': datetime.timedelta(hours=12),
                               '18 hours': datetime.timedelta(hours=18),
                               '1 day':    datetime.timedelta(days=1),
                               '2 days':   datetime.timedelta(days=2),
                               '3 days':   datetime.timedelta(days=3),
                               '4 days':   datetime.timedelta(days=4),
                               '5 days':   datetime.timedelta(days=5),
                               '6 days':   datetime.timedelta(days=6),
                               '1 week':   datetime.timedelta(days=7),
                               '2 weeks':  datetime.timedelta(days=14),
                               '3 weeks':  datetime.timedelta(days=21),
                               '1 month':  datetime.timedelta(days=28),
                               '2 months': datetime.timedelta(days=56),
                               '3 months': datetime.timedelta(days=84),
                               '4 months': datetime.timedelta(days=112),
                               '5 months': datetime.timedelta(days=140),
                               '6 months': datetime.timedelta(days=182),
                               '9 months': datetime.timedelta(days=274),
                               '1 year':   datetime.timedelta(days=365),
                               '2 years':  datetime.timedelta(days=730),
                               '3 years':  datetime.timedelta(days=1095),
                               '4 years':  datetime.timedelta(days=1460),
                               '5 years':  datetime.timedelta(days=1825)}

    def backup_database(self):
        """comment"""
        (year, month, day) = time.localtime()[0:3]
        year = str(year)
        month = '%02d' % month
        day = '%02d' % day
        folder = os.path.join(self.current_dir, '..', 'dbdump', year+month+day)
        if not os.path.exists(folder):
            os.mkdir(folder)
            port = ""
            if self.mongodb_port != 27017:
                port = ' --port '+str(self.mongodb_port)

            executable = '"'+self.mongodb_bindir+os.sep+'mongodump.exe" --verbose'+port+' --db kanbanara --out '+folder
            (status, output) = Kanbanara.getstatusoutput(executable)
            
    def create_routine_cards(self):
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if 'routine_cards' in member_document:
            projects = self.get_member_projects(member_document)
            card_created = False
            epoch = datetime.datetime.utcnow()
            modified_routine_cards = []
            for routine_card in member_document['routine_cards']:
                if routine_card['project'] in projects:
                    if routine_card['next_action'] > epoch:
                        modified_routine_cards.append(routine_card)
                    else:
                        routine_card['last_action'] = routine_card['next_action']
                        next_action = self.create_routine_card(routine_card)
                        routine_card['next_action'] = next_action
                        modified_routine_cards.append(routine_card)
                        card_created = True
            
            if card_created:
                member_document['routine_cards'] = modified_routine_cards
                self.members_collection.save(member_document)

    def create_routine_card(self, routine_card):
        username = Kanbanara.check_authentication(f'/{self.component}')
        epoch = datetime.datetime.utcnow()
        next_action = epoch
        history = []
        state = routine_card['state']
        for (attribute, value) in [('owner', username),
                                   ('state', state),
                                   ('title', routine_card['title'])]:
            history.append(self.update_card_history(username, {}, attribute, value))

        id = self.get_project_next_card_number(routine_card['project'], 'story')
        card_document = {'creator': username,
                         'hierarchy': "",
                         'history': history,
                         'id': id,
                         'iteration': "",
                         'mode': 'dynamic',
                         'owner': username,
                         'priority': routine_card.get('priority', 'medium'),
                         'project': routine_card['project'],
                         'release': "",
                         'severity': routine_card.get('severity', 'medium'),
                         'state': state,
                         'statehistory': [{'datetime': epoch, 'state': state, 'username': username}],
                         'title': routine_card['title'],
                         'type': 'story'
                        }
        doc_id = self.cards_collection.insert(card_document)
        self.add_recent_activity_entry((epoch, username, doc_id, 'added'))
        self.save_card_as_json(card_document)
        while next_action < epoch:
            next_action = self.calculate_routine_card_next_action(routine_card['next_action'],
                                                                  routine_card['interval'])
            routine_card['next_action'] = next_action

        return next_action
                
    @cherrypy.expose
    def revise_destination_state_and_priority(self, doc_id, destination_state, destination_metastate, priority=""):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        if priority == 'all':
            priority = ""

        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            source_state = card_document.get('state', destination_state)
            _, _, centric, _, _ = self.get_associated_state_information(source_state)
            if centric == 'Owner':
                owner = card_document.get('owner', '')
                coowner = card_document.get('coowner', '')
                if owner and coowner:
                    if username == owner:
                        if destination_state != source_state:
                            card_document['ownerstate'] = destination_state
                        elif 'ownerstate' in card_document:
                            del card_document['ownerstate']

                        destination_state = source_state
                    elif username == coowner:
                        if destination_state != source_state:
                            card_document['coownerstate'] = destination_state
                        elif 'coownerstate' in card_document:
                            del card_document['coownerstate']

                        destination_state = source_state

            elif centric == 'Reviewer':
                reviewer = card_document.get('reviewer', '')
                coreviewer = card_document.get('coreviewer', '')
                if reviewer and coreviewer:
                    if username == reviewer:
                        if destination_state != source_state:
                            card_document['reviewerstate'] = destination_state
                        elif 'reviewerstate' in card_document:
                            del card_document['reviewerstate']

                        destination_state = source_state
                    elif username == coreviewer:
                        if destination_state != source_state:
                            card_document['coreviewerstate'] = destination_state
                        elif 'coreviewerstate' in card_document:
                            del card_document['coreviewerstate']

                        destination_state = source_state

            testcases = card_document.get('testcases', [])
            if destination_metastate in ['acceptancetestingaccepted', 'completed', 'closed']:
                if not self.all_testcases_accepted(testcases):
                    _, _, _, preceding_state, _ = self.get_associated_state_information(destination_state)
                    destination_state = preceding_state

                if 'blocksparent' in card_document:
                    del card_document['blocksparent']

            card_document['statehistory'] = self.append_state_history(card_document['statehistory'], destination_state, username)
            card_document['state'] = destination_state
            if priority:
                card_document['priority'] = priority
            else:
                priority = card_document['priority']

            if destination_metastate == 'closed':
                for attribute in ['expedite', 'nextaction', 'reopened']:
                    if attribute in card_document:
                        del card_document[attribute]

            self.cards_collection.save(card_document)
            self.add_recent_activity_entry((datetime.datetime.utcnow(), username, doc_id, f'moved to {destination_state} {priority}'))
            break

        return destination_state, priority

    @cherrypy.expose
    def dropped_on_step_priority(self, swimlane_no, step_no, step_role, priority, doc_id):
        """Called when card dropped on a particular priority section within a column within a step
           on kanban board"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        workflow = project_document.get('workflow', [])
        step_document = workflow[int(step_no)]
        step_column = step_document[f'{step_role}column']
        column_name = step_column['name']
        state = step_column['state']
        centric = step_column['centric']
        metastate = self.get_corresponding_metastate(project_document, state)
        revised_destination_state, revised_destination_priority = self.revise_destination_state_and_priority(doc_id, state, metastate, priority)
        if state in self.metastates_list:
            metastate = state
        else:
            custom_states = project_document.get('customstates', {})
            metastate = custom_states[state]

        if (metastate in ['unittesting', 'integrationtesting', 'systemtesting', 'acceptancetesting']
                and centric == 'Reviewer' and self.card_to_bypass_review(doc_id)):
            _, _, _, _, next_state = self.get_associated_state_information(state)
            state = next_state

        if priority == 'all':
            priority_list = ['critical', 'high', 'medium', 'low']
        else:
            priority_list = [priority]

        return self.populate_kanban_column(['updatable'], ['updatable', 'minimised'], swimlane_no,
                                           step_no, step_role, column_name, state, metastate,
                                           centric, priority_list)

    def get_kanban_column_heading(self, state, column_name, step_no, step_role, centric,
                                  description, owner_count, reviewer_count, min_wip_limit,
                                  max_wip_limit, post, condensed_column_states, global_wips):
        """comment"""
        content = []
        if self.column_required(column_name):
            if centric == 'Reviewer':
                relevant_owner_or_reviewer_count = reviewer_count
                irrelevant_owner_or_reviewer_count = owner_count
            else:
                relevant_owner_or_reviewer_count = owner_count
                irrelevant_owner_or_reviewer_count = reviewer_count

            if state == 'backlog':
                content.append('<a class="column" href="/backlog/backlogsorter">')

            if step_role == 'buffer':
                if description:
                    content.append(f'<i class="columnname" title="{description}">{column_name}</i>')
                else:
                    content.append('<i class="columnname">'+column_name+'</i>')
            else:
                if description:
                    content.append(f'<b class="columnname" title="{description}">{column_name}</b>')
                else:
                    content.append('<b class="columnname">'+column_name+'</b>')

            if state == 'backlog':
                content.append('</a>')

            if relevant_owner_or_reviewer_count:
                content.append(self.insert_number_of_team_members_active_in_state(state))
                content.append('<br>')
                if 'enforcewiplimits' in global_wips and global_wips['enforcewiplimits']:
                    content.append(post)
                else:
                    if condensed_column_states and state != condensed_column_states[-1]:
                        content.append(f'<sup class="wip">{min_wip_limit}</sup><sup class="lte">&le;</sup>')
                        if max_wip_limit == -1:
                            if relevant_owner_or_reviewer_count < min_wip_limit:
                                content.append(f'<sup class="wipwarning" title="Your Minimum WIP Limit has yet to be Reached">{relevant_owner_or_reviewer_count}</sup>')
                            else:
                                content.append(f'<sup class="wip">{relevant_owner_or_reviewer_count}</sup>')

                            limit_button = '<form action="/kanban/adjust_single_wip_limit" method="post"><input type="hidden" name="state" value="'+state+'"><span class="controlgroup"><input class="wiplimits" type="submit" name="adjustment" value="Limit" title="Switch to Limit Maximum"></span></form>'
                            content.append('<sup class="lte">&le;</sup><sup class="wip">U</sup><br>'+limit_button+post)
                        else:
                            if relevant_owner_or_reviewer_count > max_wip_limit:
                                content.append(f'<sup class="wipwarning" title="Your Maximum WIP Limit has been Exceeded">{relevant_owner_or_reviewer_count}</sup>')
                            elif relevant_owner_or_reviewer_count < min_wip_limit:
                                content.append(f'<sup class="wipwarning" title="Your Minimum WIP Limit has yet to be Reached">{relevant_owner_or_reviewer_count}</sup>')
                            else:
                                content.append(f'<sup class="wip">{relevant_owner_or_reviewer_count}</sup>')

                            decrease_increase_unlimit_buttons = '<form action="/kanban/adjust_single_wip_limit" method="post"><input type="hidden" name="state" value="'+state+'"><span class="controlgroup"><input class="wiplimits" type="submit" name="adjustment" value="-" title="Decrease Maximum Limit"><input class="wiplimits" type="submit" name="adjustment" value="+" title="Increase Maximum Limit"><input class="wiplimits" type="submit" name="adjustment" value="U" title="Switch to Unlimited Maximum"></span></form>'
                            content.append(f'<sup class="lte">&le;</sup><sup class="wip">{max_wip_limit}</sup><br>{decrease_increase_unlimit_buttons}{post}')

                    else:
                        content.append('<form action="/kanban/adjust_single_wip_limit" method="post"><input type="hidden" name="state" value="'+state+'">')
                        content.append(self.create_html_select_block('adjustment', self.potential_closed_periods, onchange='this.form.submit()', default=None, current_value=max_wip_limit))
                        content.append('</form> | '+post)

            elif irrelevant_owner_or_reviewer_count:
                content.append('<br><sup title="For Your Information"><i>[FYI]</i></sup><br>'+post)
            else:
                content.append('<span class="ui-icon ui-icon-info" title="There are no cards in the '+column_name+' column!" />')

        dummy = self.generate_drop_js_script('-1', step_no, step_role, 'header')
        content.append(f'<script type="text/javascript" src="/kanban/scripts/autogenerated/swimlane-1step{step_no}{step_role}header.js"></script>')
        return "".join(content)

    def insert_number_of_team_members_active_in_state(self, state):
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        content = []
        teammember = member_document.get('teammember', '')
        if not teammember:
            projects = self.get_member_projects(member_document)
            _, _, centric, _, _ = self.get_associated_state_information(state)
            if centric == 'Reviewer':
                reviewer_distincts = self.cards_collection.find({'project': {'$in': projects},
                                                                'reviewer': {'$nin': [[], '', None]},
                                                                'state': state}).distinct('reviewer')
                coreviewer_distincts = self.cards_collection.find({'project': {'$in': projects},
                                                                  'coreviewer': {'$nin': [[], '', None]},
                                                                  'state': state}).distinct('coreviewer')
                number_of_active_team_members = len(list(set(reviewer_distincts + coreviewer_distincts)))
            else:
                owner_distincts = self.cards_collection.find({'project': {'$in': projects},
                                                             'owner': {'$nin': [[], '', None]},
                                                             'state': state}).distinct('owner')
                coowner_distincts = self.cards_collection.find({'project': {'$in': projects},
                                                               'coowner': {'$nin': [[], '', None]},
                                                               'state': state}).distinct('coowner')
                number_of_active_team_members = len(list(set(owner_distincts + coowner_distincts)))

            if number_of_active_team_members == 1:
                content.append(f' <sup class="active" title="{number_of_active_team_members} team member is currently active in the {state.capitalize()} state!">{number_of_active_team_members}</sup>')
            else:
                content.append(f' <sup class="active" title="{number_of_active_team_members} team members are currently active in the {state.capitalize()} state!">{number_of_active_team_members}</sup>')

        return "".join(content)

    def delete_outofdate_sessions(self):
        epoch = datetime.datetime.utcnow()
        for session_document in self.sessions_collection.find({'lastaccess': {'$lt': epoch-self.timedelta_day}}):
            self.sessions_collection.remove({'_id': ObjectId(session_document['_id'])})

    def remove_old_broadcast_messages(self):
        epoch = datetime.datetime.utcnow()
        for card_document in self.cards_collection.find({'lastchanged': {'$lt': epoch-self.timedelta_day},
                                                         'broadcast': {'$nin': ['', [], None]}
                                                        }):
            del card_document['broadcast']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

    def recur_closed_cards(self):
        for closed_card_document in self.cards_collection.find({'state': 'closed',
                                                                'recurring': {'$exists': True}}):
            epoch = datetime.datetime.utcnow()
            project = closed_card_document.get('project', '')
            card_type = closed_card_document.get('type', 'story')
            id = self.get_project_next_card_number(project, card_type)
            new_card_document = {'id':          id,
                                 'title':       closed_card_document.get('title', ''),
                                 'project':     project,
                                 'state':       'backlog',
                                 'type':        card_type,
                                 'creator':     closed_card_document.get('owner', ''),
                                 'owner':       closed_card_document.get('owner', ''),
                                 'lastchanged': epoch,
                                 'lasttouched': epoch,
                                 'priority':    closed_card_document.get('priority', 'medium'),
                                 'severity':    closed_card_document.get('severity', 'medium'),
                                 'recurring':   True
                                }
            self.cards_collection.insert(new_card_document)
            del closed_card_document['recurring']
            self.cards_collection.save(closed_card_document)

    def correct_legacy_database_attributes(self):
        for member_document in self.members_collection.find():

            # Added 2017-04-21
            for attribute in ['absencestartdate', 'absenceenddate']:
                if attribute in member_document and isinstance(member_document[attribute], int):
                    member_document[attribute] = datetime.datetime.fromtimestamp(member_document[attribute])
                    self.members_collection.save(member_document)
                    self.save_member_as_json(member_document)

            # Added 22-09-2016
            for attribute in ["closedwip", "committee", 'filterbarcomponent', 'states', "swinlanes"]:
                if attribute in member_document:
                    del member_document[attribute]
                    self.members_collection.save(member_document)
                    self.save_member_as_json(member_document)

            # Remove non-dictionary entries from a members list of projects
            # Added 20-05-2016
            if 'projects' in member_document and member_document['projects']:
                revised_projects = []
                for project_document in member_document['projects']:
                    if 'project' in project_document and project_document['project']:
                        revised_projects.append(project_document)

                member_document['projects'] = revised_projects
                self.members_collection.save(member_document)
                self.save_member_as_json(member_document)

        # non-min/max state+wip attributes have been removed from member
        # Added 25-01-2016
        for attribute in ['backlog', 'defined', 'analysis', 'analysed', 'development', 'developed', 'testing', 'accepted']:

            # Added 2016-09-01
            for minmax in ['','min','max']:
                for member_document in self.members_collection.find({attribute+minmax+'wip': {'$exists': True}}):
                    del member_document[attribute+minmax+'wip']
                    self.members_collection.save(member_document)
                    self.save_member_as_json(member_document)

            for project_document in self.projects_collection.find({attribute+'wip': {'$exists': True}}):
                del project_document[attribute+'wip']
                self.projects_collection.save(project_document)
                self.save_project_as_json(project_document)

        # Remove <step/state>displayname attributes in project now that workflow attribute has been added.
        # Added 15-08-2016
        for attribute in ['backlog', 'defined', 'analysis', 'analysed', 'development', 'developed', 'testing', 'accepted', 'closed', 'specify', 'implement', 'validate']:
            for project_document in self.projects_collection.find({attribute+'displayname': {'$exists': True}}):
                del project_document[attribute+'displayname']
                self.projects_collection.save(project_document)
                self.save_project_as_json(project_document)


        # Added 2017-04-19
        # Change timestamp to datetime object
        for card_document in self.cards_collection.find():
            attribute_altered = False
            for attribute in ['blockeduntil', 'deadline', 'deferreduntil', 'focusstart', 'lastchanged', 'lasttouched', 'nextaction', 'startby']:
                if attribute in card_document and isinstance(card_document[attribute], int):
                    card_document[attribute] = datetime.datetime.fromtimestamp(card_document[attribute])
                    attribute_altered = True

            if attribute_altered:
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

        # remove card where project is not set
        for card_document in self.cards_collection.find({'project': {'$exists': False}}):
            self.cards_collection.remove({'_id': ObjectId(card_document['_id'])})

        # Added 2017-04-21
        for attribute in ['history', 'actualcosthistory', 'actualtimehistory', 'blockedhistory', 'estimatedcosthistory',
                          'estimatedtimehistory', 'statehistory']:
            for card_document in self.cards_collection.find({attribute: {'$exists': True}}):
                revised_history = []
                updated = False
                for history_document in card_document[attribute]:
                    for subattribute in ['epoch', 'datetime']:
                        if subattribute in history_document and isinstance(history_document[subattribute], int):
                            try:
                                history_document[subattribute] = datetime.datetime.fromtimestamp(history_document[subattribute])
                                updated = True
                            except:
                                True

                    revised_history.append(history_document)

                if updated:
                    card_document[attribute] = revised_history
                    self.cards_collection.save(card_document)
                    self.save_card_as_json(card_document)

        # Added 2017-04-21
        for attribute in ['history', 'actualcosthistory', 'actualtimehistory', 'blockedhistory', 'estimatedcosthistory',
                          'estimatedtimehistory', 'statehistory']:
            for card_document in self.cards_collection.find({attribute: {'$exists': True}}):
                revised_history = []
                updated = False
                for history_document in card_document[attribute]:
                    if 'epoch' in history_document:
                        history_document['datetime'] = history_document['epoch']
                        del history_document['epoch']
                        updated = True

                    revised_history.append(history_document)

                if updated:
                    card_document[attribute] = revised_history
                    self.cards_collection.save(card_document)
                    self.save_card_as_json(card_document)

        # Added 2017-04-20
        for card_document in self.cards_collection.find({'focushistory': {'$exists': True}}):
            revised_focus_history = []
            updated = False
            for focus_history_document in card_document['focushistory']:
                for attribute in ['focusstart', 'focusend']:
                    if attribute in focus_history_document and isinstance(focus_history_document[attribute], int):
                        try:
                            focus_history_document[attribute] = datetime.datetime.fromtimestamp(focus_history_document[attribute])
                            updated = True
                        except:
                            True

                revised_focus_history.append(focus_history_document)

            if updated:
                card_document['focushistory'] = revised_focus_history
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

        # lastchanged not set
        for card_document in self.cards_collection.find({'lastchanged': {'$exists': False}}):
            card_document['lastchanged'] = datetime.datetime.utcnow()
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Added 13-02-2017
        # Change state=testing to state=acceptancetesting
        for card_document in self.cards_collection.find({'state': 'acceptance_testing'}):
            card_document['state'] = 'acceptancetesting'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Added 13-02-2017
        # Change state=accepted to state=acceptancetestingaccepted
        for card_document in self.cards_collection.find({'state': 'acceptance_testing_accepted'}):
            card_document['state'] = 'acceptancetestingaccepted'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Added 13-02-2017
        # Change state=testing to state=acceptancetesting
        for card_document in self.cards_collection.find({'state': 'testing'}):
            card_document['state'] = 'acceptancetesting'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Added 13-02-2017
        # Change state=accepted to state=acceptancetestingaccepted
        for card_document in self.cards_collection.find({'state': 'accepted'}):
            card_document['state'] = 'acceptancetestingaccepted'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # type not set
        for card_document in self.cards_collection.find({'type': {'$exists': False}}):
            card_document['type'] = 'story'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # state attributes have been removed
        for attribute in ['backlog', 'defined', 'analysis', 'analysed', 'development', 'developed', 'testing', 'accepted', 'closed']:
            for card_document in self.cards_collection.find({attribute: {'$exists': True}}):
                del card_document[attribute]
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

        # creator, owner, coowner, reviewer, coreviewer and lastchangedby values have been changed from fullname to username
        for attribute in ['creator', 'owner', 'coowner', 'reviewer', 'coreviewer', 'lastchangedby']:
            for card_document in self.cards_collection.find({attribute: {'$exists': True}}):
                member_document = self.members_collection.find_one({'fullname': card_document[attribute]})
                if member_document:
                    card_document[attribute] = member_document['username']
                    self.cards_collection.save(card_document)
                    self.save_card_as_json(card_document)

        # previousstate has been removed
        for card_document in self.cards_collection.find({'previousstate': {'$exists': True}}):
            del card_document['previousstate']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # hierarchy has been added
        for card_document in self.cards_collection.find({'hierarchy': {'$exists': False}}):
            card_document['hierarchy'] = self.assemble_card_hierarchy(card_document['id'])
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # forwardcount has been removed
        for card_document in self.cards_collection.find({'forwardcount': {'$exists': True}}):
            del card_document['forwardcount']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # backwardcount has been removed
        for card_document in self.cards_collection.find({'backwardcount': {'$exists': True}}):
            del card_document['backwardcount']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Older cards will not have a statehistory attribute
        for card_document in self.cards_collection.find({'statehistory': {'$exists': False}}):
            card_document['statehistory'] = []
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Older cards will not have a severity attribute
        for card_document in self.cards_collection.find({'severity': {'$exists': False}}):
            card_document['severity'] = 'medium'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # help is now in the session_document only
        # wip<state> has been replaced by <state>wip
        for attribute in ['help', 'wipaccepted', 'wipanalysed', 'wipanalysis', 'wipbacklog', 'wipclosed', 'wipdefined', 'wipdevelopment', 'wipdeveloped', 'wiptesting']:
            for member_document in self.members_collection.find({attribute: {'$exists': True}}):
                del member_document[attribute]
                self.members_collection.save(member_document)
                self.save_member_as_json(member_document)

        # timesheet has been replaced by history
        for card_document in self.cards_collection.find({'timesheet': {'$exists': True}}):
            del card_document['timesheet']
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # rfe has been replaced by enhancement
        for card_document in self.cards_collection.find({'type':'rfe'}):
            card_document['type'] = 'enhancement'
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

        # Added 2017-04-19
        for card_document in self.cards_collection.find({'rules': {'$exists': True}}):
            revised_rules = []
            updated = False
            for rule_document in card_document['rules']:
                if 'lasttriggered' in rule_document and isinstance(rule_document['lasttriggered'], int):
                    try:
                        rule_document['lasttriggered'] = datetime.datetime.fromtimestamp(rule_document['lasttriggered'])
                        updated = True
                    except:
                        True

                revised_rules.append(rule_document)

            if updated:
                card_document['rules'] = revised_rules
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

        # Added 2017-04-19
        for project_document in self.projects_collection.find({'releases': {'$exists': True}}):
            revised_releases = []
            updated = False
            for release_document in project_document['releases']:
                for attribute in ['start_date', 'end_date']:
                    if attribute in release_document and isinstance(release_document[attribute], int):
                        release_document[attribute] = datetime.datetime.fromtimestamp(release_document[attribute])
                        updated = True

                revised_releases.append(release_document)

            if updated:
                project_document['releases'] = revised_releases
                self.projects_collection.save(project_document)
                self.save_project_as_json(project_document)

        # Added 18-08-2016
        for project_document in self.projects_collection.find({'statemappings': {'$exists': True}}):
            custom_states = project_document['statemappings']
            for custom_state in custom_states:
                if custom_state in self.metastates_list:
                    del custom_states[custom_state]
                    project_document['statemappings'] = custom_states
                    self.projects_collection.save(project_document)
                    self.save_project_as_json(project_document)
                    break

        # Added 26-10-2016
        for project_document in self.projects_collection.find({'nextrecordnumber': {'$exists': True}}):
            project_document['nextcardnumber'] = project_document['nextrecordnumber']
            del project_document['nextrecordnumber']
            self.projects_collection.save(project_document)
            self.save_project_as_json(project_document)

        # Added 18-08-2016
        for project_document in self.projects_collection.find({'statemappings': {'$exists': True}}):
            project_document['customstates'] = project_document['statemappings']
            del project_document['statemappings']
            self.projects_collection.save(project_document)
            self.save_project_as_json(project_document)

        # committees has been replaced by subteams
        # Added 2016-08-06
        for project_document in self.projects_collection.find({'committees': {'$exists': True}}):
            del project_document['committees']
            self.projects_collection.save(project_document)
            self.save_project_as_json(project_document)

        # Added 2016-10-14
        for project_document in self.projects_collection.find({'project_normalised': {'$exists': False}}):
            project_document['project_normalised'] = project_document['project'].lower()
            self.projects_collection.save(project_document)
            self.save_project_as_json(project_document)

    def housekeeping(self):
        self.correct_legacy_database_attributes()
        if (not self.housekeeping_last_done or
            self.housekeeping_last_done < datetime.datetime.utcnow() - self.timedelta_hour):
            self.delete_outofdate_sessions()
            self.tidy_owner_coowner_states()
            self.create_routine_cards()
            self.tidy_reviewer_coreviewer_states()
            self.remove_expired_hiddenuntil_dates()
            self.remove_old_broadcast_messages()
            self.tidy_svgcharts_folder()
            self.remove_duplicate_member_documents()
            self.remove_expired_blockeduntil_dates()
            self.remove_expired_deferreduntil_dates()
            self.recur_closed_cards()
            self.backup_database()
            #self.synchronise_projects()
            self.housekeeping_last_done = datetime.datetime.utcnow()

    def execute_card_rules(self, card_document, mode=""):
        if card_document.get('rules', []):
            epoch = datetime.datetime.utcnow()
            revised_rules = []
            for rule_document in card_document['rules']:
                if not 'lasttriggered' in rule_document:
                    rule_document['lasttriggered'] = epoch-self.timedelta_hour

                last_triggered = rule_document['lasttriggered']
                    
                if not 'usage' in rule_document:
                    rule_document['usage'] = 'Perpetual'

                if rule_document['status'] == 'Valid' and ((last_triggered >= epoch-self.timedelta_hour) or mode == 'forced'):
                    rule_document['lasttriggered'] = epoch
                    components = rule_document['components']
                    inside_condition_block = False
                    inside_success_action_block = False
                    inside_failure_action_block = False
                    condition_block_matches = []
                    no_of_actions_required = 0
                    action_attribute_value_pairs = []
                    for component_no in range(0, len(components), 4):
                        if components[component_no] == 'if':
                            inside_condition_block = True
                        elif components[component_no] == 'then':
                            if inside_condition_block:
                                inside_condition_block = False
                                inside_success_action_block    = True

                        elif components[component_no] == 'else':
                            if inside_success_action_block:
                                inside_success_action_block = False
                                inside_failure_action_block = True

                        other_card_id = ""
                        other_card_project = ""
                        attribute = components[component_no+1]
                        if '.' in attribute:
                            [other_card_id, attribute] = attribute.split('.')
                            [other_card_project, _] = other_card_id.split('-')

                        operand   = components[component_no+2]
                        value     = components[component_no+3]
                        if value.startswith("'''") and value.endswith("'''"):
                            value = ast.literal_eval(value)
                        elif value.startswith('"""') and value.endswith('"""'):
                            value = ast.literal_eval(value)
                        elif value.startswith("'") and value.endswith("'"):
                            value = ast.literal_eval(value)
                            value = value.replace("\\'", "'")
                        elif value.startswith('"') and value.endswith('"'):
                            value = ast.literal_eval(value)
                            value = value.replace('\\"', '"')
                        elif value.isdigit():
                            value = int(value)
                        elif self.isfloat(value):
                            value = float(value)
                        elif value.startswith("[") and value.endswith("]"):
                            value = ast.literal_eval(value)
                        elif value.lower() in ['true', 'false', 'enabled', 'disabled']:
                            if value in ['true', 'enabled']:
                                value = True
                            else:
                                value = False

                        if inside_condition_block:
                            if components[component_no] == 'or' and not all(condition_block_matches):
                                condition_block_matches = []

                            if attribute in self.rule_condition_allowable_card_attributes:
                                if other_card_id:
                                    if other_card_project == card_document['project']:
                                        if self.card_attribute_matches(other_card_id, attribute, operand, value):
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    else:
                                        condition_block_matches.append(False)

                                elif attribute in card_document:
                                    if operand == '=':
                                        if card_document[attribute] == value:
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == 'is':
                                        if value == 'undefined':
                                            if attribute not in card_document:
                                                condition_block_matches.append(True)
                                            else:
                                                condition_block_matches.append(False)

                                        elif value == 'defined':
                                            if attribute in card_document:
                                                condition_block_matches.append(True)
                                            else:
                                                condition_block_matches.append(False)

                                        elif value == 'populated':
                                            if attribute in card_document and card_document[attribute]:
                                                condition_block_matches.append(True)
                                            else:
                                                condition_block_matches.append(False)

                                        elif value == 'unpopulated':
                                            if attribute in card_document and not card_document[attribute]:
                                                condition_block_matches.append(True)
                                            else:
                                                condition_block_matches.append(False)

                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == 'in':
                                        if isinstance(value, (list, str)):
                                            if card_document[attribute] in value:
                                                condition_block_matches.append(True)
                                            else:
                                                condition_block_matches.append(False)

                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == 'nin':
                                        if isinstance(value, (list, str)):
                                            if card_document[attribute] not in value:
                                                condition_block_matches.append(True)
                                            else:
                                                condition_block_matches.append(False)

                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == '!=':
                                        if card_document[attribute] != value:
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == '<':
                                        if card_document[attribute] < value:
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == '<=':
                                        if card_document[attribute] <= value:
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == '>':
                                        if card_document[attribute] > value:
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    elif operand == '>=':
                                        if card_document[attribute] >= value:
                                            condition_block_matches.append(True)
                                        else:
                                            condition_block_matches.append(False)

                                    else:
                                        condition_block_matches.append(False)

                                else:
                                    condition_block_matches.append(False)

                            else:
                                condition_block_matches.append(False)

                        elif inside_success_action_block and all(condition_block_matches):
                            no_of_actions_required += 1
                            if components[component_no] in ['then', 'and']:
                                if attribute in self.rule_action_allowable_card_attributes:
                                    if other_card_id:
                                        if other_card_project == card_document['project']:
                                            if isinstance(value, (str, float, int, bool)):
                                                action_attribute_value_pairs.append((other_card_id, attribute, value))

                                    elif operand == '=':
                                        if isinstance(value, (bool, float, int, str)):
                                            action_attribute_value_pairs.append(('', attribute, value))

                        elif inside_failure_action_block and not all(condition_block_matches):
                            no_of_actions_required += 1
                            if components[component_no] in ['else', 'and']:
                                if attribute in self.rule_action_allowable_card_attributes:
                                    if other_card_id:
                                        if other_card_project == card_document['project']:
                                            if isinstance(value, (bool, float, int, str)):
                                                action_attribute_value_pairs.append((other_card_id, attribute, value))

                                    elif operand == '=':
                                        if isinstance(value, (bool, float, int, str)):
                                            action_attribute_value_pairs.append(('', attribute, value))

                else:
                    no_of_actions_required = 0

                no_of_actions_performed = 0
                if no_of_actions_required and no_of_actions_required == len(action_attribute_value_pairs):
                    for (other_card_id, attribute, value) in action_attribute_value_pairs:
                        if other_card_id:
                            for other_card_document in self.cards_collection.find({"id": other_card_id}):
                                other_card_document[attribute] = value
                                self.cards_collection.save(other_card_document)
                                no_of_actions_performed += 1
                                break

                        else:
                           card_document[attribute] = value
                           no_of_actions_performed += 1

                    if all(condition_block_matches) and no_of_actions_performed == no_of_actions_required:
                        rule_document['status'] = 'Applied'

                if rule_document['status'] == 'Applied' and rule_document['usage'] == 'Transient':
                    continue
                else:
                    revised_rules.append(rule_document)

            card_document['rules'] = revised_rules
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)

    def card_attribute_matches(self, id, attribute, operand, value):
        status = False
        for card_document in self.cards_collection.find({"id": id}):
            current_value = card_document.get(attribute,'')
            if operand == '=':
                if current_value == value:
                    return True

            elif operand == '>':
                if current_value > value:
                    return True

            elif operand == '>=':
                if current_value >= value:
                    return True

            elif operand == '<':
                if current_value < value:
                    return True

            elif operand == '<=':
                if current_value <= value:
                    return True

            break

        return status

    @cherrypy.expose
    def index(self, projection="0", debug=""):
        """Displays the kanban board"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        cherrypy.session.acquire_lock()
        self.user_kanban_board_settings[username] = {'loaded_js_files': []}
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        projection = int(projection)
        if projection:
            self.user_kanban_board_settings[username]['projection'] = projection

        if debug:
            if debug in ['on', '1']:
                session_document['debug'] = True
            else:
                session_document['debug'] = False

            self.sessions_collection.save(session_document)

        member_document = Kanbanara.get_member_document(self, session_document)
        self.housekeeping()
        content = []
        content.append(Kanbanara.header(self, 'index', 'Index'))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        if projection:
            day_labels = self.get_projection_day_labels()
            content.append('<p class="projection">')
            content.append(f'<a href="/kanban/index?projection={projection-1}"><span class="fa fa-arrow-circle-left fa-lg" title="Back 1 Day"></span></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
            content.append('Future Projection: '+day_labels[projection])
            if projection < 6:
                content.append(f'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="/kanban/index?projection={projection+1}"><span class="fa fa-arrow-circle-right fa-lg" title="Forward 1 Day"></span></a>')

            content.append('</p>')

        required_columns, required_states = self.get_displayable_columns()
        project = member_document.get('project', '')
        if project and self.projects_collection.find({'project': project}).count():
            project_document = self.projects_collection.find_one({'project': project})
            self.tidy_project_announcements(project_document)

            workflow = project_document.get('workflow', [])
            workflow_index = project_document.get('workflow_index', {})

            buffer_column_states = workflow_index.get('buffer_column_states', [])
            condensed_column_names = workflow_index.get('condensed_column_names', [])
            condensed_column_states = workflow_index.get('condensed_column_states', [])

            for card_document in self.cards_collection.find({'project': project,
                                                            'state': {'$in': condensed_column_states},
                                                            'rules': {'$nin': [[], None]}}):
                self.execute_card_rules(card_document, '')

            disowned_card_doc_ids = self.cards_collection.find({'project': project,
                                                               'state': {'$nin': condensed_column_states}}).distinct('_id')
            project_document = self.projects_collection.find_one({'project': project})
            expedite = False
            if self.cards_collection.find({'project': project, 'expedite': True}).count():
                expedite = True
                
            deferred = False
            if self.cards_collection.find({'project': project, 'deferred': {'$nin': ["", None]}}).count():
                deferred = True

            if not workflow:
                content.append('<div id="noworkflow"><table><tr><th>Welcome to "'+project+'" Project!</th></tr><tr><td><h2>Create Your Workflow</h2><p>Click on the <b>Projects -&gt; Update</b> menu option followed by the <b>Workflow</b> tab</p></td></tr></table></div>')

            content.append('<div id="kanbanboardcontainer"><table id="kanbanboard"><thead><tr>')

            if disowned_card_doc_ids:
                content.append('<th id="disownedstep">Disowned By Workflow!</th>')

            for step_document in workflow:
                step_name = step_document['step']
                number_of_columns = 0
                state = ""
                for column in ['maincolumn', 'counterpartcolumn', 'buffercolumn']:
                    if column in step_document:
                        number_of_columns += 1
                        if not state:
                            variable_column = step_document[column]
                            state = variable_column['state']
                            if state not in self.metastates_list:
                                custom_states = project_document.get('customstates', {})
                                if state in custom_states:
                                    state = custom_states[state]

                if number_of_columns:
                    if number_of_columns > 1:
                        content.append(f'<th id="{state}step" colspan="{number_of_columns}">{step_name}</th>')
                    else:
                        content.append(f'<th id="{state}step">{step_name}</th>')

            content.append('</tr>')

            content.append('<tr>')
            if disowned_card_doc_ids:
                if len(disowned_card_doc_ids) > 1:
                    content.append(f'<th id="disownedcolumnheader">The following {len(disowned_card_doc_ids)} Cards are currently assigned to one or more States no longer in your Workflow!</th>')
                else:
                    content.append('<th id="disownedcolumnheader">The following Card is currently assigned to a State no longer in your Workflow!</th>')

            for step_no, step_document in enumerate(workflow):
                if 'maincolumn' in step_document:
                    main_column = step_document['maincolumn']
                    name = main_column['name']
                    state = main_column['state']
                    if state not in self.metastates_list:
                        custom_states = project_document.get('customstates', {})
                        if state in custom_states:
                            state = custom_states[state]

                    content.append(f'<th id="{state}columnheader"><div id="swimlane-1step{step_no}mainheader"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></th>')

                if 'counterpartcolumn' in step_document:
                    counterpart_column = step_document['counterpartcolumn']
                    name = counterpart_column['name']
                    state = counterpart_column['state']
                    if state not in self.metastates_list:
                        custom_states = project_document.get('customstates', {})
                        if state in custom_states:
                            state = custom_states[state]

                    content.append(f'<th id="{state}columnheader"><div id="swimlane-1step{step_no}counterpartheader"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></th>')

                if 'buffercolumn' in step_document:
                    buffer_column = step_document['buffercolumn']
                    name = buffer_column['name']
                    state = buffer_column['state']
                    if state not in self.metastates_list:
                        custom_states = project_document.get('customstates', {})
                        if state in custom_states:
                            state = custom_states[state]

                    content.append(f'<th id="{state}columnheader"><div id="swimlane-1step{step_no}bufferheader"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></th>')

            content.append('</tr></thead>')

            # ENTRY CRITERIA ROW
            content.append('<tbody><tr>')
            if disowned_card_doc_ids:
                content.append('<td></td>')

            for step_no, step_document in enumerate(workflow):
                if 'maincolumn' in step_document:
                    content.append(f'<td><div id="step{step_no}mainentrycriteria"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                if 'counterpartcolumn' in step_document:
                    content.append(f'<td><div id="step{step_no}counterpartentrycriteria"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                if 'buffercolumn' in step_document:
                    content.append(f'<td><div id="step{step_no}bufferentrycriteria"><!-- This section is automatically populated by JQuery and Ajax -->  <p>Please enable Javascript!</p></div></td>')

            content.append('</tr>')

            # EXPEDITE ROW
            if expedite:
                content.append('<tr class="expedite">')

                if disowned_card_doc_ids:
                    content.append('<td></td>')

                for step_no, step_document in enumerate(workflow):
                    if 'maincolumn' in step_document:
                        main_column = step_document['maincolumn']
                        state = main_column['state']
                        centric = main_column['centric']
                        if state == condensed_column_states[-1]:
                            content.append('<td></td>')
                        else:
                            content.append(f'<td class="{centric.lower()}maincolumn" class="expedite"><div id="swimlane-1step{step_no}mainexpedite"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                    if 'counterpartcolumn' in step_document:
                        counterpart_column = step_document['counterpartcolumn']
                        state = counterpart_column['state']
                        centric = counterpart_column['centric']
                        if state == condensed_column_states[-1]:
                            content.append('<td></td>')
                        else:
                            content.append(f'<td class="{centric.lower()}maincolumn" class="expedite"><div id="swimlane-1step{step_no}counterpartexpedite"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                    if 'buffercolumn' in step_document:
                        buffer_column = step_document['buffercolumn']
                        state = buffer_column['state']
                        content.append(f'<td></td>')

                content.append('</tr>')

            content.append('<tr>')
            swimlanes = []
            if 'swimlanes' in member_document and member_document['swimlanes']:
                swimlane_display_name = ""
                for (selectable_swim_lanes_displayname, selectable_swim_lanes_attribute) in self.swim_lanes_attributes:
                    if selectable_swim_lanes_attribute == member_document['swimlanes']:
                        self.user_kanban_board_settings[username]['swimlane_attribute'] = selectable_swim_lanes_attribute
                        swimlane_display_name = selectable_swim_lanes_displayname
                        break

                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                swimlanes = self.cards_collection.find(owner_reviewer_search_criteria).distinct(member_document['swimlanes'])

            if swimlanes:
                self.user_kanban_board_settings[username]['swimlane_values'] = swimlanes
                for swimlane_no, swimlane in enumerate(swimlanes):
                    content.append(f'<tr class="swimlaneheader"><td colspan="{len(condensed_column_names)}">Swimlane {swimlane_no}: {swimlane_display_name} = {swimlane}</td></tr><tr class="swimlane">')

                    if disowned_card_doc_ids:
                        content.append(f'<td><div id="swimlane{swimlane_no}step-1bufferall"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                    for step_no, step_document in enumerate(workflow):
                        if 'maincolumn' in step_document:
                            main_column = step_document['maincolumn']
                            state = main_column['state']
                            centric = main_column['centric']
                            content.append('<td class="{centric.lower()}maincolumn">')
                            if state == condensed_column_states[-1]:
                                content.append(f'<div id="swimlane{swimlane_no}step{step_no}mainall"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')
                            else:
                                for priority in ['critical', 'high', 'medium', 'low']:
                                    content.append(f'<div id="swimlane{swimlane_no}step{step_no}main'+priority+'"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                                if state != condensed_column_states[0]:
                                    state_pos = condensed_column_states.index(state)
                                    previous_state = condensed_column_states[state_pos-1]
                                    if previous_state in buffer_column_states:
                                        content.append(f'<div id="swimlane{swimlane_no}step{step_no}mainpull"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                                for special in ['waiting', 'blocked', 'deferred', 'ghosted']:
                                    if special == 'deferred' and not deferred:
                                        continue
                                
                                    content.append(f'<div id="swimlane{swimlane_no}step{step_no}main{special}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            content.append('</td>')

                        if 'counterpartcolumn' in step_document:
                            counterpart_column = step_document['counterpartcolumn']
                            state = counterpart_column['state']
                            centric = counterpart_column['centric']
                            content.append('<td class="{centric.lower()}maincolumn">')
                            if state == condensed_column_states[-1]:
                                content.append(f'<div id="swimlane{swimlane_no}step{step_no}counterpartall"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')
                            else:
                                for priority in ['critical', 'high', 'medium', 'low']:
                                    content.append(f'<div id="swimlane{swimlane_no}step{step_no}counterpart'+priority+'"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                                if state != condensed_column_states[0]:
                                    state_pos = condensed_column_states.index(state)
                                    previous_state = condensed_column_states[state_pos-1]
                                    if previous_state in buffer_column_states:
                                        content.append(f'<div id="swimlane{swimlane_no}step{step_no}counterpartpull"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                                for special in ['waiting', 'blocked', 'deferred', 'ghosted']:
                                    if special == 'deferred' and not deferred:
                                        continue
                                
                                    content.append(f'<div id="swimlane{swimlane_no}step{step_no}counterpart{special}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            content.append('</td>')

                        if 'buffercolumn' in step_document:
                            buffer_column = step_document['buffercolumn']
                            state = buffer_column['state']
                            centric = buffer_column['centric']
                            content.append(f'<td class="{centric.lower()}buffercolumn">')
                            for priority_or_special in ['critical', 'high', 'medium', 'low', 'waiting', 'blocked', 'deferred', 'ghosted']:
                                if priority_or_special == 'deferred' and not deferred:
                                    continue
                            
                                content.append(f'<div id="swimlane{swimlane_no}step{step_no}buffer{priority_or_special}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            content.append('</td>')

                    content.append('</tr>')

                content.append('<script type="text/javascript" src="/kanban/scripts/swimlanes.js"></script>')
            else:
                if disowned_card_doc_ids:
                    content.append('<td><div id="swimlane0step-1bufferall"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                for step_no, step_document in enumerate(workflow):
                    if 'maincolumn' in step_document:
                        main_column = step_document['maincolumn']
                        state = main_column['state']
                        centric = main_column['centric']
                        content.append(f'<td class="{centric.lower()}maincolumn">')
                        if condensed_column_states and state == condensed_column_states[-1]:
                            content.append(f'<div id="swimlane0step{step_no}mainall"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')
                        else:
                            for priority in ['critical', 'high', 'medium', 'low']:
                                content.append(f'<div id="swimlane0step{step_no}main{priority}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            if condensed_column_states and state != condensed_column_states[0]:
                                state_pos = condensed_column_states.index(state)
                                previous_state = condensed_column_states[state_pos-1]
                                if previous_state in buffer_column_states:
                                    content.append(f'<div id="swimlane0step{step_no}mainpull"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            for special in ['waiting', 'blocked', 'deferred', 'ghosted']:
                                if special == 'deferred' and not deferred:
                                    continue
                            
                                content.append(f'<div id="swimlane0step{step_no}main{special}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                        content.append('</td>')

                    if 'counterpartcolumn' in step_document:
                        counterpart_column = step_document['counterpartcolumn']
                        state = counterpart_column['state']
                        centric = counterpart_column['centric']
                        content.append(f'<td class="{centric.lower()}maincolumn">')
                        if condensed_column_states and state == condensed_column_states[-1]:
                            content.append(f'<div id="swimlane0step{step_no}counterpartall"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')
                        else:
                            for priority in ['critical', 'high', 'medium', 'low']:
                                content.append(f'<div id="swimlane0step{step_no}counterpart{priority}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            if condensed_column_states and state != condensed_column_states[0]:
                                state_pos = condensed_column_states.index(state)
                                previous_state = condensed_column_states[state_pos-1]
                                if previous_state in buffer_column_states:
                                    content.append(f'<div id="swimlane0step{step_no}counterpartpull"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                            for special in ['waiting', 'blocked', 'deferred', 'ghosted']:
                                if special == 'deferred' and not deferred:
                                    continue
                            
                                content.append(f'<div id="swimlane0step{step_no}counterpart{special}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                        content.append('</td>')

                    if 'buffercolumn' in step_document:
                        buffer_column = step_document['buffercolumn']
                        state = buffer_column['state']
                        centric = buffer_column['centric']
                        content.append(f'<td class="{centric.lower()}buffercolumn">')
                        for priority_or_special in ['critical', 'high', 'medium', 'low', 'waiting', 'blocked', 'deferred',
                                                    'ghosted']:
                            if priority_or_special == 'deferred' and not deferred:
                                continue
                                                    
                            content.append(f'<div id="swimlane0step{step_no}buffer{priority_or_special}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div>')

                        content.append('</td>')

                content.append('</tr>')

            # EXIT CRITERIA ROW
            content.append('<tr>')
            if disowned_card_doc_ids:
                content.append('<td></td>')

            for step_no, step_document in enumerate(workflow):
                if 'maincolumn' in step_document:
                    main_column = step_document['maincolumn']
                    state = main_column['state']
                    if condensed_column_states and state == condensed_column_states[-1]:
                        content.append('<td></td>')
                    else:
                        content.append(f'<td><div id="step{step_no}mainexitcriteria"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                if 'counterpartcolumn' in step_document:
                    counterpart_column = step_document['counterpartcolumn']
                    state = counterpart_column['state']
                    if condensed_column_states and state == condensed_column_states[-1]:
                        content.append('<td></td>')
                    else:
                        content.append(f'<td><div id="step{step_no}counterpartexitcriteria"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

                if 'buffercolumn' in step_document:
                    content.append(f'<td><div id="step{step_no}bufferexitcriteria"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

            content.append('</tr></tbody>')
            content.append('</table></div>')
        else:
            content.append('<div id="noproject"><table><tr><th colspan="3">Welcome to Kanbanara!</th></tr><tr><td><h2>Select a Project</h2><p>Select one of the Projects from the Project menu on the Filter Bar above</p></td><td><h2>Become a Member of an Existing Project</h2><p>Ask your Project Manager to invite you to an Existing Project using the same email address (username) you used to log on</p></td><td><h2>Create a New Project</h2><p>Click on the <b>Projects -&gt; Add Project</b> menu option</p></td></tr></table></div>')

        content.append(self.footer())
        cherrypy.session.release_lock()
        return "".join(content)

    def tidy_project_announcements(self, project_document):
        announcements = project_document.get('announcements', [])
        if announcements:
            datetime_now = datetime.datetime.utcnow()
            modified_announcements = []
            for announcement_document in announcements:
                end_date = announcement_document.get('enddate', datetime.timedelta())
                if end_date and end_date > datetime_now:
                    modified_announcements.append(announcement_document)

            if announcements != modified_announcements:
                project_document['announcements'] = modified_announcements
                self.projects_collection.save(project_document)
                self.save_project_as_json(project_document)

    def insert_stuck_cards(self, standard_mode, minimised_mode, swimlane_no, state, metastate, priorities):
        """comment"""
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        owner_search_criteria, reviewer_search_criteria, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])
        if 'teammember' in member_document and not member_document["teammember"] in ["", 'Unassigned']:
            owner_search_criteria["$and"] = [{"owner":      {"$ne": member_document["teammember"]}},
                                             {"coowner":    {"$ne": member_document["teammember"]}},
                                             {"reviewer":   {"$ne": member_document["teammember"]}},
                                             {"coreviewer": {"$ne": member_document["teammember"]}}]
            owner_search_criteria['priority'] = {'$in': priorities}
            owner_search_criteria['stuck'] = {'$nin': ['', [], None]}
            someone_else_is_stuck_status = True
            if metastate == 'closed':
                someone_else_is_stuck_status = False

            subheading_displayed = False
            for document in self.cards_collection.find(owner_search_criteria):
                if not subheading_displayed:
                    content.append('<p class="'+state+document['priority']+'type"><span class="fa fa-toggle-right"></span>&nbsp;Can You Help?</p>')
                    content.append('<div>')
                    subheading_displayed = True

                content.append(self.assemble_kanban_card(['owner', 'coowner'], standard_mode, swimlane_no, document['_id'], someone_else_is_stuck_status, projection))

        return "".join(content)

    @cherrypy.expose
    def adjust_single_wip_limit(self, state, adjustment):
        """comment"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document['project']
        project_wips = member_document.get('project_wips', {})
        if project in project_wips:
            personal_wip_document = project_wips[project]
        else:
            personal_wip_document = {}

        step_no, step_role, _, _, _ = self.get_associated_state_information(state)
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        if state == condensed_column_states[-1]:
            # Set to a value such as '4 days'
            new_limit = adjustment
            personal_wip_document['closedwip'] = new_limit
        else:
            if 'step'+str(step_no)+step_role+'maxwip' in personal_wip_document and personal_wip_document['step'+str(step_no)+step_role+'maxwip']:
                current_limit = personal_wip_document['step'+str(step_no)+step_role+'maxwip']
            else:
                current_limit = -1

            new_limit = -1
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])
            if adjustment == '-':
                new_limit = current_limit-1
                if new_limit == -2:
                   new_limit = -1

            elif adjustment == '+':
                new_limit = current_limit+1
            elif adjustment == 'U':
                new_limit = -1
            elif adjustment == 'Limit':
                count = self.cards_collection.find(owner_reviewer_search_criteria).count()
                if count:
                    new_limit = count
                else:
                    new_limit = -1

            personal_wip_document['step'+str(step_no)+step_role+'maxwip'] = new_limit

        project_wips[project] = personal_wip_document
        member_document['project_wips'] = project_wips
        self.members_collection.save(member_document)
        raise cherrypy.HTTPRedirect("/kanban/index", 302)

    def populate_kanban_column(self, standard_mode, minimised_mode, swimlane_no, step_no, step_role,
                               column_name, state, metastate, centric, priorities):
        """Populates a given column on the kanban board with cards"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        _, _, _, max_wip_limit = self.get_document_count(state, [])
        swimlane_attribute = ""
        swimlane_value = ""
        swimlane_no = int(swimlane_no)
        projection = self.user_kanban_board_settings[username].get('projection', 0)
        if swimlane_no > -1:
            if 'swimlane_attribute' in self.user_kanban_board_settings[username] and self.user_kanban_board_settings[username]['swimlane_attribute']:
                if 'swimlane_values' in self.user_kanban_board_settings[username] and self.user_kanban_board_settings[username]['swimlane_values']:
                    swimlane_attribute = self.user_kanban_board_settings[username]['swimlane_attribute']
                    swimlane_value = self.user_kanban_board_settings[username]['swimlane_values'][swimlane_no]

        if priorities == ['high']:
            higher_priorities_owner_count, higher_priorities_reviewer_count, _, _ = self.get_document_count(state, ['critical'])
            if centric == 'reviewer':
                no_of_countable_cards_displayed = higher_priorities_reviewer_count
            else:
                no_of_countable_cards_displayed = higher_priorities_owner_count

        elif priorities == ['medium']:
            higher_priorities_owner_count, higher_priorities_reviewer_count, _, _ = self.get_document_count(state, ['critical', 'high'])
            if centric == 'reviewer':
                no_of_countable_cards_displayed = higher_priorities_reviewer_count
            else:
                no_of_countable_cards_displayed = higher_priorities_owner_count

        elif priorities == ['low']:
            higher_priorities_owner_count, higher_priorities_reviewer_count, _, _ = self.get_document_count(state, ['critical', 'high', 'medium'])
            if centric == 'reviewer':
                no_of_countable_cards_displayed = higher_priorities_reviewer_count
            else:
                no_of_countable_cards_displayed = higher_priorities_owner_count

        else:
            no_of_countable_cards_displayed = 0

        content.append(self.insert_stuck_cards(standard_mode, minimised_mode, swimlane_no, state, metastate, priorities))

        owner_search_criteria, reviewer_search_criteria, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])

        if 'card' in member_document and member_document['card']:
            hierarchical_ids = self.find_all_hierarchical_ids(member_document['card'], [member_document['card']])
            owner_search_criteria['id']          = {"$in": hierarchical_ids}
            reviewer_search_criteria['id']       = {"$in": hierarchical_ids}
            owner_reviewer_search_criteria['id'] = {"$in": hierarchical_ids}

        owner_search_criteria['priority'] = {"$in": priorities}
        owner_search_criteria['expedite'] = {"$ne": True}
        reviewer_search_criteria['priority'] = {"$in": priorities}
        reviewer_search_criteria['expedite'] = {"$ne": True}
        owner_reviewer_search_criteria['priority'] = {"$in": priorities}
        owner_reviewer_search_criteria['expedite'] = {"$ne": True}
        if not projection:
            owner_search_criteria['blocked']  = {"$in": ['', None]}
            owner_search_criteria['deferred'] = {"$in": ['', None]}
            reviewer_search_criteria['blocked']  = {"$in": ['', None]}
            reviewer_search_criteria['deferred'] = {"$in": ['', None]}
            owner_reviewer_search_criteria['blocked']  = {"$in": ['', None]}
            owner_reviewer_search_criteria['deferred'] = {"$in": ['', None]}

        project = member_document["project"]
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        global_wips = project_document.get('global_wips', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        if condensed_column_states and state == condensed_column_states[-1]:
            epoch = datetime.datetime.utcnow()
            owner_reviewer_search_criteria['lastchanged'] = {'$gte':epoch-self.closed_periods[max_wip_limit]}

        count = self.cards_collection.find(owner_reviewer_search_criteria).count()
        if count and self.column_required(column_name):
            max_wip_limit_reached_warning = False
            max_wip_limit_exceeded_warning = False
            already_displayed = []
            if metastate in ['backlog', 'analysis']:
                if 'type' in owner_reviewer_search_criteria:
                    types = [owner_reviewer_search_criteria['type']]
                elif metastate == 'backlog':
                    types = ['epic', 'feature', 'story', 'enhancement', 'defect',
                             'task', 'test', 'bug', 'transient']
                elif metastate == 'analysis':
                    types = ['transient', 'bug', 'test', 'task', 'defect',
                             'enhancement', 'story', 'feature', 'epic']

                for priority in priorities:
                    for type in types:
                        if swimlane_attribute == 'type' and swimlane_value != type:
                            continue

                        content.append(self.populate_kanban_column_priority_section(standard_mode, swimlane_no, step_no,
                                                                                    step_role, owner_search_criteria,
                                                                                    column_name, state, metastate, centric, priority, type,
                                                                                    no_of_countable_cards_displayed,
                                                                                    max_wip_limit))

                    epoch = datetime.datetime.utcnow()
                    projected_epoch = epoch + (self.timedelta_day * projection)
                    hidden_search_criteria = {'priority': priority,
                                              'hiddenuntil': {'$gte': projected_epoch}
                                             }
                    hidden_count = self.cards_collection.find(hidden_search_criteria).count()
                    if hidden_count == 1:
                        content.append(f'<sup class="hiddenwarning">{hidden_count} hidden card</sup>')
                    elif hidden_count > 1:
                        content.append(f'<sup class="hiddenwarning">{hidden_count} hidden cards</sup>')

            elif metastate == 'closed':
                for document in self.cards_collection.find(owner_reviewer_search_criteria).sort("lastchanged", pymongo.DESCENDING):
                    if swimlane_attribute == 'type' and swimlane_value != document['type']:
                        continue

                    content.append(self.assemble_kanban_card(['owner', 'coowner'], standard_mode, swimlane_no, document['_id'], False, projection))

            elif metastate in ['unittesting', 'integrationtesting', 'systemtesting', 'acceptancetesting']:
                for priority in priorities:
                    reviewer_search_criteria["priority"] = priority
                    if 'type' in reviewer_search_criteria:
                        types = [reviewer_search_criteria['type']]
                    else:
                        types = ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']

                    for type in types:
                        for severity in self.severities:
                            if self.displayable_severity(severity):
                                reviewer_search_criteria["severity"] = severity
                                reviewer_search_criteria["type"] = type
                                subheading_displayed = False
                                for reviewer_document in self.cards_collection.find(reviewer_search_criteria):
                                    if not self.card_deferred(reviewer_document, projection) and not self.card_hidden(reviewer_document, projection) and not self.card_blocked_by_any_means(reviewer_document, projection) and not self.card_waiting_for_other_owner_or_reviewer(reviewer_document, projection):
                                        if not subheading_displayed:
                                            plural_type = self.plurals[type]
                                            if member_document and 'teammember' in member_document and not member_document["teammember"] in ["", 'Unassigned']:
                                                content.append('<p class="'+metastate+priority+'type"><span class="fa fa-toggle-right"></span>&nbsp;'+priority.capitalize()+' '+plural_type.capitalize()+' (As Reviewer)</p>')
                                            else:
                                                content.append('<p class="'+metastate+priority+'type"><span class="fa fa-toggle-right"></span>&nbsp;'+priority.capitalize()+' '+plural_type.capitalize()+'</p>')

                                            content.append('<div>')
                                            subheading_displayed = True

                                        content.append(self.assemble_kanban_card(['reviewer', 'coreviewer'], standard_mode, swimlane_no, reviewer_document['_id'], False, projection))
                                        no_of_countable_cards_displayed = self.increment_number_of_countable_cards_displayed(state, centric, reviewer_document, no_of_countable_cards_displayed, projection)
                                        if max_wip_limit > -1:
                                            if no_of_countable_cards_displayed == max_wip_limit and not max_wip_limit_reached_warning:
                                                content.append(self.max_wip_limit_reached())
                                                max_wip_limit_reached_warning = True
                                            elif no_of_countable_cards_displayed > max_wip_limit and not max_wip_limit_exceeded_warning:
                                                content.append(self.max_wip_limit_exceeded(step_no, step_role, column_name, state, centric, global_wips))
                                                max_wip_limit_exceeded_warning = True

                                        already_displayed.append(reviewer_document['id'])

                                if subheading_displayed:
                                    content.append('</div>')

            elif centric == 'Owner':
                if step_role == 'buffer':
                    content.append(self.bottleneck_condition(project, state, metastate))

                if 'type' in owner_search_criteria and owner_search_criteria['type']:
                    types = [owner_search_criteria['type']]
                else:
                    types = ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']

                for priority in priorities:
                    owner_search_criteria["priority"] = priority
                    for type in types:
                        if swimlane_attribute and swimlane_value:
                            if swimlane_attribute == 'type' and swimlane_value != type:
                                continue
                            else:
                                owner_search_criteria[swimlane_attribute] = swimlane_value

                        owner_search_criteria["type"] = type
                        for severity in self.severities:
                            if self.displayable_severity(severity):
                                owner_search_criteria["severity"] = severity
                                subheading_displayed = False
                                for document in self.cards_collection.find(owner_search_criteria):
                                    if not self.card_deferred(document, projection) and not self.card_hidden(document, projection) and not self.card_blocked_by_any_means(document, projection) and not self.card_waiting_for_other_owner_or_reviewer(document, projection):
                                        if not subheading_displayed:
                                            plural_type = self.plurals[type]
                                            content.append('<p class="'+metastate+priority+'type"><span class="fa fa-toggle-right"></span>&nbsp;'+priority.capitalize()+' '+plural_type.capitalize()+'</p>')
                                            content.append('<div>')
                                            subheading_displayed = True

                                        content.append(self.assemble_kanban_card(['owner', 'coowner'], standard_mode, swimlane_no, document['_id'], False, projection))
                                        already_displayed.append(document['id'])
                                        no_of_countable_cards_displayed = self.increment_number_of_countable_cards_displayed(state, centric, document, no_of_countable_cards_displayed, projection)
                                        if state != condensed_column_states[-1] and max_wip_limit > -1:
                                            if no_of_countable_cards_displayed == max_wip_limit and not max_wip_limit_reached_warning:
                                                content.append(self.max_wip_limit_reached())
                                                max_wip_limit_reached_warning = True
                                            elif no_of_countable_cards_displayed > max_wip_limit and not max_wip_limit_exceeded_warning:
                                                content.append(self.max_wip_limit_exceeded(step_no, step_role, column_name, state, centric, global_wips))
                                                max_wip_limit_exceeded_warning = True

                                if subheading_displayed:
                                    content.append('</div>')

            elif centric == 'Reviewer':
                if step_role == 'buffer':
                    content.append(self.bottleneck_condition(project, state, metastate))

                if 'type' in owner_search_criteria and owner_search_criteria['type']:
                    types = [owner_search_criteria['type']]
                else:
                    types = ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']

                for priority in priorities:
                    reviewer_search_criteria["priority"] = priority
                    if 'type' in reviewer_search_criteria:
                        types = [reviewer_search_criteria['type']]
                    else:
                        types = ['epic', 'feature', 'story', 'enhancement', 'defect', 'task', 'test', 'bug', 'transient']

                    for type in types:
                        reviewer_search_criteria["type"] = type
                        for severity in self.severities:
                            if self.displayable_severity(severity):
                                reviewer_search_criteria["severity"] = severity
                                subheading_displayed = False
                                for document in self.cards_collection.find(reviewer_search_criteria):
                                    if not self.card_deferred(document, projection) and not self.card_hidden(document, projection) and not self.card_blocked_by_any_means(document, projection) and not self.card_waiting_for_other_owner_or_reviewer(document, projection):
                                        if document['id'] not in already_displayed:
                                            if not subheading_displayed:
                                                plural_type = self.plurals[type]
                                                if member_document and 'teammember' in member_document and not member_document["teammember"] in ["", 'Unassigned']:
                                                    content.append('<p class="'+metastate+priority+'type"><span class="fa fa-toggle-right"></span>&nbsp;'+priority.capitalize()+' '+plural_type.capitalize()+' (As Reviewer)</p>')
                                                else:
                                                    content.append('<p class="'+metastate+priority+'type"><span class="fa fa-toggle-right"></span>&nbsp;'+priority.capitalize()+' '+plural_type.capitalize()+'</p>')

                                                content.append('<div>')
                                                subheading_displayed = True

                                            content.append(self.assemble_kanban_card(['reviewer', 'coreviewer'], minimised_mode, swimlane_no, document['_id'], False, projection))
                                            no_of_countable_cards_displayed = self.increment_number_of_countable_cards_displayed(state, centric, document, no_of_countable_cards_displayed, projection)
                                            if max_wip_limit > -1:
                                                if no_of_countable_cards_displayed == max_wip_limit and not max_wip_limit_reached_warning:
                                                    content.append(self.max_wip_limit_reached())
                                                    max_wip_limit_reached_warning = True
                                                elif no_of_countable_cards_displayed > max_wip_limit and not max_wip_limit_exceeded_warning:
                                                    content.append(self.max_wip_limit_exceeded(step_no, step_role, column_name, state, centric, global_wips))
                                                    max_wip_limit_exceeded_warning = True

                                if subheading_displayed:
                                    content.append('</div>')

        else:
            if len(priorities) == 1:
                content.append('<span class="ui-icon ui-icon-info" title="There are no '+priorities[0]+' priority cards in the '+state.capitalize()+' state!" />')
            else:
                content.append('<span class="ui-icon ui-icon-info" title="There are no cards in the '+state.capitalize()+' state!" />')
        
        
        content.append('<script type="text/javascript" src="/scripts/kanban.js"></script>')
        content.append('<script type="text/javascript" src="/scripts/dragdrop.js"></script>')

        loaded_js_files = self.user_kanban_board_settings[username].get('loaded_js_files', [])
        if len(priorities) == 1:
            js_file = f'swimlane{swimlane_no}step{step_no}{step_role}{priorities[0]}.js'
            #if js_file not in loaded_js_files:
            dummy = self.generate_drop_js_script(swimlane_no, step_no, step_role, priorities[0])
            content.append(f'<script type="text/javascript" src="/kanban/scripts/autogenerated/{js_file}"></script>')
            self.user_kanban_board_settings[username]['loaded_js_files'].append(js_file)
                
        elif len(priorities) == 4:
            js_file = f'swimlane{swimlane_no}step{step_no}{step_role}all.js'
            if js_file not in loaded_js_files:
                dummy = self.generate_drop_js_script(swimlane_no, step_no, step_role, 'all')
                content.append(f'<script type="text/javascript" src="/kanban/scripts/autogenerated/{js_file}"></script>')
                self.user_kanban_board_settings[username]['loaded_js_files'].append(js_file)

        return "".join(content)

    def populate_kanban_column_blocked(self, standard_mode, minimised_mode, column_name, state, priorities, projection):
        '''Show blocked cards in the blocked section of the specified state on the kanban board'''
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        subheading_displayed = False
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])
        if member_document and 'card' in member_document and member_document['card']:
            hierarchical_ids = self.find_all_hierarchical_ids(member_document['card'], [member_document['card']])
            owner_reviewer_search_criteria['id'] = {"$in": hierarchical_ids}

        owner_reviewer_search_criteria['priority'] = {"$in": priorities}
        owner_reviewer_search_criteria['expedite'] = {"$ne": True}
        project = member_document.get('project', '')
        project_document = ""
        if project:
            project_document = self.projects_collection.find_one({'project': project})

        if self.column_required(column_name):
            subheading_displayed = False
            for priority in priorities:
                owner_reviewer_search_criteria["priority"] = priority
                for type in ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']:
                    for severity in self.severities:
                        if self.displayable_severity(severity):
                            owner_reviewer_search_criteria["severity"] = severity
                            owner_reviewer_search_criteria["type"] = type
                            for document in self.cards_collection.find(owner_reviewer_search_criteria):
                                if self.card_blocked_by_any_means(document, projection):
                                    if not subheading_displayed:
                                        content.append('<p class="'+state+'blockedtype"><span class="fa fa-toggle-right"></span>&nbsp;Blocked</p>')
                                        subheading_displayed = True

                                    content.append(self.assemble_kanban_card(['owner', 'coowner', 'reviewer', 'coreviewer'], standard_mode, -1, document['_id'], False, projection))

                            if subheading_displayed:
                                content.append('</div>')

        return "".join(content)

    def populate_kanban_column_deferred(self, standard_mode, minimised_mode, column_name, state, priorities, projection):
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        subheading_displayed = False
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])
        if member_document and 'card' in member_document and member_document['card']:
            hierarchical_ids = self.find_all_hierarchical_ids(member_document['card'], [member_document['card']])
            owner_reviewer_search_criteria['id'] = {"$in": hierarchical_ids}

        owner_reviewer_search_criteria['priority'] = {"$in": priorities}
        owner_reviewer_search_criteria['expedite'] = {"$ne": True}
        project = member_document.get('project', '')
        project_document = ""
        if project:
            project_document = self.projects_collection.find_one({'project': project})

        if self.column_required(column_name):
            subheading_displayed = False
            for priority in priorities:
                owner_reviewer_search_criteria["priority"] = priority
                for type in ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']:
                    for severity in self.severities:
                        if self.displayable_severity(severity):
                            owner_reviewer_search_criteria["severity"] = severity
                            owner_reviewer_search_criteria["type"] = type
                            for document in self.cards_collection.find(owner_reviewer_search_criteria):
                                if self.card_deferred(document, projection):
                                    if not subheading_displayed:
                                        content.append('<p class="'+state+'deferredtype"><span class="fa fa-toggle-right"></span>&nbsp;Deferred</p>')
                                        subheading_displayed = True

                                    content.append(self.assemble_kanban_card(['owner', 'coowner', 'reviewer', 'coreviewer'], standard_mode, -1, document['_id'], False, projection))

                            if subheading_displayed:
                                content.append('</div>')

        return "".join(content)

    def populate_kanban_column_ghosted(self, standard_mode, minimised_mode, column_name, state, centric, priorities):
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        teammember = member_document.get('teammember', '')
        if teammember not in ['Unassigned', '']:
            subheading_displayed = False
            owner_search_criteria, reviewer_search_criteria, _ = self.get_filtered_search_criteria(session_document, [state])
            if centric == 'Owner':
                search_criteria = owner_search_criteria
                subheading = "Ghosts (As Owner)"
                explanation = "These are your own ghost cards having only materialised to others whilst in this review period"
            else:
                search_criteria = reviewer_search_criteria
                subheading = "Ghosts"
                explanation = "These are the ghost cards of others which only materialise to you during their review period"

            if member_document and 'card' in member_document and member_document['card']:
                hierarchical_ids = self.find_all_hierarchical_ids(member_document['card'], [member_document['card']])
                search_criteria['id'] = {"$in": hierarchical_ids}

            search_criteria['priority'] = {"$in": priorities}
            project = ""
            project_document = ""
            if 'project' in member_document and member_document["project"]:
                project = member_document["project"]
                project_document = self.projects_collection.find_one({'project': project})

            if self.column_required(column_name):
                subheading_displayed = False
                for priority in priorities:
                    search_criteria["priority"] = priority
                    for type in ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']:
                        for severity in self.severities:
                            if self.displayable_severity(severity):
                                search_criteria["severity"] = severity
                                search_criteria["type"] = type
                                for document in self.cards_collection.find(search_criteria):
                                    if not subheading_displayed:
                                        content.append('<p class="'+state+'ghostedtype" title="'+explanation+'"><span class="fa fa-toggle-right"></span>&nbsp;'+subheading+'</p>')
                                        subheading_displayed = True

                                    content.append(self.assemble_kanban_card(['owner', 'coowner', 'reviewer', 'coreviewer'], standard_mode, -1, document['_id'], False, 0))

                                if subheading_displayed:
                                    content.append('</div>')

        return "".join(content)

    def populate_kanban_column_waiting(self, standard_mode, column_name, state, projection):
        '''Show waiting cards in the waiting section of the specified state on the kanban board'''
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        subheading_displayed = False
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, [state])
        if member_document and 'card' in member_document and member_document['card']:
            hierarchical_ids = self.find_all_hierarchical_ids(member_document['card'], [member_document['card']])
            owner_reviewer_search_criteria['id'] = {"$in": hierarchical_ids}

        project = member_document.get('project', '')
        project_document = ""
        if project:
            project_document = self.projects_collection.find_one({'project': project})

        if self.column_required(column_name):
            subheading_displayed = False
            for priority in self.priorities:
                owner_reviewer_search_criteria["priority"] = priority
                for type in ['transient', 'bug', 'test', 'task', 'defect', 'enhancement', 'story', 'feature', 'epic']:
                    for severity in self.severities:
                        if self.displayable_severity(severity):
                            owner_reviewer_search_criteria["severity"] = severity
                            owner_reviewer_search_criteria["type"] = type
                            for document in self.cards_collection.find(owner_reviewer_search_criteria):
                                if self.card_waiting_for_other_owner_or_reviewer(document, projection) and not self.card_deferred(document, projection) and not self.card_hidden(document, projection) and not self.card_blocked_by_any_means(document, projection):
                                    if not subheading_displayed:
                                        content.append('<p class="'+state+'waitingtype"><span class="fa fa-toggle-right"></span>&nbsp;Waiting</p>')
                                        subheading_displayed = True

                                    content.append(self.assemble_kanban_card(['owner', 'coowner', 'reviewer', 'coreviewer'], standard_mode, -1, document['_id'], False, projection))

                            if subheading_displayed:
                                content.append('</div>')

        return "".join(content)

    @cherrypy.expose
    def step_counterpart_entry_criteria(self, step_no):
        content = ""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                workflow = project_document.get('workflow', [])
                step_document = workflow[int(step_no)]
                counterpart_column = step_document['counterpartcolumn']
                state = counterpart_column['state']
                if state+'entrycriteria'in project_document and project_document[state+'entrycriteria']:
                    return '<input class="toggleentrycriteria" type="button" id="togglestep'+str(step_no)+'counterpartentrycriteria" value="Entry Criteria" />'
                    # <p>'+project_document[state+'entrycriteria'].replace('\r','<br>')+'</p>

        return content

    @cherrypy.expose
    def step_counterpart_exit_criteria(self, step_no):
        content = ""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                workflow = project_document.get('workflow', [])
                step_document = workflow[int(step_no)]
                counterpart_column = step_document['counterpartcolumn']
                state = counterpart_column['state']
                if state+'exitcriteria'in project_document and project_document[state+'exitcriteria']:
                    return '<input class="toggleexitcriteria" type="button" id="togglestep'+str(step_no)+'counterpartexitcriteria" value="Exit Criteria" />'
                    # <p>'+project_document[state+'exitcriteria'].replace('\r','<br>')+'</p>

        return content

    @cherrypy.expose
    def step_main_entry_criteria(self, step_no):
        content = ""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                workflow = project_document.get('workflow', [])
                step_document = workflow[int(step_no)]
                main_column = step_document['maincolumn']
                state = main_column['state']
                if state+'entrycriteria'in project_document and project_document[state+'entrycriteria']:
                    return '<input class="toggleentrycriteria" type="button" id="togglestep'+str(step_no)+'mainentrycriteria" value="Entry Criteria" />'
                    # <p>'+project_document[state+'entrycriteria'].replace('\r','<br>')+'</p>

        return content

    @cherrypy.expose
    def step_main_exit_criteria(self, step_no):
        content = ""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                workflow = project_document.get('workflow', [])
                step_document = workflow[int(step_no)]
                main_column = step_document['maincolumn']
                state = main_column['state']
                if state+'exitcriteria'in project_document and project_document[state+'exitcriteria']:
                    return '<input class="toggleexitcriteria" type="button" id="togglestep'+str(step_no)+'mainexitcriteria" value="Exit Criteria" />'
                    # <p>'+project_document[state+'exitcriteria'].replace('\r','<br>')+'</p>

        return content

    @cherrypy.expose
    def step_buffer_entry_criteria(self, step_no):
        content = ""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                workflow = project_document.get('workflow', [])
                step_document = workflow[int(step_no)]
                buffer_column = step_document['buffercolumn']
                state = buffer_column['state']
                if state+'entrycriteria'in project_document and project_document[state+'entrycriteria']:
                    return '<input class="toggleentrycriteria" type="button" id="togglestep'+str(step_no)+'bufferentrycriteria" value="Entry Criteria" />'
                    # <p>'+project_document[state+'entrycriteria'].replace('\r','<br>')+'</p>

        return content

    @cherrypy.expose
    def step_buffer_exit_criteria(self, step_no):
        content = ""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                workflow = project_document.get('workflow', [])
                step_document = workflow[int(step_no)]
                buffer_column = step_document['buffercolumn']
                state = buffer_column['state']
                if state+'exitcriteria'in project_document and project_document[state+'exitcriteria']:
                    return '<input class="toggleexitcriteria" type="button" id="togglestep'+str(step_no)+'bufferexitcriteria" value="Exit Criteria" />'
                    # <p>'+project_document[state+'exitcriteria'].replace('\r','<br>')+'</p>

        return content

    def populate_kanban_column_priority_section(self, standard_mode, swimlane_no, step_no, step_role, search_criteria,
                                                column_name, state, metastate, centric, priority, type,
                                                no_of_countable_cards_displayed, max_wip_limit):
        """comment"""
        # TODO - Could this be used for testing state as well?
        # TODO - Do we need to pass in owner_reviewer_search_criteria or just owner_search_criteria?
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document['project']
        project_document = self.projects_collection.find_one({'project': project})
        global_wips = project_document.get('global_wips', {})
        content = []
        search_criteria["priority"] = priority
        search_criteria["type"] = type
        search_criteria['expedite'] = {"$ne": True}
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            projection = kanban_board_settings.get('projection', 0)
            if swimlane_no:
                swimlane_no = int(swimlane_no)
                if swimlane_no > -1:
                    if 'swimlane_attribute' in kanban_board_settings and kanban_board_settings['swimlane_attribute']:
                        if 'swimlane_values' in kanban_board_settings and kanban_board_settings['swimlane_values']:
                            swimlane_attribute = kanban_board_settings['swimlane_attribute']
                            swimlane_value = kanban_board_settings['swimlane_values'][swimlane_no]
                            if swimlane_attribute == 'type' and swimlane_value != type:
                                return ""
                            else:
                                search_criteria[swimlane_attribute] = swimlane_value

        if "severity" in search_criteria:
            # The "severity" value seemed to be being retained from a previous call of this function!
            del search_criteria["severity"]

        if member_document and 'card' in member_document and member_document['card']:
            hierarchical_ids = self.find_all_hierarchical_ids(member_document['card'], [member_document['card']])
            search_criteria['id'] = {"$in": hierarchical_ids}

        if self.cards_collection.find(search_criteria).count():
            subheading_displayed = False
            for severity in self.severities:
                if self.displayable_severity(severity):
                    search_criteria["severity"] = severity
                    wip_limit_reached_warning = False
                    wip_limit_exceeded_warning = False
                    for document in self.cards_collection.find(search_criteria).sort('position', pymongo.ASCENDING):
                        if not self.card_deferred(document, projection) and not self.card_hidden(document, projection) and not self.card_blocked_by_any_means(document, projection) and not self.card_waiting_for_other_owner_or_reviewer(document, projection):
                            if not subheading_displayed:
                                plural_type = self.plurals[type]
                                content.append('<p class="'+metastate+priority+'type"><span class="fa fa-toggle-right"></span>&nbsp;'+priority.capitalize()+' '+plural_type.capitalize()+'</p>')
                                content.append('<div id="'+metastate+type+priority+'">')
                                subheading_displayed = True

                            content.append(self.assemble_kanban_card(['owner', 'coowner'], standard_mode, swimlane_no, document['_id'], False, projection))
                            no_of_countable_cards_displayed = self.increment_number_of_countable_cards_displayed(state, centric, document, no_of_countable_cards_displayed, projection)
                            if isinstance(max_wip_limit, int) and max_wip_limit > -1:
                                if no_of_countable_cards_displayed == max_wip_limit and not wip_limit_reached_warning:
                                    content.append(self.max_wip_limit_reached())
                                    wip_limit_reached_warning = True
                                elif no_of_countable_cards_displayed > max_wip_limit and not wip_limit_exceeded_warning:
                                    content.append(self.max_wip_limit_exceeded(step_no, step_role, column_name, state, centric, global_wips))
                                    wip_limit_exceeded_warning = True

            if subheading_displayed:
                content.append('</div>')

        return "".join(content)

    def max_wip_limit_exceeded(self, step_no, step_role, column_name, state, centric, global_wips):
        """Displays a message on kanban board to indicate maximum WIP limit has been exceeded"""
        content = []
        content.append('<div class="wiplimitexceeded">WIP Limit Exceeded!')
        if not ('enforcewiplimits' in global_wips and global_wips['enforcewiplimits']):
            if self.column_required(column_name):
                owner_count, reviewer_count, min_wip_limit, max_wip_limit = self.get_document_count(state, [])
                if centric == 'Reviewer':
                    relevant_count = reviewer_count
                else:
                    relevant_count = owner_count

                if relevant_count:
                    increase_unlimit_buttons = '<form action="/kanban/adjust_single_wip_limit" method="post"><input type="hidden" name="state" value="'+state+'"><span class="controlgroup"><input class="wiplimits" type="submit" name="adjustment" value="+" title="Increase Maximum Limit"><input class="wiplimits" type="submit" name="adjustment" value="U" title="Switch to Unlimited Maximum"></span></form>'
                    content.append(f'<br><sup class="wip">{min_wip_limit}</sup><sup class="lte">&le;</sup>')
                    if relevant_count > max_wip_limit:
                        content.append(f'<sup class="wipwarning" title="Your Maximum WIP Limit has been Exceeded">{relevant_count}</sup>')
                    elif relevant_count < min_wip_limit:
                        content.append(f'<sup class="wipwarning" title="Your Minimum WIP Limit has yet to be Reached">{relevant_count}</sup>')
                    else:
                        content.append(f'<sup class="wip">{relevant_count}</sup>')

                    content.append(f'<sup class="lte">&le;</sup><sup class="wip">{max_wip_limit}</sup><br>{increase_unlimit_buttons}')

        content.append('</div>')
        return "".join(content)

    def tidy_owner_coowner_states(self):
        search_criteria = {'owner':        {'$exists': True, '$ne': ''},
                           'coowner':      {'$exists': True, '$ne': ''},
                           'ownerstate':   {'$exists': True, '$ne': ''},
                           'coownerstate': {'$exists': True, '$ne': ''}
                          }
        for card_document in self.cards_collection.find(search_criteria):
            if card_document['ownerstate'] == card_document['coownerstate']:
                card_document['state'] = card_document['ownerstate']
                del card_document['ownerstate']
                del card_document['coownerstate']
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

    def tidy_reviewer_coreviewer_states(self):
        search_criteria = {'reviewer':        {'$exists': True, '$ne': ''},
                           'coreviewer':      {'$exists': True, '$ne': ''},
                           'reviewerstate':   {'$exists': True, '$ne': ''},
                           'coreviewerstate': {'$exists': True, '$ne': ''}
                          }
        for card_document in self.cards_collection.find(search_criteria):
            if card_document['reviewerstate'] == card_document['coreviewerstate']:
                card_document['state'] = card_document['reviewerstate']
                del card_document['reviewerstate']
                del card_document['coreviewerstate']
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
conf = {'/': {'tools.staticdir.root':   CURRENT_DIR,
              'tools.sessions.on':      True,
              'tools.sessions.locking': 'explicit'
              }}
for directory in ['images', 'scripts']:
    if os.path.exists(CURRENT_DIR+os.sep+directory):
        conf['/'+directory] = {'tools.staticdir.on':  True,
                               'tools.staticdir.dir': directory}

cherrypy.tree.mount(Kanban(), '/kanban', config=conf)
