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

'''Kanbanara Common Code'''

import ast
import datetime
from decimal import Decimal
import html
import os
import random
import re
import statistics
import sys
import urllib.parse
import uuid

from http import cookies
import bson
from bson import ObjectId
import cherrypy
import lipsum
from mako.template import Template
from pymongo import MongoClient
import pygal
from pygal.style import LightStyle


class Kanbanara:
    '''Kanbanara Common Code'''

    def __init__(self):
        self.current_dir = os.path.dirname(os.path.abspath(__file__))
        self.assemble_page_component_mappings()

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

        # 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
        kanbanara_db = connection['kanbanara']

        # Connect to 'cards' collection
        self.cards_collection = kanbanara_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)

        # Connect to 'sessions' collection
        self.sessions_collection = kanbanara_db['sessions']

        # Connect to 'members' collection
        self.members_collection = kanbanara_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 'projects' collection
        self.projects_collection = kanbanara_db['projects']
        for attribute in ['project']:
            self.projects_collection.create_index(attribute, unique=True, background=True)

        # Connect to 'deletions' collection
        self.deletions_collection = kanbanara_db['deletions']

        #self.local_tz = datetime.time.tzname
        #logging.info(self.local_tz)
        
        self.user_kanban_board_settings = {}

        self.timedelta_minute = datetime.timedelta(minutes=1)
        self.timedelta_hour   = datetime.timedelta(hours=1)
        self.timedelta_day    = datetime.timedelta(days=1)
        self.timedelta_week   = datetime.timedelta(days=7)
        self.timedelta_month  = datetime.timedelta(days=28)
        self.timedelta_year   = datetime.timedelta(days=365)

        self.kanbanara_settings = {'instance': "Untitled"}
        self.read_kanbanara_ini_file()
        self.instance = self.kanbanara_settings['instance']
        
        self.next_card_numbers = {}

        self.metastates_list = ['untriaged', 'triaged', 'backlog', 'defined',
                                'analysis', 'analysispeerreview', 'analysed',
                                'design', 'designpeerreview', 'designed',
                                'development', 'developmentpeerreview', 'developed',
                                'unittesting', 'unittestingaccepted', 'integrationtesting',
                                'integrationtestingaccepted', 'systemtesting',
                                'systemtestingaccepted', 'acceptancetesting',
                                'acceptancetestingaccepted', 'completed', 'closed']

        self.metastates_main_list = ['untriaged', 'triaged', 'backlog', 'analysis',
                                     'analysispeerreview', 'design', 'designpeerreview',
                                     'development', 'developmentpeerreview', 'unittesting',
                                     'integrationtesting', 'systemtesting', 'acceptancetesting',
                                     'completed', 'closed']

        self.metastates_buffer_list = ['defined', 'analysed', 'designed', 'developed',
                                       'unittestingaccepted', 'integrationtestingaccepted',
                                       'systemtestingaccepted', 'acceptancetestingaccepted']

        self.months = ['January', 'February', 'March', 'April', 'May', 'June',
                       'July', 'August', 'September', 'October', 'November', 'December']

        self.recent_activities = []

        self.currencies = {'GBP': ('Great British Pounds', '&pound;'),
                           'USD': ('United States Dollars', '$')
                          }

        # TODO - Could I use an ordered dictionary here?
        self.synchronisation_periods = {'15 minutes': datetime.timedelta(minutes=15),
                                        '30 minutes': datetime.timedelta(minutes=30),
                                        '45 minutes': datetime.timedelta(minutes=45),
                                        '1 hour':     datetime.timedelta(hours=1),
                                        '2 hours':    datetime.timedelta(hours=2),
                                        '3 hours':    datetime.timedelta(hours=3),
                                        '6 hours':    datetime.timedelta(hours=6),
                                        '9 hours':    datetime.timedelta(hours=9),
                                        '12 hours':   datetime.timedelta(hours=12),
                                        '15 hours':   datetime.timedelta(hours=15),
                                        '18 hours':   datetime.timedelta(hours=18),
                                        '21 hours':   datetime.timedelta(hours=21),
                                        '1 day':      datetime.timedelta(days=1)
                                       }

        self.potential_synchronisation_periods = ['15 minutes', '30 minutes', '45 minutes',
                                                  '1 hour', '2 hours', '3 hours', '6 hours',
                                                  '9 hours', '12 hours', '15 hours', '18 hours',
                                                  '21 hours', '1 day']
                          
        self.card_attributes_datatypes = {'actualcost': 'float',
                                          'actualtime': 'float',
                                          'affectsversion': 'string',
                                          'artifacts': 'list',
                                          'broadcast': 'string',
                                          'bypassreview': 'boolean',
                                          'category': 'string',
                                          'classofservice': 'string',
                                          'comment': 'list',
                                          'coowner': 'string',
                                          'coreviewer': 'string', 
                                          'creator': 'string', 
                                          'crmcase': 'string', 
                                          'customer': 'string', 
                                          'deadline': 'datetime', 
                                          'dependsupon': 'string', 
                                          'deferred': 'string', 
                                          'deferreduntil': 'datetime',
                                          'description': 'string',
                                          'difficulty': 'string', 
                                          'emotion': 'string', 
                                          'escalation': 'string', 
                                          'estimatedcost': 'float', 
                                          'estimatedtime': 'float', 
                                          'externalhyperlink': 'string', 
                                          'externalreference': 'string', 
                                          'fixversion': 'string', 
                                          'iteration': 'string', 
                                          'nextaction': 'datetime', 
                                          'notes': 'string', 
                                          'owner': 'string', 
                                          'priority': 'string', 
                                          'question': 'string', 
                                          'recurring': 'boolean', 
                                          'release': 'string', 
                                          'resolution': 'string', 
                                          'reviewer': 'string', 
                                          'rootcauseanalysis': 'string', 
                                          'rules': 'list', 
                                          'severity': 'string', 
                                          'startby': 'datetime', 
                                          'state': 'string', 
                                          'status': 'string', 
                                          'tags': 'string', 
                                          'testcases': 'list', 
                                          'title': 'string'}

        self.rule_condition_allowable_card_attributes = {'affectsversion':    True,
                                                         'blocked':           True,
                                                         'coowner':           True,
                                                         'coreviewer':        True,
                                                         'externalreference': True,
                                                         'fixversion':        True,
                                                         'nextaction':        True,
                                                         'owner':             True,
                                                         'reviewer':          True,
                                                         'state':             True
                                                        }

        self.rule_action_allowable_card_attributes = {
            'affectsversion':    True,
            'blocked':           True,
            'coowner':           True,
            'coreviewer':        True,
            'externalreference': True,
            'fixversion':        True,
            'nextaction':        True,
            'owner':             True,
            'reviewer':          True,
            'state':             True}

        self.file_locks = {}

        # <function>,<textual name>,<project search criteria>,<card search criteria>
        self.metrics = [('abandoned', 'Abandoned', [], [('resolution', 'Abandoned')]),
                        ('blockages', 'Blockages', [], [('blockedhistory', {"$exists": True})]),
                        ('bottlenecks', 'Bottlenecks', [], []),
                        ('budget_burn', 'Budget Burn', [('budget', {'$gt': 0})],
                         [("$or", [{"estimatedcost": {"$exists": True, '$nin': [[], '', None]}},
                                   {"actualcost": {"$exists": True, '$nin': [[], '', None]}}])]
                        ),
                        ('buffer_burn_rate', 'Buffer Burn Rate', [], []),
                        ('bugs_and_defects', 'Bugs and Defects', [], [('type', {'$in': ['bug', 'defect']})]),
                        ('burndownchart', 'Burndown Chart', [],
                         [("estimatedtime",{"$exists": True, '$nin': [[], '', None]}),
                          ("actualtimehistory",{"$exists": True, '$nin': [[], '', None]})]),
                        ('burnup_chart', 'Burnup Chart', [], []),
                        ('completedcards', 'Completed Cards', [], [('state', 'closed')]),
                        ('controlchart', 'Control Chart', [], []),
                        ('cfd', 'Cumulative Flow Diagram', [], []),
                        ('cycletime', 'Cycle Time', [], []),
                        ('divisionoflabour', 'Division Of Labour', [], []),
                        ('duedateperformance', 'Due Date Performance', [],
                         [('deadline', {'$exists': True, '$nin': [[], '', None]})]),
                        ('earned_business_value', 'Earned Business Value', [], []),
                        ('emotions_chart', 'Emotions Chart', [], [('emotion', {'$nin': [[], '', None]})]),
                        ('leadtime', 'Lead Time', [], [('backlog', {"$exists": True})]),
                        ('percentage_of_scope_complete', 'Percentage of Scope Complete', [], []),
                        ('quality', 'Quality', [], []),
                        ('recidivism_rate', 'Recidivism Rate', [], []),
                        ('running_tested_features', 'Running Tested Features', [], []),
                        ('spc', 'Statistical Process Control', [], []),
                        ('tagschart', 'Tags Chart', [], [('tags', {"$exists": True})]),
                        ('teamautonomy', 'Team Autonomy', [], []),
                        ('throughput', 'Throughput', [], [('resolution', 'Released')]),
                        ('valuefailuredemand', 'Value/Failure Demand', [], []),
                        ('velocitychart', 'Velocity', [], []),
                        ('velocitybyeffort', 'Velocity by Effort', [], [])
                       ]

        self.classes_of_service = ['Expedite', 'Fixed Delivery Date', 'Standard', 'Intangible']
        
        self.potential_closed_periods = ['1 hour', '3 hours', '6 hours', '9 hours', '12 hours',
                                         '18 hours', '1 day', '2 days', '3 days', '4 days',
                                         '5 days', '6 days', '1 week', '2 weeks', '3 weeks',
                                         '1 month', '2 months', '3 months', '4 months', '5 months',
                                         '6 months', '9 months', '1 year', '2 years', '3 years',
                                         '4 years', '5 years']

        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')}

        self.searchable_attributes = ['actualcost', 'actualcosthistory', 'actualtime',
                                      'actualtimehistory', 'affectsversion', 'blocked',
                                      'blockeduntil', 'blocksparent', 'broadcast', 'category',
                                      'comments', 'coowner', 'coreviewer', 'creator', 'crmcase',
                                      'customer', 'deadline', 'deferred', 'deferreduntil',
                                      'dependsupon', 'description', 'difficulty', 'emotion',
                                      'escalation', 'estimatedcost', 'estimatedcosthistory',
                                      'estimatedtime', 'estimatedtimehistory', 'externalhyperlink',
                                      'externalreference', 'fixversion', 'hashtags', 'hiddenuntil',
                                      'id', 'iteration', 'lastchanged', 'lastchangedby',
                                      'lasttouched', 'lasttouchedby', 'nextaction', 'notes',
                                      'owner', 'parent', 'question', 'recurring',
                                      'release', 'resolution', 'reviewer', 'rootcauseanalysis',
                                      'rules', 'startby', 'statehistory', 'status', 'stuck',
                                      'tags', 'testcases', 'title', 'cardtype'
                                     ]

        self.severities = ['critical', 'high', 'medium', 'low']
        self.priorities = ['critical', 'high', 'medium', 'low']

        self.emotions = {'alert': 'tongue_out.png',
                         'bored': 'drugged.png',
                         'calm': 'winking.png',
                         'contented': 'cool.png',
                         'depressed': 'crying.png',
                         'elated': 'gasping.png',
                         'excited': 'smiling.png',
                         'fatigued': 'tired.png',
                         'happy': 'happy.png',
                         'nervous': 'frowning.png',
                         'relaxed': 'winking.png',
                         'sad': 'sick.png',
                         'serene': 'grinning.png',
                         'stressed': 'terrified.png',
                         'tense': 'malicious.png',
                         'upset': 'irritated.png'}

        self.kanbanboards = [('Tabbed', 'full', 'Shows each card as a tabbed dialog'),
                             ('Absenteeism', 'placeholder', ''),
                             ('Activity', 'placeholder', ''),
                             ('AffectsVersion', 'placeholder', ''),
                             ('Artifact Clash', 'placeholder', ''),
                             ('Attachments', 'placeholder', ''),
                             ('Attributes', 'full', ''),
                             ('Avatar', 'full', ''),
                             ('Blockages', 'placeholder', ''),
                             ('Bypass Reviews', 'placeholder', ''),
                             ('Capitals', 'full', ''),
                             ('Children', 'full', "Shows a card's children"),
                             ('Class Of Service', 'full', ''),
                             ('Comments', 'placeholder', ''),
                             ('Co-Owner', 'placeholder', 'Highlights the co-owner for each card'),
                             ('Co-Reviewer', 'placeholder', 'Highlights the co-reviewer for each card'),
                             ('Cost', 'placeholder', ''),
                             ('Creator', 'full', ''),
                             ('CRM Cases', 'placeholder', ''),
                             ('Custom Attributes', 'full', ''),
                             ('Customer', 'placeholder', 'Highlights the customer for each card'),
                             ('Days In State', 'full', 'Highlights the number of days each card has resided in its current state'),
                             ('Deadline', 'placeholder', ''),
                             ('Deferrals', 'placeholder', ''),
                             ('Difficulty', 'full', ''),
                             ('Emotions', 'placeholder', ''),
                             ('Escalation', 'placeholder', ''),
                             ('Ext Ref', 'placeholder', ''),
                             ('FixVersion', 'placeholder', ''),
                             ('Focus', 'full', ''),
                             ('Hashtags', 'placeholder', ''),
                             ('Identifier', 'full', ''),
                             ('Iteration', 'placeholder', 'Highlights the iteration each card is assigned to'),
                             ('Last Changed', 'full', ''),
                             ('Last Touched', 'full', ''),
                             ('Lipsum', 'full', ''),
                             ('New', 'placeholder', ''),
                             ('Next Action', 'placeholder', ''),
                             ('Owner Unassigned', 'placeholder', ''),
                             ('Questions', 'placeholder', ''),
                             ('Reassignments', 'placeholder', ''),
                             ('Recidivism', 'placeholder', ''),
                             ('Recurring', 'placeholder', ''),
                             ('Release', 'placeholder', 'Highlights the release each card is assigned to'),
                             ('Reopened', 'placeholder', ''),
                             ('Resolution', 'placeholder', ''),
                             ('Reviewer', 'full', ''),
                             ('Rules', 'full', ''),
                             ('Scope Creep', 'placeholder', ''),
                             ('Search', 'placeholder', ''),
                             ('Severity', 'full', ''),
                             ('Similars', 'placeholder', ''),
                             ('Status', 'placeholder', ''),
                             ('Subteam', 'placeholder', ''),
                             ('Test Cases', 'full', ''),
                             ('Time', 'placeholder', ''),
                             ('Today', 'placeholder', ''),
                             ('Velocity', 'full', ''),
                             ('Votes', 'placeholder', ''),
                             ('Yesterday', 'placeholder', ''),
                             ('Internals', 'full', 'Shows just the internal attributes for each card')
                            ]

        self.cardmenufonticons = {'Add Bug':       'bug',
                                  'Add Defect':    'bug',
                                  'Add Task':      'sticky-note-o',
                                  'Add Substory':  'sticky-note-o',
                                  'Add Test':      'sticky-note-o',
                                  'Attach':        'upload',
                                  'Bottom':        'arrow-circle-down',
                                  'Delete':        'remove',
                                  'Down':          'arrow-circle-down',
                                  'Execute Rules': 'external-link-square',
                                  'Expedite':      'rocket',
                                  'Give Focus':    'thumbs-o-up',
                                  'Hierarchy':     'object-group',
                                  'Internals':     'microchip',
                                  'JSON':          'download',
                                  'Push':          'hand-o-right',
                                  'Recurring':     'recycle',
                                  'Reopen':        'reply',
                                  'Split Story':   'clone',
                                  'Synchronise':   'exchange',
                                  'Top':           'arrow-circle-up',
                                  'Touch':         'hand-pointer-o',
                                  'Up':            'arrow-circle-up',
                                  'Update':        'edit',
                                  'View':          'binoculars'
                                 }
                            
        self.fonticons = {'abandoned_chart':                  'bar-chart',
                          'abandoned':                        'bar-chart',
                          'absence':                          'user',
                          'activity_stream':                  'history',
                          'add_card':                         'sticky-note-o',
                          'add_project':                      'object-group',
                          'add_resolution':                   'sticky-note-o',
                          'advanced_search':                  'search',
                          'avatar':                           'photo',
                          'backlogsorter':                    'binoculars',
                          'backlog_trend':                    'file-text-o',
                          'blockages':                        'bar-chart',
                          'bottlenecks':                      'bar-chart',
                          'budget_burn':                      'bar-chart',
                          'buffer_burn_rate':                 'bar-chart',
                          'bugs_and_defects':                 'bar-chart',
                          'burndownchart':                    'bar-chart',
                          'burnup_chart':                     'bar-chart',
                          'card_as_json':                     'sticky-note-o',
                          'card_internals':                   'sticky-note-o',
                          'cardtypes':                        'sticky-note-o',
                          'cfd':                              'bar-chart',
                          'completedcards':                   'bar-chart',
                          'controlchart':                     'bar-chart',
                          'costs_report':                     'file-text-o',
                          'cumulative_flow_diagram_chart':    'bar-chart',
                          'cycletime':                        'bar-chart',
                          'dashboard':                        'th',
                          'dashboard_settings':               'th',
                          'database_backup':                  'database',
                          'database_delete':                  'database',
                          'database_rebuild':                 'database',
                          'database_relink':                  'database',
                          'database_restore':                 'database',
                          'deferrals':                        'bar-chart',
                          'delete_card':                      'remove',
                          'delete_iteration':                 'remove',
                          'delete_project':                   'remove',
                          'delete_release':                   'remove',
                          'diary':                            'calendar',
                          'divisionoflabour':                 'bar-chart',
                          'duedateperformance':               'bar-chart',
                          'earned_business_value':            'bar-chart',
                          'emotions_chart':                   'bar-chart',
                          'epub':                             'file-pdf-o',
                          'escalation_report':                'file-text-o',
                          'export_cards_as_json':             'sticky-note-o',
                          'filter_bar_components':            'filter',
                          'filter_manager':                   'filter',
                          'hierarchy':                        'sitemap',
                          'html':                             'file-pdf-o',
                          'index':                            'th',
                          'libraries':                        'book',
                          'licences':                         'legal',
                          'jira_csv_import':                  'upload',
                          'jsonview':                         'sticky-note-o',
                          'leadtime':                         'bar-chart',
                          'listview':                         'tasks',
                          'listview_settings':                'tasks',
                          'member_as_json':                   'user',
                          'members':                          'users',
                          'pair_programming':                 'handshake-o',
                          'percentage_of_scope_complete':     'bar-chart',
                          'personal_work_in_progress_limits': 'user',
                          'project_as_json':                  'object-group',
                          'projects':                         'object-group',
                          'quality':                          'bar-chart',
                          'recidivism_rate':                  'bar-chart',
                          'releasekickoff':                   'binoculars',
                          'report_generator':                 'file-text-o',
                          'report_manager':                   'file-text-o',
                          'retrospective':                    'binoculars',
                          'roadmap':                          'binoculars',
                          'rootcauseanalysis_report':         'file-text-o',
                          'running_tested_features':          'bar-chart',
                          'save_current_filter':              'filter',
                          'search':                           'search',
                          'session_as_json':                  'exchange',
                          'sessions':                         'exchange',
                          'spc':                              'bar-chart',
                          'split_story':                      'clone',
                          'standup':                          'th',
                          'statusreport':                     'file-text-o',
                          'tagschart':                        'tags',
                          'teamautonomy':                     'bar-chart',
                          'teammembers':                      'users',
                          'themes':                           'photo',
                          'throughput':                       'bar-chart',
                          'timeline':                         'binoculars',
                          'timesheet':                        'history',
                          'times_report':                     'file-text-o',
                          'tokens':                           'ticket',
                          'update_category':                  'object-ungroup',
                          'updateiteration':                  'object-ungroup',
                          'update_project_base_attributes':   'object-group',
                          'updaterelease':                    'object-ungroup',
                          'update_subteam':                   'object-ungroup',
                          'update_teammember':                'user',
                          'upgrade':                          'support',
                          'upload_attachment':                'upload',
                          'valuefailuredemand':               'bar-chart',
                          'velocitybyeffort':                 'bar-chart',
                          'velocitychart':                    'bar-chart',
                          'wallboard':                        'columns',
                          'which_metric':                     'bar-chart'}

        self.day_of_week = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']

        self.major, self.minor, self.revision, self.build, self.date = self.read_version_ini_file()

        self.attribute_display_names = {'actualcost':           'Actual Cost',
                                        'actualcosthistory':    'Actual Cost History',
                                        'actualtime':           'Actual Time',
                                        'actualtimehistory':    'Actual Time History',
                                        'affectsversion':       'Affects Version',
                                        'blockedhistory':       'Blocked History',
                                        'blockeduntil':         'Blocked Until',
                                        'blocksparent':         'Blocks Parent',
                                        'bypassreview':         'Bypass Review',
                                        'classofservice':       'Class Of Service',
                                        'coowner':              'Co-Owner',
                                        'coownerstate':         'Co-Owner State',
                                        'coreviewer':           'Co-Reviewer',
                                        'coreviewerstate':      'Co-Reviewer State',
                                        'crmcase':              'CRM Case',
                                        'deferreduntil':        'Deferred Until',
                                        'dependsupon':          'Depends Upon',
                                        'estimatedcost':        'Estimated Cost',
                                        'estimatedcosthistory': 'Estimated Cost History',
                                        'estimatedtime':        'Estimated Time',
                                        'estimatedtimehistory': 'Estimated Time History',
                                        'externalhyperlink':    'External Hyperlink',
                                        'externalreference':    'External Reference',
                                        'fixversion':           'Fix Version',
                                        'focusby':              'Focus By',
                                        'focushistory':         'Focus History',
                                        'focusstart':           'Focus Start',
                                        'hiddenuntil':          'Hidden Until',
                                        'id':                   'ID',
                                        'lastchanged':          'Last Changed',
                                        'lastchangedby':        'Last Changed By',
                                        'lasttouched':          'Last Touched',
                                        'lasttouchedby':        'Last Touched By',
                                        'nextaction':           'Next Action',
                                        'ownerstate':           'Owner State',
                                        'reassigncoowner':      'Reassign Co-Owner',
                                        'reassigncoreviewer':   'Reassign Co-Reviewer',
                                        'reassignowner':        'Reassign Owner',
                                        'reassignreviewer':     'Reassign Reviewer',
                                        'reviewerstate':        'Reviewer State',
                                        'rootcauseanalysis':    'Root-Cause Analysis',
                                        'statehistory':         'State History',
                                        'testcases':            'Test Cases'
                                       }

        self.colours = ['AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige',
                        'Bisque', 'Black', 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown',
                        'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', 'Coral',
                        'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan',
                        'DarkGoldenRod', 'DarkGray', 'DarkGreen', 'DarkKhaki', 'DarkMagenta',
                        'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', 'DarkRed', 'DarkSalmon',
                        'DarkSeaGreen', 'DarkSlateBlue', 'DarkSlateGray', 'DarkTurquoise',
                        'DarkViolet', 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DodgerBlue',
                        'FireBrick', 'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro',
                        'GhostWhite', 'Gold', 'GoldenRod', 'Gray', 'Green', 'GreenYellow',
                        'HoneyDew', 'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender',
                        'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral',
                        'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGreen', 'LightPink',
                        'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray',
                        'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta',
                        'Maroon', 'MediumAquaMarine', 'MediumBlue', 'MediumOrchid', 'MediumPurple',
                        'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise',
                        'MediumVioletRed', 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin',
                        'NavajoWhite', 'Navy', 'OldLace', 'Olive', 'OliveDrab', 'Orange',
                        'OrangeRed', 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise',
                        'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum',
                        'PowderBlue', 'Purple', 'RebeccaPurple', 'Red', 'RosyBrown', 'RoyalBlue',
                        'SaddleBrown', 'Salmon', 'SandyBrown', 'SeaGreen', 'SeaShell', 'Sienna',
                        'Silver', 'SkyBlue', 'SlateBlue', 'SlateGray', 'Snow', 'SpringGreen',
                        'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet',
                        'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen']

        self.swim_lanes_attributes = [('Category',         'category'),
                                      ('Class Of Service', 'classofservice'),
                                      ('Customer',         'customer'),
                                      ('Iteration',        'iteration'),
                                      ('Release',          'release'),
                                      ('Subteam',          'subteam'),
                                      ('Type',             'type'),
                                      ('Team Member',      'username')]

    def abandoned_chart(self, number_of_days="", division="", rawdatarequired="", csvrequired=""):
        """This function is called by abandoned()"""
        username = Kanbanara.check_authentication('/metrics/abandoned')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        number_of_days, division = self.metrics_settings(session_document, number_of_days, division)
        member_document = Kanbanara.get_member_document(self, session_document)
        project, release, iteration = self.get_member_project_release_iteration(member_document)
        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', [])
        closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
        content = []
        content.append(self.insert_page_title_and_online_help(session_document, 'abandoned_chart',
                                                              'Abandoned'))
        content.append(self.assemble_chart_buttons('abandoned', number_of_days, division, project,
                                                   release, iteration))
        states = self.get_custom_states_mapped_onto_metastates(['closed'])
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                 states)
        owner_reviewer_search_criteria['resolution'] = 'Abandoned'
        chart = pygal.Line(x_label_rotation=90, style=LightStyle)
        epoch = datetime.datetime.utcnow()
        chartstart_epoch = epoch - (self.timedelta_day * number_of_days)
        day_labels = self.assemble_chart_day_labels(number_of_days, division)
        chart.title = self.assemble_chart_title('Abandoned', project, release, iteration, day_labels)
        chart.x_labels = day_labels
        owner_reviewer_search_criteria['project'] = project
        day_count = 0
        day_values = []
        while day_count < number_of_days:
            day_count += division
            past_epoch = chartstart_epoch + (self.timedelta_day * day_count)
            number_of_documents = 0
            for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                state_metrics = self.get_state_metrics(condensed_column_states, card_document['statehistory'])
                for closed_state in closed_states:
                    closed = state_metrics.get(closed_state, 0)
                    if closed:
                        break

                if closed and closed <= past_epoch:
                    number_of_documents += 1

            day_values.append({'value': number_of_documents, 'tooltip': str(number_of_documents)})

        chart.add(project, day_values)
        chart.render_to_file(os.path.join(self.current_dir, '..', 'svgcharts', username+'_'+str(int(epoch.timestamp()))+'.svg'))
        content.append(self.display_chart(username, epoch))
        if rawdatarequired:
            content.append('<p>Raw Data Output to be completed!</p>')

        if csvrequired:
            content.append('<p>CSV Output to be completed!</p>')

        return "".join(content)

    def add_recent_activity_entry(self, activity_entry):
        '''Allows a recent activity entry to be added'''
        (past_time, username, doc_id, mode) = activity_entry
        if (past_time, username, doc_id, mode) not in self.recent_activities:
            self.recent_activities.append((past_time, username, doc_id, mode))

    @staticmethod
    def all_testcases_accepted(testcases):
        """Returns true if all of a card's test cases have been accepted"""
        for testcase_document in testcases:
            if testcase_document['state'] != 'accepted':
                return False

        return True

    @staticmethod
    def append_state_history(statehistory, state, username):
        """Append the latest state a card has just entered to its state history"""
        epoch = datetime.datetime.utcnow()
        if statehistory:
            latest_statehistory_document = statehistory[-1]
            if latest_statehistory_document['state'] != state:
                statehistory.append({'datetime': epoch, 'state': state, 'username': username})

        else:
            statehistory = [{'datetime': epoch, 'state': state, 'username': username}]

        return statehistory

    def ascertain_card_menu_items(self, card_document, member_document):
        """Ascertain the items required for a particular card's menu"""
        card_id = card_document.get('id', '')
        blocked = card_document.get('blocked', '')
        blockeduntil = card_document.get('blockeduntil', 0)
        coowner = card_document.get('coowner', '')
        expedite = card_document.get('expedite', '')
        hiddenuntil = card_document.get('hiddenuntil', 0)
        owner = card_document.get('owner', '')
        parent = card_document.get('parent', '')
        rules = card_document.get('rules', '')
        project = card_document['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', [])
        release = card_document.get('release', '')
        resolution = card_document.get('resolution', '')
        state = card_document.get('state', '')
        metastate = self.get_corresponding_metastate(project_document, state)
        card_type = card_document.get('type', '')
        buttons = {'delete': True, 'focus': True, 'touch': True}
        if re.search('^'+project+r'-\w{32}$', card_id):
            buttons['synchronise'] = True

        if hiddenuntil:
            buttons['unhide'] = True

        if blocked or blockeduntil:
            buttons['unblock'] = True

        if metastate == 'closed' or (condensed_column_states and state == condensed_column_states[-1]):
            buttons['view'] = True
            buttons['recurring'] = True
            buttons['reopen'] = True
            if not resolution:
                buttons['add resolution'] = True

            if release:
                buttons['delete release'] = True

        else:
            buttons['hierarchy'] = True
            buttons['update'] = True
            buttons['view'] = True
            if project:
                if expedite:
                    buttons['unexpedite'] = True
                elif not self.cards_collection.find({'project': project, 'expedite': True}).count():
                    buttons['expedite'] = True

            if parent:
                buttons['parent'] = True

            if rules:
                buttons['execute rules'] = True

            if metastate == 'backlog':
                buttons['attach'] = True
                if card_type == 'feature':
                    buttons['add story'] = True
                elif card_type == 'story':
                    buttons['split story'] = True

                _, _, _, _, next_state = self.get_associated_state_information(state)
                owner_count, _, _, max_wip_limit = self.get_document_count(next_state, [])
                if isinstance(max_wip_limit, int):
                    if (max_wip_limit == -1) or (owner_count < max_wip_limit):
                        buttons['push'] = True

            elif metastate in ['defined', 'analysis']:
                buttons['attach'] = True
                if card_type == 'feature':
                    buttons['add story'] = True
                elif card_type == 'story':
                    buttons['split story'] = True
                    buttons['add substory'] = True
                    buttons['add task'] = True
                elif card_type in ['enhancement', 'defect']:
                    buttons['add task'] = True
                elif card_type == 'task':
                    buttons['add subtask'] = True

                if metastate == 'analysis':
                    _, _, _, _, next_state = self.get_associated_state_information(state)
                    owner_count, _, _, max_wip_limit = self.get_document_count(next_state, [])
                    if isinstance(max_wip_limit, int) and (max_wip_limit == -1 or owner_count < max_wip_limit):
                        buttons['push'] = True

            elif metastate in ['analysed', 'design', 'designed', 'development', 'developed',
                               'unittesting', 'integrationtesting', 'systemtesting',
                               'acceptancetesting']:
                buttons['add test'] = True
                buttons['add defect'] = True
                buttons['add bug'] = True
                buttons['attach'] = True
                if card_type == 'feature':
                    buttons['add story'] = True
                elif card_type == 'enhancement':
                    buttons['add task'] = True
                elif card_type == 'story':
                    buttons['add substory'] = True
                    buttons['add task'] = True
                    if metastate in ['analysed', 'development']:
                        buttons['split story'] = True

                elif card_type == 'task':
                    buttons['add subtask'] = True

                if metastate == 'development':
                    _, _, _, _, next_state = self.get_associated_state_information(state)
                    owner_count, _, _, max_wip_limit = self.get_document_count(next_state, [])
                    if isinstance(max_wip_limit, int):
                        if (max_wip_limit == -1) or (owner_count < max_wip_limit):
                            buttons['push'] = True
                            
                    else:
                        buttons['push'] = True

                elif metastate in ['unittesting', 'integrationtesting', 'systemtesting',
                                   'acceptancetesting']:
                    _, _, _, _, next_state = self.get_associated_state_information(state)
                    owner_count, _, _, max_wip_limit = self.get_document_count(next_state, [])
                    if isinstance(max_wip_limit, int):
                        if (max_wip_limit == -1) or (owner_count < max_wip_limit):
                            if member_document and "teammember" in member_document and ((owner and owner == member_document['teammember']) or (coowner and coowner == member_document['teammember'])):
                                True
                            else:
                                buttons['push'] = True

            elif metastate in ['unittestingaccepted', 'integrationtestingaccepted',
                               'systemtestingaccepted', 'acceptancetestingaccepted']:
                buttons['push'] = True
                buttons['attach'] = True

            if metastate != 'closed' and condensed_column_states and state != condensed_column_states[-1]:
                for button in ['top', 'up', 'down', 'bottom']:
                    buttons[button] = True

        return buttons

    def ascertain_comment_class(self, comment_document, owner, coowner):
        '''Ascertain the class required for a particular comment'''
        comment_class = ""
        epoch = datetime.datetime.utcnow()
        if comment_document['datetime'] > epoch-self.timedelta_day:
            if comment_document['username'] == owner or comment_document['username'] == coowner:
                comment_class = 'commentownernew'
            else:
                comment_class = 'commentanothernew'

        elif comment_document['datetime'] > epoch-self.timedelta_week:
            if comment_document['username'] == owner or comment_document['username'] == coowner:
                comment_class = 'commentownerrecent'
            else:
                comment_class = 'commentanotherrecent'

        else:
            if comment_document['username'] in [owner, coowner]:
                comment_class = 'commentowner'
            else:
                comment_class = 'commentanother'

        return comment_class

    def assemble_absenteeism_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                         someone_else_is_stuck_status, projection):
        """ Displays a card affected by a user's absence on the kanban board """
        content = []
        datetime_now = datetime.datetime.utcnow()
        absenteeisms = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            for user in [owner, coowner, reviewer, coreviewer]:
                if user:
                    user_absence_start_date, user_absence_end_date = self.get_user_absence_dates(user)
                    if user_absence_start_date:
                        if user_absence_end_date and user_absence_start_date < datetime_now < user_absence_end_date:
                            date_format = self.convert_time_to_display_format(user_absence_end_date)
                            absenteeisms.append(user+' is absent until '+date_format)
                        elif datetime_now > user_absence_start_date and not user_absence_end_date:
                            absenteeisms.append(user+' is absent until further notice')

            if absenteeisms:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('activity', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                for absenteeism in absenteeisms:
                    content.append('<sup class="absenteeism">'+absenteeism+'</sup>')

                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_activity_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """ Displays a card recently updated on the kanban board """
        content = []
        epoch = datetime.datetime.utcnow()
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            lastchanged = card_document.get('lastchanged', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            if lastchanged > epoch - self.timedelta_day:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('activity', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr><tr><th>')
                content.append(self.show_category_in_top_right(project, category))
                content.append('<sup class="new">Updated!</sup></th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_artifact_clash_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                            someone_else_is_stuck_status, projection):
        """Displays a card on the kanban board whose artifacts clash with another card"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            artifacts = card_document.get('artifacts', [])
            identical_artifacts = []
            closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
            for other_card_document in self.cards_collection.find(
                    {'project': card_document['project'],
                     '_id': {'$ne': ObjectId(doc_id)},
                     'type': card_document['type'],
                     'state': {'$nin': closed_states},
                     'artifacts': {"$exists": True, '$ne': []}
                    }):
                other_artifacts = other_card_document['artifacts']
                matches = list(set(artifacts).intersection(set(other_artifacts)))
                for match in matches:
                    identical_artifacts.append((match, other_card_document['title'], other_card_document['id']))

            if identical_artifacts:
                doc_id, blocked, coowner, coreviewer, deferred, expedite, owner, parent = self.get_card_attribute_values(card_document,
                    ['_id', 'blocked', 'coowner', 'coreviewer', 'deferred', 'expedite', 'owner', 'parent'])
                priority, release, reviewer, severity, state, title, card_type = self.get_card_attribute_values(card_document,
                    ['priority', 'release', 'reviewer', 'severity', 'state', 'title', 'type'])
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(card_document['project'], state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id,
                                                        card_document['project'], state, priority,
                                                        severity, expedite, blocked, deferred,
                                                        release))

                table_initialise, _ = self.assign_card_table_class('reopened', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append('</th><th></th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody><tr><td>')
                if identical_artifacts:
                    content.append('<h4>Cards With Identical Artifacts:</h4>')
                    content.append('<ul>')
                    for (match, other_title, other_id) in identical_artifacts:
                        content.append(f'<li><a href="/cards/view_card?id={other_id}">{other_title}</a> [{match}]</li>')

                    content.append('</ul>')

                content.append('</td></tr></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_attachments_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                         someone_else_is_stuck_status, projection):
        """ Displays a card in possession of one or more attachments on the kanban board """
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            card_id = card_document.get('id', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            attach_dir = os.path.join(self.current_dir, '..', 'attachments', project, card_id)
            if os.path.exists(attach_dir):
                attachments = os.listdir(attach_dir)
            else:
                attachments = []

            if attachments:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('attachments', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append('</th><th>')
                content.append(self.show_category_in_top_right(project, category))
                content.append('<sup class="new">Updated!</sup></th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr><tr><th colspan="2"><table class="admin">')
                for attachment in attachments:
                    content.append('<tr><td><p class="'+fontsize+'"><a href="/attachments/'+project+'/'+card_id+'/'+attachment+'">'+attachment+'</a></p></td></tr>')

                content.append('</table></th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_attributes_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                        someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority,
                                                    severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('attributes', session_document,
                                                               card_document, mode, state, card_type,
                                                               owner, coowner, reviewer, coreviewer,
                                                               member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')

            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document,
                                                   buttons, 'index'))
            content.append('</th></tr></thead><tbody><tr><td>')
            content.append('<table>')
            for key, value in sorted(card_document.items()):
                if value and key not in ['_id', 'history', 'priority', 'state']:
                    content.append('<tr><th valign="top"><p class="'+fontsize+'">'+self.displayable_key(key)+'</p></th>')
                    if key in ['deadline', 'lastchanged', 'nextaction']:
                        date_format = self.convert_time_to_display_format(value)
                        content.append('<td><p class="'+fontsize+'left">'+date_format+'</p></td>')
                    else:
                        content.append(f'<td><p class="{fontsize}left">{value}</p></td>')

                    content.append('</tr>')

            content.append('</table>')
            content.append('</td></tr></tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_avatar_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                    someone_else_is_stuck_status, projection):
        """Assemble an avatar-based card for the kanban board"""
        epoch = datetime.datetime.utcnow()
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            resolution = card_document.get('resolution', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            project_document = self.projects_collection.find_one({'project': project})
            metastate = self.get_corresponding_metastate(project_document, state)
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('avatar', session_document, card_document, mode, state, card_type, owner, coowner, reviewer, coreviewer, member_document, projection)
            content.append(table_initialise)

            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th>')
            if metastate == 'closed' and resolution:
                content.append('<sup class="resolution">'+resolution+'</sup>')

            content.append(self.show_category_in_top_right(project, category))
            content.append(self.insert_new_recent_days_old_message(card_document, state, epoch))
            content.append('</th></tr><tr><th colspan="2">')

            if usertypes:
                owner_avatar_code = ""
                coowner_avatar_code = ""
                reviewer_avatar_code = ""
                coreviewer_avatar_code = ""
                for image_file_format in ['.png', '.gif', '.jpg']:
                    if not owner_avatar_code and 'owner' in usertypes and owner:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+owner.replace(' ', '')+image_file_format):
                            owner_avatar_code = '<img src="/images/avatars/'+owner.replace(' ', '')+image_file_format+'" height="26" width="26" title="'+owner+'">'

                    if not coowner_avatar_code and 'coowner' in usertypes and coowner:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+coowner.replace(' ', '')+image_file_format):
                            coowner_avatar_code = '<img src="/images/avatars/'+coowner.replace(' ', '')+image_file_format+'" height="26" width="26" title="'+coowner+'">'

                    if not reviewer_avatar_code and 'reviewer' in usertypes and reviewer:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+reviewer.replace(' ', '')+image_file_format):
                            reviewer_avatar_code = '<img src="/images/avatars/'+reviewer.replace(' ', '')+image_file_format+'" height="26" width="26" title="'+reviewer+'">'

                    if not coreviewer_avatar_code and 'coreviewer' in usertypes and coreviewer:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+coreviewer.replace(' ', '')+image_file_format):
                            coreviewer_avatar_code = '<img src="/images/avatars/'+coreviewer.replace(' ', '')+image_file_format+'" height="26" width="26" title="'+coreviewer+'">'

                if owner and not owner_avatar_code:
                    owner_avatar_code = '<img src="/images/avatars/default.jpg" height="26" width="26" title="'+owner+'">'

                if coowner and not coowner_avatar_code:
                    coowner_avatar_code = '<img src="/images/avatars/default.jpg" height="26" width="26" title="'+coowner+'">'

                if reviewer and not reviewer_avatar_code:
                    reviewer_avatar_code = '<img src="/images/avatars/default.jpg" height="26" width="26" title="'+reviewer+'">'

                if coreviewer and not coreviewer_avatar_code:
                    coreviewer_avatar_code = '<img src="/images/avatars/default.jpg" height="26" width="26" title="'+coreviewer+'">'

                if owner_avatar_code or coowner_avatar_code or reviewer_avatar_code or coreviewer_avatar_code:
                    content.append('<table><tr>')
                    if owner_avatar_code:
                        content.append('<td align="center" rowspan="2">'+owner_avatar_code+'</td><th align="center">Owner</th>')

                    if coowner_avatar_code:
                        content.append('<td align="center" rowspan="2">'+coowner_avatar_code+'</td><th align="center">Co-Owner</th>')

                    if reviewer_avatar_code:
                        content.append('<td align="center" rowspan="2">'+reviewer_avatar_code+'</td><th align="center">Reviewer</th>')

                    if coreviewer_avatar_code:
                        content.append('<td align="center" rowspan="2">'+coreviewer_avatar_code+'</td><th align="center">Co-Reviewer</th>')

                    content.append('</tr><tr>')
                    if owner_avatar_code:
                        content.append('<td align="center">'+owner+'</td>')

                    if coowner_avatar_code:
                        content.append('<td align="center">'+coowner+'</td>')

                    if reviewer_avatar_code:
                        content.append('<td align="center">'+reviewer+'</td>')

                    if coreviewer_avatar_code:
                        content.append('<td align="center">'+coreviewer+'</td>')

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

            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr></thead>')
            content.append('<tbody></tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_blockages_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                       someone_else_is_stuck_status, projection):
        """Assemble a blockages card for the 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)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            if blocked:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('blockages', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append('</th><th>')
                content.append('<sup class="blocked" title="'+blocked+'">Blocked</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_bypassreviews_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                           someone_else_is_stuck_status, projection):
        """Displays on the kanban board just those cards who have their bypass reviews attribute set"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id, blocked, bypassreview, coowner, coreviewer, deferred, expedite, owner, parent, priority, project, release, reviewer, severity, state, title, type = self.get_card_attribute_values(card_document, ['_id', 'blocked', 'bypassreview', 'coowner', 'coreviewer', 'deferred', 'expedite', 'owner', 'parent',
                                                      'priority', 'project', 'release', 'reviewer', 'severity', 'state', 'title', 'type'])
            if bypassreview:

                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('bypassreviews', session_document, card_document,
                                                                   mode, state, type, owner, coowner, reviewer,
                                                                   coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                content.append('<sup class="bypassreview">Bypass Review</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_capitals_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """Displays a card with its title in upper case on the kanban board"""
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority,
                                                    severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('new', session_document,
                                                               card_document, mode, state,
                                                               card_type, owner, coowner, reviewer,
                                                               coreviewer, member_document,
                                                               projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document,
                                                   buttons, 'index'))
            content.append('</th><th></th></tr><tr><th colspan="2">')
            displayable_title = self.insert_card_title(session_document, title, parent, fontsize).upper()
            displayable_title = displayable_title.replace('&RARR;', '&rarr;')
            content.append(displayable_title)
            content.append('</th></tr></thead><tbody></tbody></table></div>')

        return "".join(content)

    def assemble_card_hierarchical_title(self, title, parent):
        joiner = '<b>&rArr;</b>'
        if not title:
            title = '?'

        if not parent:
            return html.escape(title, quote=True)
        else:
            parent_title = ""
            parent_document = self.cards_collection.find_one({'id': parent})
            if parent_document and 'title' in parent_document and parent_document['title']:
                parent_title = '<small><i>'+html.escape(parent_document['title'], quote=True)+'</i> '+joiner+'</small> '

            if not(parent_document and 'parent' in parent_document and parent_document['parent']):
                return parent_title+html.escape(title, quote=True)
            else:
                grandparent_title = ""
                grandparent_document = self.cards_collection.find_one({'id': parent_document['parent']})
                if grandparent_document and 'title' in grandparent_document and grandparent_document['title']:
                    grandparent_title = '<small><i>'+html.escape(grandparent_document['title'], quote=True)+'</i> '+joiner+'</small> '

                return grandparent_title+parent_title+html.escape(title, quote=True)

    def assemble_card_hierarchy(self, card_id):
        hierarchy = card_id
        for card_document in self.cards_collection.find({'id': card_id}):
            parent = card_document.get('parent', '')
            if parent:
                parent_hierarchy = self.assemble_card_hierarchy(parent)
                hierarchy = parent_hierarchy+' '+hierarchy

            break

        return hierarchy

    def assemble_card_menu(self, member_document, card_document, buttons, destination=""):
        """Assemble the menu for a given card"""
        content = []
        entries = []
        classofservice = card_document.get('classofservice', '')
        deadline = card_document.get('deadline', '')
        doc_id = card_document.get('_id', '')
        crmcase = card_document.get('crmcase', '')
        escalation = card_document.get('escalation', '')
        externalhyperlink = card_document.get('externalhyperlink', '')
        externalreference = card_document.get('externalreference', '')
        id = card_document.get('id', '')
        iteration = card_document.get('iteration', '')
        nextaction = card_document.get('nextaction', '')
        project = card_document.get('project', '')
        release = card_document.get('release', '')
        startby = card_document.get('startby', '')
        subteam = card_document.get('subteam', '')
        role = ""
        for project_document in member_document['projects']:
            if project_document['project'] == project:
                role = project_document['role']
                break

        if 'parent' in buttons:
            parent = card_document.get('parent', '')
            entries.append(self.assemble_card_menu_option('view_card', 'Parent', '', parent, ''))

        if 'view' in buttons:
            entries.append(self.assemble_card_menu_option('view_card', 'View', '', id, ''))

        if 'update' in buttons and role not in ['Guest']:
            entries.append(self.assemble_card_menu_option('update_card', 'Update', doc_id, '', ''))

        if 'synchronise' in buttons:
            entries.append(self.assemble_card_menu_option('synchronise_card', 'Synchronise', doc_id, '', destination))

        if 'reopen' in buttons:
            entries.append(self.assemble_card_menu_option('reopen_card', 'Reopen', doc_id, '', ''))

        if 'execute rules' in buttons:
            entries.append(self.assemble_card_menu_option('execute_card_rules_wrapper', 'Execute Rules', doc_id, '', ''))

        if 'hierarchy' in buttons:
            entries.append(self.assemble_card_menu_option('hierarchy', 'Hierarchy', doc_id, '', ''))

        if role not in ['Guest']:
            if 'focus' in buttons:
                if 'focusby' in card_document and card_document['focusby'] == member_document["username"]:
                    entries.append(self.assemble_card_menu_option('take_focus', 'Take Focus', doc_id, '', ''))
                else:
                    entries.append(self.assemble_card_menu_option('give_focus', 'Give Focus', doc_id, '', ''))

            if 'touch' in buttons:
                entries.append(self.assemble_card_menu_option('touch', 'Touch', doc_id, '', ''))

            if 'recurring' in buttons:
                entries.append(self.assemble_card_menu_option('recurring', 'Recurring', doc_id, '', ''))

            if 'push' in buttons:
                entries.append(self.assemble_card_menu_option('push', 'Push', doc_id, '', ''))

            if 'add story' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Story',
                                                                      doc_id, id, 'story', project,
                                                                      release, iteration, crmcase,
                                                                      escalation, externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'add substory' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Substory',
                                                                      doc_id, id, 'substory',
                                                                      project, release, iteration,
                                                                      crmcase, escalation,
                                                                      externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'add task' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Task',
                                                                      doc_id, id, 'task', project,
                                                                      release, iteration, crmcase,
                                                                      escalation, externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'add subtask' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Subtask',
                                                                      doc_id, id, 'subtask',
                                                                      project, release, iteration,
                                                                      crmcase, escalation,
                                                                      externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'add test' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Test',
                                                                      doc_id, id, 'test', project,
                                                                      release, iteration, crmcase,
                                                                      escalation, externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'add defect' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Defect',
                                                                      doc_id, id, 'defect', project,
                                                                      release, iteration, crmcase,
                                                                      escalation, externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'add resolution' in buttons:
                entries.append(self.assemble_card_menu_option('add_resolution', 'Add Resolution', doc_id, '', ''))

            if 'add bug' in buttons:
                entries.append(self.assemble_card_menu_complex_option('add_card', 'Add Bug', doc_id,
                                                                      id, 'bug', project, release,
                                                                      iteration, crmcase, escalation,
                                                                      externalreference,
                                                                      externalhyperlink, nextaction,
                                                                      deadline, startby,
                                                                      classofservice, subteam))

            if 'attach' in buttons:
                entries.append(self.assemble_card_menu_option('upload_attachment', 'Attach', doc_id, '', ''))

            if 'split story' in buttons:
                entries.append(self.assemble_card_menu_option('split_story', 'Split Story', doc_id, '', ''))

            if 'unhide' in buttons:
                entries.append(self.assemble_card_menu_option('unhide_card', 'Unhide', doc_id, '', ''))

            if 'unblock' in buttons:
                entries.append(self.assemble_card_menu_option('unblock_card', 'Unblock', doc_id, '', ''))

        if 'delete' in buttons and role in ['Deputy Project Manager', 'Project Manager']:
            entries.append(self.assemble_card_menu_option('delete_card', 'Delete', doc_id, '', destination))

        if 'delete release' in buttons and role == 'Project Manager':
            # TODO - DO I REALLY NEED THIS ON A CARD MENU?
            entries.append('<li><form action="/kanban/delete_release" method="post"><input type="hidden" name="project" value="'+project+'"><input type="hidden" name="release" value="'+release+'"><input type="submit" value="Delete Release"></form></li>')

        entries.append(self.assemble_card_menu_option('card_internals', 'Internals', doc_id, '', ''))
        entries.append(self.assemble_card_menu_option('card_as_json', 'JSON', doc_id, '', ''))
        if role not in ['Guest']:
            if 'expedite' in buttons:
                entries.append(self.assemble_card_menu_option('expedite', 'Expedite', doc_id, '', ''))
            elif 'unexpedite' in buttons:
                entries.append(self.assemble_card_menu_option('unexpedite', 'Unexpedite', doc_id, '', ''))

        if role not in ['Guest']:
            if 'top' in buttons:
                entries.append(self.assemble_card_menu_option('move_card_to_top', 'Top', doc_id, '', ''))

            if 'up' in buttons:
                entries.append(self.assemble_card_menu_option('move_card_up', 'Up', doc_id, '', ''))

            if 'down' in buttons:
                entries.append(self.assemble_card_menu_option('move_card_down', 'Down', doc_id, '', ''))

            if 'bottom' in buttons:
                entries.append(self.assemble_card_menu_option('move_card_to_bottom', 'Bottom', doc_id, '', ''))

        num_of_entries = len(entries)
        max_per_row = 3
        for i in reversed(range(3, 6)):
            j = i + 1
            if num_of_entries % j == 0:
                max_per_row = j

        content.append('<ul class="cardmenu"><li><a href="#"><span class="fa fa-navicon fa-lg"></span></a>')
        content.append('<div class="megamenu">')
        content.append('<table><tr><td>')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Card&nbsp;Menu</h3></div>')
        content.append('</td>')
        content.append('<td valign="top"><table><tr>')
        for i, entry in enumerate(entries, start=1):
            remainder = i % max_per_row
            content.append(f'<td>{entry}</td>')
            if remainder == 0 and i < num_of_entries:
                content.append('</tr><tr>')

        content.append('</tr></table></td>')
        content.append('</table>')
        content.append('</div></li></ul>')
        return "".join(content)

    def assemble_card_menu_complex_option(self, page, text, doc_id, card_id, card_type, project,
                                          release, iteration, crmcase, escalation,
                                          externalreference, externalhyperlink, nextaction,
                                          deadline, startby, classofservice, subteam):
        """Assembles the more complex menu options for a card menu"""
        menu_option = []
        menu_option.append(f'<form class="cardmenu" action="/{self.get_page_component(page)}/{page}" method="post">')
        if doc_id:
            menu_option.append('<input type="hidden" name="doc_id" value="'+str(doc_id)+'">')

        if card_id:
            menu_option.append('<input type="hidden" name="id" value="'+card_id+'">')

        if card_type:
            menu_option.append('<input type="hidden" name="type" value="'+card_type+'">')

        if project:
            menu_option.append('<input type="hidden" name="project" value="'+project+'">')

        if release:
            menu_option.append('<input type="hidden" name="release" value="'+release+'">')

        if iteration:
            menu_option.append('<input type="hidden" name="iteration" value="'+iteration+'">')

        if crmcase:
            menu_option.append('<input type="hidden" name="crmcase" value="'+crmcase+'">')

        if escalation:
            menu_option.append('<input type="hidden" name="escalation" value="'+escalation+'">')

        if externalreference:
            menu_option.append('<input type="hidden" name="externalreference" value="'+externalreference+'">')

        if externalhyperlink:
            menu_option.append('<input type="hidden" name="externalhyperlink" value="'+externalhyperlink+'">')

        if nextaction:
            menu_option.append('<input type="hidden" name="nextaction" value="'+str(nextaction)+'">')

        if deadline:
            menu_option.append('<input type="hidden" name="deadline" value="'+str(deadline)+'">')

        if startby:
            menu_option.append('<input type="hidden" name="startby" value="'+str(startby)+'">')

        if classofservice:
            menu_option.append('<input type="hidden" name="classofservice" value="'+classofservice+'">')

        if subteam:
            menu_option.append('<input type="hidden" name="subteam" value="'+subteam+'">')

        if text in ['Delete']:
            menu_option.append(f'<button class="warning" type="submit" value="{text}">')
        else:
            menu_option.append(f'<button type="submit" value="{text}">')
            
        if text in self.cardmenufonticons and self.cardmenufonticons[text]:
            menu_option.append(f'<span class="fa fa-{self.cardmenufonticons[text]} fa-lg"></span>&nbsp;')
        else:
            menu_option.append('<span class="fa fa-sticky-note-o fa-lg"></span>&nbsp;')

        menu_option.append(f'{text.replace(" ", "&nbsp;")}</button></form>')
        return ''.join(menu_option)

    def assemble_card_menu_option(self, page, text, doc_id, id, destination):
        """Assembles the simpler menu options for a card menu"""
        menu_option = []

        menu_option.append(f'<form class="cardmenu" action="/{self.get_page_component(page)}/{page}" method="post">')
        if doc_id:
            menu_option.append('<input type="hidden" name="doc_id" value="'+str(doc_id)+'">')

        if id:
            menu_option.append('<input type="hidden" name="id" value="'+id+'">')

        if destination:
            menu_option.append('<input type="hidden" name="destination" value="'+destination+'">')

        if text in ['Delete']:
            menu_option.append(f'<button class="warning" type="submit" value="{text}">')
        else:
            menu_option.append(f'<button type="submit" value="{text}">')

        if text in self.cardmenufonticons and self.cardmenufonticons[text]:
            menu_option.append(f'<span class="fa fa-{self.cardmenufonticons[text]} fa-lg"></span>&nbsp;')
        else:
            menu_option.append('<span class="fa fa-sticky-note-o fa-lg"></span>&nbsp;')

        menu_option.append(f'{text.replace(" ", "&nbsp;")}</button></form>')
        return ''.join(menu_option)

    def assemble_card_warning_statements(self, statehistory, blocked_reason, blocked,
                                         blockedhistory, blockeduntil, deferred, deferreduntil,
                                         coowner, coreviewer, deadline, inherited_deadline,
                                         inherited_deadline_entity, lastchanged, nextaction, owner,
                                         project, reviewer, session_document,
                                         someone_else_is_stuck_status, state, stuck, card_type,
                                         resolution):
        """Assembles a card's warning statements"""
        warning_statements = []
        days_stale = 0
        _, _, centric, preceding_state, next_state = self.get_associated_state_information(state)
        epoch = datetime.datetime.utcnow()
        if someone_else_is_stuck_status:
            if centric == 'Owner':
                if owner and coowner:
                    warning_statements.append(f'<blink>{owner} and {coowner} are stuck!</blink>')
                elif owner:
                    warning_statements.append(f'<blink>{owner} is stuck!</blink>')
                elif coowner:
                    warning_statements.append(f'<blink>{coowner} is stuck!</blink>')
                else:
                    warning_statements.append('<blink>Someone else is stuck!</blink>')

            elif centric == 'Reviewer':
                if reviewer and coreviewer:
                    warning_statements.append(f'<blink>{reviewer} and {coreviewer} are stuck!</blink>')
                elif reviewer:
                    warning_statements.append(f'<blink>{reviewer} is stuck!</blink>')
                elif coreviewer:
                    warning_statements.append(f'<blink>{coreviewer} is stuck!</blink>')
                else:
                    warning_statements.append('<blink>Someone else is stuck!</blink>')

        else:
            if stuck:
                member_document = Kanbanara.get_member_document(self, session_document)
                if member_document and 'teammember' in member_document:
                    if centric == 'Owner':
                        if (owner and member_document["teammember"] == owner) or (coowner and member_document["teammember"] == coowner):
                            warning_statements.append(f'<b title="{stuck}">I\'m stuck!</b>')
                        else:
                            if owner and coowner:
                                warning_statements.append(f'<blink title="{stuck}">{owner} and {coowner} are stuck!</blink>')
                            elif owner:
                                warning_statements.append(f'<blink title="{stuck}">{owner} is stuck!</blink>')
                            elif coowner:
                                warning_statements.append(f'<blink title="{stuck}">{coowner} is stuck!</blink>')
                            else:
                                warning_statements.append(f'<blink title="{stuck}">Someone else is stuck!</blink>')

                    elif centric == 'Reviewer':
                        if (reviewer and member_document["teammember"] == reviewer) or (coreviewer and member_document["teammember"] == coreviewer):
                            warning_statements.append(f'<b title="{stuck}">I\'m stuck!</b>')
                        else:
                            if reviewer and coreviewer:
                                warning_statements.append(f'<blink title="{stuck}">{reviewer} and {coreviewer} are stuck!</blink>')
                            elif reviewer:
                                warning_statements.append(f'<blink title="{stuck}">{reviewer} is stuck!</blink>')
                            elif coreviewer:
                                warning_statements.append(f'<blink title="{stuck}">{coreviewer} is stuck!</blink>')
                            else:
                                warning_statements.append(f'<blink title="{stuck}">Someone else is stuck!</blink>')

            if deferred:
                if deferreduntil:
                    date_format = self.convert_time_to_display_format(deferreduntil)
                    warning_statements.append(f'Deferred ({deferred}) until {date_format}')
                else:
                    warning_statements.append(deferred)

            if blocked_reason:
                warning_statements.append(blocked_reason)
            elif blocked:
                if blockeduntil:
                    date_format = self.convert_time_to_display_format(blockeduntil)
                    warning_statements.append(f'Blocked ({blocked}) until {date_format}')
                else:
                    if blockedhistory:
                        epoch = datetime.datetime.utcnow()
                        latest_blockedhistory_document = blockedhistory[-1]
                        blocked_date = latest_blockedhistory_document['datetime']
                        no_of_days_blocked = int((epoch-int(blocked_date))/self.timedelta_day)
                        if no_of_days_blocked == 1:
                            warning_statements.append(f'Blocked ({blocked}) for {no_of_days_blocked} day')
                        elif no_of_days_blocked > 1:
                            warning_statements.append(f'Blocked ({blocked}) for {no_of_days_blocked} days')

                    else:
                        warning_statements.append(blocked)

            if next_state and nextaction:
                if nextaction < epoch:
                    days_different = int((epoch-nextaction)/self.timedelta_day)
                    if days_different == 0:
                        warning_statements.append('Requires attention today!')
                    elif days_different == 1:
                        warning_statements.append('Required attention yesterday!')
                    else:
                        warning_statements.append(f'Required attention {days_different} days ago!')

            if preceding_state and next_state:
                if not owner:
                    warning_statements.append('An owner should be assigned!')
                elif owner and reviewer and owner == reviewer:
                    warning_statements.append(f'An owner can not review their own {card_type}!')
                elif owner and coreviewer and owner == coreviewer:
                    warning_statements.append(f'An owner can not review their own {card_type}!')
                elif coowner and reviewer and coowner == reviewer:
                    warning_statements.append(f'An owner can not review their own {card_type}!')
                elif coowner and coreviewer and coowner == coreviewer:
                    warning_statements.append(f'An owner can not review their own {card_type}!')

            if centric == 'Reviewer' and not reviewer:
                warning_statements.append('A reviewer should be assigned!')

            if next_state:
                if deadline:
                    if deadline < epoch:
                        days_different = int((epoch-deadline)/self.timedelta_day)
                        if days_different == 0:
                            warning_statements.append('Due today!')
                        elif days_different == 1:
                            warning_statements.append('1 day overdue!')
                        else:
                            warning_statements.append(f'{days_different} days overdue!')

                elif inherited_deadline and inherited_deadline < epoch:
                    days_different = int((epoch-inherited_deadline)/self.timedelta_day)
                    if days_different == 0:
                        warning_statements.append(f'Due today as the {inherited_deadline_entity} has finished!')
                    elif days_different == 1:
                        warning_statements.append(f'1 day overdue as the {inherited_deadline_entity} has finished!')
                    else:
                        warning_statements.append(f'{days_different} days overdue as the {inherited_deadline_entity} has finished!')

            if preceding_state and next_state and lastchanged:
                if lastchanged < epoch - self.timedelta_week:
                    days_stale = int((epoch-lastchanged)/self.timedelta_day)
                    warning_statements.append(f'Last updated {days_stale} days ago!')

            if not preceding_state:
                backlog = epoch
                for statehistory_document in statehistory:
                    if statehistory_document['state'] == state:
                        backlog = statehistory_document['datetime']
                        break

            if not next_state and not resolution:
                warning_statements.append('A resolution needs to be set!')

        return warning_statements, days_stale

    def assemble_chart_buttons(self, destination, number_of_days, division, project, release,
                               iteration):
        content = []
        epoch = datetime.datetime.utcnow()
        days_since_start_date = 0
        selectable_chart_buttons = [(365, 31, 'Month / Year'), (186, 31, 'Month / Six Months'),
                                    (93, 31, 'Month / Three Months'), (28, 1, 'Day / Month'),
                                    (7, 1, 'Day / Week')]
        start_date, _, scope = self.get_project_release_iteration_dates(project, release, iteration)
        try:
            days_since_start_date = int((epoch-start_date)/self.timedelta_day)
        except:
            days_since_start_date = 0

        selectable_chart_buttons.append((days_since_start_date, 1, 'Day / '+scope))
        selectable_chart_buttons.append((days_since_start_date, 7, 'Week / '+scope))
        selectable_chart_buttons.append((days_since_start_date, 28, 'Month / '+scope))
        selectable_chart_buttons.sort()
        content.append('<h2 id="chartbuttons">')
        for (selectable_number_of_days, selectable_division, button_text) in selectable_chart_buttons:
            if not days_since_start_date or selectable_number_of_days <= days_since_start_date:
                content.append(f'<form class="chartleft" action="/metrics/{destination}" method="post"><input type="hidden" name="number_of_days" value="{selectable_number_of_days}"><input type="hidden" name="division" value="{selectable_division}">')
                if selectable_number_of_days == number_of_days and selectable_division == division:
                    content.append(f'<input class="daysselected" type="submit" value="{button_text}">')
                else:
                    content.append(f'<input class="daysunselected" type="submit" value="{button_text}">')

                content.append('</form>')

        content.append(f'<form class="chartright" action="/metrics/{destination}" method="post"><input type="hidden" name="number_of_days" value="{number_of_days}"><input type="hidden" name="division" value="{division}"><input type="hidden" name="csvrequired" value="true"><input type="submit" value="CSV"></form>')
        content.append(f'<form class="chartright" action="/metrics/{destination}" method="post"><input type="hidden" name="number_of_days" value="{number_of_days}"><input type="hidden" name="division" value="{division}"><input type="hidden" name="rawdatarequired" value="true"><input type="submit" value="Raw Data"></form>')
        content.append('</h2>')
        return "".join(content)

    def assemble_chart_day_labels(self, number_of_days, division):
        epoch = datetime.datetime.utcnow()
        chart_start_epoch = epoch - (self.timedelta_day * number_of_days)
        day_count = 0
        day_labels = []
        # TODO - This could be better written as a stepped for statement
        while day_count < number_of_days:
            day_count += division
            past_epoch = chart_start_epoch + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_chart_display_format(past_epoch, number_of_days,
                                                                    division)
            day_labels.append(date_format)

        return day_labels

    @staticmethod
    def assemble_chart_title(chart, project, release, iteration, day_labels):
        title = ""
        if project and release and iteration:
            title = chart+' on Project: '+project+', Release: '+release+', Iteration: '+iteration
        elif project and release:
            title = chart+' on Project: '+project+', Release: '+release
        elif project:
            title = chart+' on Project: '+project
        else:
            title = chart

        if day_labels:
            title += ' Period: '+day_labels[0]+' - '+day_labels[-1]

        return title

    def assemble_children_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            id = card_document.get('id', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            child_count = self.cards_collection.find({"parent": id}).count()
            child_documents = self.cards_collection.find({"parent": id})
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority,
                                                    severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('children', session_document,
                                                               card_document, mode, state, type,
                                                               owner, coowner, reviewer, coreviewer,
                                                               member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th>')
            if child_count:
                project_document = self.projects_collection.find_one({'project': card_document['project']})
                workflow_index = project_document.get('workflow_index', {})
                uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
                condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
                for (search_criteria, explanation) in [({"parent": id, "blocksparent": True},
                                                        'All Blocking Children are in Next State or Beyond!'
                                                       ),
                                                       ({"parent": id},
                                                        'All Children are in Next State or Beyond!'
                                                       )]:
                    child_states = self.cards_collection.find(search_criteria).distinct('state')
                    if child_states:
                        lowest_child_state, _ = self.calculate_child_state_range(uncondensed_column_states, condensed_column_states_dict, child_states)
                        if lowest_child_state > condensed_column_states_dict[state]:
                            content.append('<sup class="new">'+explanation+'</sup>')

                        break

            else:
                content.append('<sup class="children">No Children</sup>')

            content.append('</th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr>')
            content.append('<tr><th colspan="2">')

            if child_count:
                content.append('<table class="unsortable">')
                content.append('<thead><tr><th>ID</th><th>Title</th><th>State</th><th>Children</th></tr></thead><tbody>')
                for child_document in child_documents:
                    child_id    = child_document.get('id',    '')
                    child_state = child_document.get('state', '')
                    child_title = child_document.get('title', '')
                    child_type  = child_document.get('type',  '')
                    content.append(f'<tr class="{child_type}"><td>')
                    buttons = self.ascertain_card_menu_items(child_document, member_document)
                    content.append(self.assemble_card_menu(member_document, child_document, buttons, 'index'))
                    content.append('<p class="'+fontsize+'"><a href="/cards/view_card?id='+child_id+'">'+child_id+'</a></p></td>')
                    content.append('<td><p class="'+fontsize+'">'+child_title+'</p></td>')
                    content.append('<td><p class="'+fontsize+'">'+child_state+'</p></td><td><p class="'+fontsize+'">')
                    content.append(str(self.cards_collection.find({"parent": child_id}).count()))
                    content.append('</p></td></tr>')

                content.append('</tbody></table>')
                content.append('</th></tr>')

            content.append('</thead>')
            content.append('<tbody></tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_comments_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """ Displays a card's latest comment """
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            comments = card_document.get('comments', [])
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            iteration = card_document.get('iteration', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if comments:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('comments', session_document,
                                                                   card_document, mode, state, type,
                                                                   owner, coowner, reviewer,
                                                                   coreviewer, member_document,
                                                                   projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                comment_document = comments[-1]
                date_format = self.convert_time_to_display_format(comment_document['datetime'])
                comment_class = self.ascertain_comment_class(comment_document, owner, coowner)
                content.append('<sup class="'+comment_class+'">'+comment_document['username']+' on '+date_format+'</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr><tr><th colspan="2">')
                modified_comment = self.format_multiline(comment_document['comment'])
                content.append('<div class="'+comment_class+'"><p class="'+fontsize+'left">'+modified_comment+'</p></div>')
                content.append('</th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)
        
    def assemble_custom_attributes_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                               someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            project_document = self.projects_collection.find_one({'project': project})
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority,
                                                    severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('attributes', session_document,
                                                               card_document, mode, state, card_type,
                                                               owner, coowner, reviewer, coreviewer,
                                                               member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')

            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document,
                                                   buttons, 'index'))
            content.append('</th><th></th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr></thead><tbody><tr><td colspan="2">')
            content.append('<table>')
            if 'customattributes' in project_document and project_document['customattributes']:
                for custom_attribute, _ in project_document['customattributes'].items():
                    value = card_document.get(custom_attribute, '')
                    content.append(f'<tr><th valign="top"><p class="{fontsize}">{custom_attribute}</p></th><td><p class="{fontsize}left">{value}</p></td></tr>')

            content.append('</table>')
            content.append('</td></tr></tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_days_in_state_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                           someone_else_is_stuck_status, projection):
        """ Displays the days in the current state for every card on the kanban board """
        content = []
        epoch = datetime.datetime.utcnow()
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            statehistory = card_document.get('statehistory', [])
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            if state:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('daysinstate', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append('</th><th>')
                project_document = self.projects_collection.find_one({'project': card_document['project']})
                workflow_index = project_document.get('workflow_index', {})
                condensed_column_states = workflow_index.get('condensed_column_states', {})
                state_metrics = self.get_state_metrics(condensed_column_states, statehistory)
                if state_metrics[state]:
                    if len(statehistory) > 1:
                        statehistory_document = statehistory[-2]
                        previous_state = statehistory_document['state']
                        if previous_state and previous_state in condensed_column_states:
                            current_minus_previous = state_metrics[state]-state_metrics[previous_state]
                            try:
                                days_in_current_state = int(current_minus_previous/self.timedelta_day)
                            except:
                                days_in_current_state = 0

                            content.append(f'<sup class="new">{days_in_current_state} Days In This State!</sup>')

                    else:
                        days_in_current_state = int((epoch-state_metrics[state])/self.timedelta_day)
                        content.append(f'<sup class="new">{days_in_current_state} Days In This State!</sup>')

                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_deadline_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """comment"""
        content = []
        epoch = datetime.datetime.utcnow()
        search_criteria = {"_id": ObjectId(doc_id)}
        warning_statements = []
        days_stale = 0
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deadline = card_document.get('deadline', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            iteration = card_document.get('iteration', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            project_document = self.projects_collection.find_one({'project': project})
            metastate = self.get_corresponding_metastate(project_document, state)
            inherited_deadline = 0
            inherited_deadline_entity = ""
            if all([project, release, iteration]):
                project_document = self.projects_collection.find_one({"project": project,
                                                                      "releases.release": release,
                                                                      "releases.iterations.iteration": iteration})
                if project_document:
                    for release_document in project_document['releases']:
                        if release_document['release'] == release:
                            for iteration_document in release_document['iterations']:
                                if iteration_document['iteration'] == iteration:
                                    if 'end_date' in iteration_document and iteration_document['end_date']:
                                        inherited_deadline = iteration_document['end_date']
                                        inherited_deadline_entity = "iteration"

            elif project and release:
                project_document = self.projects_collection.find_one({"project": project,
                                                                      "releases.release": release})
                if project_document:
                    for release_document in project_document['releases']:
                        if release_document['release'] == release:
                            if 'end_date' in release_document and release_document['end_date']:
                                inherited_deadline = release_document['end_date']
                                inherited_deadline_entity = "release"

            elif project:
                project_document = self.projects_collection.find_one({"project": project})
                if project_document and 'end_date' in project_document and project_document['end_date']:
                    inherited_deadline = project_document['end_date']
                    inherited_deadline_entity = "project"

            if metastate not in ['closed', 'completed', 'acceptancetestingaccepted']:
                if deadline:
                    time_to_display = deadline
                    epoch = datetime.datetime.utcnow()

                    # Deadlines due in the next seven days
                    if deadline < epoch + self.timedelta_day:
                        days_different = int((epoch-deadline)/self.timedelta_day)
                        if days_different < -1:
                            warning_statements.append(f'The deadline for this {type} is in the next {abs(days_different)} days!')
                        elif days_different == -1:
                            warning_statements.append(f'The deadline for this {type} is tomorrow!')
                        elif days_different == 0:
                            warning_statements.append(f'The deadline for this {type} is today!')
                        elif days_different == 1:
                            warning_statements.append(f'<b class="blink">The deadline for this {type} was yesterday!</b>')
                        else:
                            warning_statements.append(f'<b class="blink">The deadline for this {type} was {days_different} days ago!</b>')

                elif inherited_deadline:
                    time_to_display = inherited_deadline
                    epoch = datetime.datetime.utcnow()
                    if inherited_deadline < epoch:
                        days_different = int((epoch-inherited_deadline)/self.timedelta_day)
                        if days_different == 0:
                            warning_statements.append(f'The deadline for this {type} is today as the {inherited_deadline_entity} has completed!')
                        elif days_different == 1:
                            warning_statements.append(f'The deadline for this {type} was yesterday as the {inherited_deadline_entity} has completed!')
                        else:
                            warning_statements.append(f'The deadline for this {type} was {days_different} days ago as the {inherited_deadline_entity} has completed!')

            if warning_statements:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('deadline', session_document,
                                                                   card_document, mode, state, type,
                                                                   owner, coowner, reviewer,
                                                                   coreviewer, member_document,
                                                                   projection)
                content.append(table_initialise)

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

                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))

                content.append('</th><th>')
                content.append(self.show_category_in_top_right(project, category))
                content.append(self.insert_new_recent_days_old_message(card_document, state, epoch))
                content.append(f'<sup class="deadlineconfirmation">{self.convert_time_to_display_format(time_to_display)}</sup>')

                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr><tr>')
                if days_stale > 90:
                    content.append('<td class="antiquated" colspan="2">')
                else:
                    content.append('<td class="warning" colspan="2">')

                content.append(self.assemble_warning_statements(fontsize, warning_statements, days_stale))
                content.append('</td></tr></thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_deferrals_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                       someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            deferred = card_document.get('deferred', '')
            if deferred:
                doc_id = card_document["_id"]
                blocked = card_document.get('blocked', '')
                coowner = card_document.get('coowner', '')
                coreviewer = card_document.get('coreviewer', '')
                expedite = card_document.get('expedite', '')
                owner = card_document.get('owner', '')
                parent = card_document.get('parent', '')
                priority = card_document.get('priority', '')
                project = card_document.get('project', '')
                release = card_document.get('release', '')
                reviewer = card_document.get('reviewer', '')
                severity = card_document.get('severity', '')
                state = card_document.get('state', '')
                title = card_document.get('title', '')
                type = card_document.get('type', '')
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('deferrals', session_document,
                                                                   card_document, mode, state, type,
                                                                   owner, coowner, reviewer,
                                                                   coreviewer, member_document,
                                                                   projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append('</th><th>')
                content.append('<sup class="deferred" title="'+deferred+'">Deferred</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_disowned_kanban_card(self, swimlane_no, doc_id):
        """Only used by the Disowned column to display a disowned card along with its state on the kanban board"""
        content = []
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            doc_id = card_document["_id"]
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            content.append(f'<div class="card" id="{swimlane_no}###{doc_id}###-1###buffer###{state}###all###{severity}###-1">')
            table_initialise, _ = self.assign_card_table_class('new', session_document,
                                                               card_document, [], state, card_type,
                                                               owner, coowner, reviewer, coreviewer,
                                                               member_document, 0)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th>')
            content.append('<sup class="state">'+state+'</sup>')
            content.append('</th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr></thead><tbody></tbody></table></div>')
            break

        return "".join(content)

    def assemble_focus_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                   someone_else_is_stuck_status, projection):
        """comment"""
        epoch = datetime.datetime.utcnow()
        blocked_reason = ""
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            blockedhistory = card_document.get('blockedhistory', [])
            blockeduntil = card_document.get('blockeduntil', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deadline = card_document.get('deadline', '')
            deferred = card_document.get('deferred', '')
            deferreduntil = card_document.get('deferreduntil', '')
            expedite = card_document.get('expedite', '')
            focushistory = card_document.get('focushistory', [])
            id = card_document.get('id', '')
            iteration = card_document.get('iteration', '')
            lastchanged = card_document.get('lastchanged', '')
            nextaction = card_document.get('nextaction', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            resolution = card_document.get('resolution', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            statehistory = card_document.get('statehistory', [])
            stuck = card_document.get('stuck', '')
            title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority,
                                                    severity, expedite, blocked, deferred, release))

            table_initialise, blocked_reason = self.assign_card_table_class('small',
                                                                            session_document,
                                                                            card_document, mode,
                                                                            state, card_type, owner,
                                                                            coowner, reviewer,
                                                                            coreviewer,
                                                                            member_document,
                                                                            projection)
            content.append(table_initialise)
            content.append('<thead><tr><th valign="top">')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th width="100%">')
            if 'focusstart' in card_document and card_document['focusstart']:
                focus_start = card_document['focusstart']
                minutes_spent = int((epoch - focus_start) / datetime.timedelta(seconds=60))
                content.append(f'<sup class="focus">In Focus for {minutes_spent} minutes!</sup>')
            elif focushistory:
                latest_focushistory_document = focushistory[-1]
                minutes_spent = int((latest_focushistory_document['focusend'] - latest_focushistory_document['focusstart']) / datetime.timedelta(seconds=60))
                hours_ago = int((epoch - latest_focushistory_document['focusend']) / self.timedelta_hour)
                content.append(f'<sup class="focus">Last Focus by {latest_focushistory_document["focusby"]} for {minutes_spent} minutes {hours_ago} hours ago!</sup>')

            content.append('</th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr>')
            inherited_deadline, inherited_deadline_entity = self.calculate_inherited_deadline(project, release, iteration)
            warning_statements, days_stale = self.assemble_card_warning_statements(statehistory,
                    blocked_reason, blocked, blockedhistory, blockeduntil, deferred, deferreduntil,
                    coowner, coreviewer, deadline, inherited_deadline, inherited_deadline_entity,
                    lastchanged, nextaction, owner, project, reviewer, session_document,
                    someone_else_is_stuck_status, state, stuck, card_type, resolution)
            if warning_statements:
                content.append('<tr>')
                if days_stale > 90:
                    content.append('<td class="antiquated" colspan="2">')
                else:
                    content.append('<td class="warning" colspan="2">')

                content.append(self.assemble_warning_statements(fontsize, warning_statements, days_stale))
                content.append('</td></tr>')

            content.append('</thead><tbody></tbody></table></div>')

        return "".join(content)
        
    def assemble_lipsum_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                    someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id, blocked, coowner, coreviewer, deferred, expedite, owner = self.get_card_attribute_values(card_document,
                                                                ['_id', 'blocked', 'coowner', 'coreviewer', 'deferred', 'expedite', 'owner'])
            project, priority, release, reviewer, severity, state, title, type = self.get_card_attribute_values(card_document,
                                                     ['project', 'priority', 'release', 'reviewer', 'severity', 'state', 'title', 'type'])
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('lipsum', session_document, card_document, mode, state,
                                                               type, owner, coowner, reviewer,
                                                               coreviewer, member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th></th></tr><tr><th colspan="2">')
            number_of_words = len(title.split(' '))
            lipsum_sentence = lipsum.generate_words(number_of_words)
            content.append(f'<p class="{fontsize}" title="{title}">{lipsum_sentence}</p>')
            content.append('</th></tr></thead>')
            content.append('<tbody></tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_internals_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                       someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if 'minimised' in mode:
                content += self.create_minimised_card_div(project, state)
            elif 'ghosted' in mode:
                content += '<div class="ghostedcard">'
            else:
                content += self.create_card_div(swimlane_no, doc_id, project, state, priority,
                                                severity, expedite, blocked, deferred, release)

            table_initialise, _ = self.assign_card_table_class('internals', session_document,
                                                               card_document, mode, state, type,
                                                               owner, coowner, reviewer, coreviewer,
                                                               member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th></tr></thead><tbody><tr><td>')
            content.append('<table>')
            for key, value in sorted(card_document.items()):
                if value and key in ['_id', 'id', 'actualcosthistory', 'actualtimehistory', 'blockedhistory',
                                     'blockeduntil', 'deadline', 'deferreduntil', 'estimatedcosthistory',
                                     'estimatedtimehistory', 'hashtags', 'hiddenuntil', 'history', 'lastchanged',
                                     'mode', 'nextaction', 'statehistory']:
                    content.append(f'<tr><th valign="top"><p class="{fontsize}">{self.displayable_key(key)}</p></th>')
                    content.append(f'<td><p class="{fontsize}left">{value}</p></td>')
                    content.append('</tr>')

            content.append('</table></td></tr></tbody></table></div>')

        return "".join(content)

    def assemble_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                             someone_else_is_stuck_status, projection):
        """Assemble the correct card for the kanban board selected"""
        kanbanboard = ""
        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:
            kanbanboard = member_document.get("kanbanboard", '')

        # TODO - 'someone_else_is_stuck_status' isn't used on some of these cards
        if kanbanboard == 'Absenteeism':
            return self.assemble_absenteeism_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                         someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Activity':
            return self.assemble_activity_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Artifact Clash':
            return self.assemble_artifact_clash_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                            someone_else_is_stuck_status,
                                                            projection)
        elif kanbanboard == 'Attachments':
            return self.assemble_attachments_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                         someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Attributes':
            return self.assemble_attributes_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                        someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Avatar':
            return self.assemble_avatar_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                    someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Blockages':
            return self.assemble_blockages_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                       someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Bypass Reviews':
            return self.assemble_bypassreviews_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                           someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Capitals':
            return self.assemble_capitals_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Children':
            return self.assemble_children_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Comments':
            return self.assemble_comments_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Custom Attributes':
            return self.assemble_custom_attributes_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                               someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Days In State':
            return self.assemble_days_in_state_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                           someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Deadline':
            return self.assemble_deadline_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Deferrals':
            return self.assemble_deferrals_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                       someone_else_is_stuck_status, projection)
        elif kanbanboard in ['AffectsVersion', 'Class Of Service', 'Co-Owner', 'Co-Reviewer',
                             'Cost', 'Creator', 'CRM Cases', 'Customer', 'Difficulty', 'Emotions',
                             'Escalation', 'Ext Ref', 'FixVersion', 'Hashtags', 'Identifier',
                             'Iteration', 'Last Changed', 'Last Touched', 'Next Action',
                             'Reassignments', 'Recurring', 'Release', 'Resolution', 'Scope Creep',
                             'Severity', 'Status', 'Subteam', 'Yesterday']:
            return self.assemble_variable_kanban_card(kanbanboard, usertypes, mode, swimlane_no,
                                                      doc_id, someone_else_is_stuck_status,
                                                      projection)
        elif kanbanboard == 'Focus':
            return self.assemble_focus_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                   someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Lipsum':
            return self.assemble_lipsum_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)                        
        elif kanbanboard == 'Internals':
            return self.assemble_internals_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                       someone_else_is_stuck_status, projection)
        elif kanbanboard == 'New':
            return self.assemble_new_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                 someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Owner Unassigned':
            return self.assemble_owner_unassigned_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                              someone_else_is_stuck_status,
                                                              projection)
        elif kanbanboard == 'Questions':
            return self.assemble_questions_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                       someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Recidivism':
            return self.assemble_recidivism_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                        someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Reopened':
            return self.assemble_reopened_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Reviewer':
            return self.assemble_reviewer_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Rules':
            return self.assemble_rules_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                   someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Search':
            return self.assemble_search_kanban_card(mode, swimlane_no, doc_id, {}, projection)
        elif kanbanboard == 'Similars':
            return self.assemble_similars_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Tabbed':
            return self.assemble_tabbed_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                    someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Test Cases':
            return self.assemble_testcases_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                       someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Time':
            return self.assemble_time_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                  someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Today':
            return self.assemble_today_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                   someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Velocity':
            return self.assemble_velocity_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                      someone_else_is_stuck_status, projection)
        elif kanbanboard == 'Votes':
            return self.assemble_votes_kanban_card(usertypes, mode, swimlane_no, doc_id,
                                                   someone_else_is_stuck_status, projection)
        else:
            return Kanbanara.assemble_variable_kanban_card(self, 'Identifier', usertypes, mode,
                                                           swimlane_no, doc_id,
                                                           someone_else_is_stuck_status, projection)

    def assemble_new_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                 someone_else_is_stuck_status, projection):
        """ Displays a card recently added or moved to a new state on the kanban board """
        content = []
        datetime_now = datetime.datetime.utcnow()
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coownerstate = card_document.get('coownerstate', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            statehistory = card_document.get('statehistory', [])
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            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', {})
            state_metrics = self.get_state_metrics(condensed_column_states, statehistory)
            if state in state_metrics and state_metrics[state] and state_metrics[state] > datetime_now - self.timedelta_day:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('new', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                content.append(self.show_category_in_top_right(project, category))
                content.append('<sup class="new" title="Newly arrived in this state">New!</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_owner_unassigned_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                              someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if not owner and not coowner:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('reopened', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                content.append('<sup class="owner">No Owner(s)</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_page_component_mappings(self):
        self.page_component_mappings = {'index': 'kanban',
                                        'html':  'html'
                                       }
        clashes = {}
        for component in ['kanbanara', 'root']:
            tree = ast.parse(open(os.path.join(self.current_dir, component+'.py')).read())
            function_definitions = [f.name for f in ast.walk(tree) if isinstance(f, ast.FunctionDef)]
            for function_definition in function_definitions:
                if function_definition not in ['__init__', 'index']:
                    if function_definition in self.page_component_mappings:
                        if function_definition in clashes:
                            clashes[function_definition].append('')
                        else:
                            clashes[function_definition] = [self.page_component_mappings[function_definition], '']

                    else:
                        self.page_component_mappings[function_definition] = ''


        for component in ['admin', 'api', 'backlog', 'cards', 'export', 'filters', 'import',
                          'kanban', 'linting', 'members', 'metrics', 'projects', 'reports',
                          'search', 'sync', 'themes', 'visuals']:
            if component != 'linting' or os.path.exists(os.path.join(self.current_dir, component, component+'.py')):
                tree = ast.parse(open(os.path.join(self.current_dir, component, component+'.py')).read())
                function_definitions = [f.name for f in ast.walk(tree) if isinstance(f, ast.FunctionDef)]
                for function_definition in function_definitions:
                    if function_definition not in ['__init__', 'index']:
                        if function_definition in self.page_component_mappings:
                            if function_definition in clashes:
                                clashes[function_definition].append(component)
                            else:
                                clashes[function_definition] = [self.page_component_mappings[function_definition], component]

                        else:
                            self.page_component_mappings[function_definition] = component

        if clashes:
            for i, key in enumerate(clashes.keys()):
                print(f'{i}: {key} in all of {clashes[key]}')

    def assemble_placeholder_card(self, swimlane_no, card_document, mode, member_document,
                                  projection):
        """Assemble a placeholder card for placing on a kanban board"""
        content = []
        doc_id, blocked, coowner, coreviewer, deferred, expedite, owner, priority, project, release, reviewer, severity, state, title, type = self.get_card_attribute_values(card_document,
                ['_id', 'blocked', 'coowner', 'coreviewer', 'deferred', 'expedite', 'owner', 'priority', 'project', 'release', 'reviewer', 'severity', 'state', 'title', 'type'])
        if 'minimised' in mode:
            content.append(self.create_minimised_card_div(project, state))
        elif 'ghosted' in mode:
            content.append('<div class="ghostedcard">')
        else:
            content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked,
                                                deferred, release))

        table_initialise, _ = self.assign_card_table_class('placeholder', {}, card_document, mode, state, type, owner, coowner,
                                                           reviewer, coreviewer, member_document, projection)
        content.append(table_initialise)
        content.append('<thead><tr><th>')
        buttons = self.ascertain_card_menu_items(card_document, member_document)
        content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
        content.append('<b title="'+title+'">-</b></th></tr></thead><tbody></tbody></table></div>')
        return "".join(content)

    def assemble_questions_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                       someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            question = card_document.get('question', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if question:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('question', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                content.append('<sup class="question">'+question+'</sup>')
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_recent_activity_entry(self, past_time, username, doc_id, mode):
        recent_activity_entry = ""
        fullname = ""
        member_document = self.members_collection.find_one({"username": username})
        if member_document:
            fullname = member_document.get('fullname', '')

        if past_time+self.timedelta_day >= datetime.datetime.utcnow():
            modifiedtime_string = "%02d:%02d" % (past_time.hour, past_time.minute)
            card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
            if card_document:
                doc_id = card_document["_id"]
                id = card_document.get('id', '')
                title = card_document.get('title', '')
                card_type = card_document.get('type', '')
                if fullname:
                    if mode in ['added', 'updated']:
                        recent_activity_entry = '<b class="'+card_type+'"><a href="/cards/view_card?id='+str(id)+'">'+card_type.capitalize()+' '+str(id)+"</a></b>"+fullname+' has '+mode+" '<i>"+title+"</i>'"+' at '+modifiedtime_string
                    else:
                        recent_activity_entry = '<b class="'+card_type+'">'+card_type.capitalize()+' '+str(id)+'</b>'+fullname+' has '+mode+" '<i>"+title+"</i>'"+' at '+modifiedtime_string

                else:
                    recent_activity_entry = '<b class="'+card_type+'"><a href="/cards/view_card?id='+str(id)+'">'+card_type.capitalize()+' '+str(id)+"</a></b> '<i>"+title+"</i>' has been "+mode+' at '+modifiedtime_string

            elif mode == 'deleted':
                card_document = self.deletions_collection.find_one({"_id": ObjectId(doc_id)})
                if card_document:
                    doc_id = card_document["_id"]
                    title = card_document.get('title', '')
                    card_type = card_document.get('type', '')
                    recent_activity_entry = '<b class="'+card_type+'">'+card_type.capitalize()+' '+str(id)+"</b> '<i>"+title+"</i>' has been deleted at "+modifiedtime_string
                else:
                    self.recent_activities.remove((past_time, username, doc_id, mode))

            else:
                self.recent_activities.remove((past_time, username, doc_id, mode))

        return recent_activity_entry

    def assemble_recidivism_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                        someone_else_is_stuck_status, projection):
        """comment"""
        epoch = datetime.datetime.utcnow()
        blocked_reason = ""
        content = []
        recidivism = []
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            project = card_document.get('project', '')
            state = card_document.get('state', '')
            statehistory = card_document.get('statehistory', [])
            project_document = self.projects_collection.find_one({'project': card_document['project']})
            workflow_index = project_document.get('workflow_index', {})
            uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])          
            state_metrics = self.get_state_metrics(uncondensed_column_states, statehistory)
            states = uncondensed_column_states.copy()
            states.reverse()
            for i, state_i in enumerate(states):
                for j in range(i, len(uncondensed_column_states)):
                    if not recidivism and states[j] == state:
                        if state_metrics[state_i] and state_metrics[states[j]]:
                            if state_metrics[state_i] < state_metrics[states[j]]:
                                recidivism = [states[i], states[j]]

            if recidivism:
                doc_id = card_document["_id"]
                blocked = card_document.get('blocked', '')
                blockedhistory = card_document.get('blockedhistory', [])
                blockeduntil = card_document.get('blockeduntil', '')
                coowner = card_document.get('coowner', '')
                coreviewer = card_document.get('coreviewer', '')
                deadline = card_document.get('deadline', '')
                deferred = card_document.get('deferred', '')
                deferreduntil = card_document.get('deferreduntil', '')
                expedite = card_document.get('expedite', '')
                iteration = card_document.get('iteration', '')
                lastchanged = card_document.get('lastchanged', '')
                nextaction = card_document.get('nextaction', '')
                owner = card_document.get('owner', '')
                parent = card_document.get('parent', '')
                priority = card_document.get('priority', '')
                release = card_document.get('release', '')
                resolution = card_document.get('resolution', '')
                reviewer = card_document.get('reviewer', '')
                severity = card_document.get('severity', '')
                stuck = card_document.get('stuck', '')
                title = card_document.get('title', '')
                type = card_document.get('type', '')
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, blocked_reason = self.assign_card_table_class('recidivism', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th valign="top">')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                content.append(self.insert_new_recent_days_old_message(card_document, state, epoch))
                content.append('</th><th valign="top">')
                content.append('<sup class="recidivism">'+recidivism[0].capitalize()+' &rArr; '+recidivism[1].capitalize()+'</sup>')
                recidivism_rate = ''
                forwardcount, backwardcount = self.calculate_recidivism(project, statehistory)
                if backwardcount and forwardcount:
                    recidivism_rate = int((backwardcount / (forwardcount + backwardcount)) * 100)
                    content.append(f'<sup class="recidivism">{recidivism_rate}%</sup>')

                content.append('</th></tr><tr><th colspan="3">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr>')
                inherited_deadline, inherited_deadline_entity = self.calculate_inherited_deadline(project, release, iteration)
                warning_statements, days_stale = self.assemble_card_warning_statements(statehistory, blocked_reason, blocked, blockedhistory, blockeduntil, deferred, deferreduntil,
                                                                                       coowner, coreviewer, deadline, inherited_deadline, inherited_deadline_entity, lastchanged, nextaction, owner, project, reviewer, session_document, someone_else_is_stuck_status, state, stuck, type, resolution)
                if warning_statements:
                    content.append('<tr>')
                    if days_stale > 90:
                        content.append('<td class="antiquated" colspan="3">')
                    else:
                        content.append('<td class="warning" colspan="3">')

                    content.append(self.assemble_warning_statements(fontsize, warning_statements,
                                                                    days_stale))
                    content.append('</td></tr>')

                content.append('</thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_reopened_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            reopened = card_document.get('reopened', '')
            if reopened:
                doc_id, blocked, coowner, coreviewer, deferred, expedite, owner, parent = self.get_card_attribute_values(card_document,
                                                                    ['_id', 'blocked', 'coowner', 'coreviewer', 'deferred', 'expedite', 'owner', 'parent'])
                project, priority, release, reviewer, severity, state, title, type = self.get_card_attribute_values(card_document,
                                                         ['project', 'priority', 'release', 'reviewer', 'severity', 'state', 'title', 'type'])
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('reopened', session_document, card_document, mode, state,
                                                                   type, owner, coowner, reviewer,
                                                                   coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th></th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_reviewer_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            reviewer = card_document.get('reviewer', '')
            coreviewer = card_document.get('coreviewer', '')
            doc_id, blocked, bypassreview, coowner, deferred, expedite, owner, parent, priority, project, release, severity, state, title, type = self.get_card_attribute_values(card_document,
                            ['_id', 'blocked', 'bypassreview', 'coowner', 'deferred', 'expedite', 'owner', 'parent', 'priority', 'project', 'release', 'severity', 'state', 'title', 'type'])
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity,
                                                    expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('reopened', session_document, card_document, mode, state,
                                                               type, owner, coowner, reviewer,
                                                               coreviewer, member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th>')
            if bypassreview:
                content.append('<sup class="reviewer">Bypassing Review</sup>')
            elif reviewer:
                reviewer_member_document = self.members_collection.find_one({'username': reviewer})
                fullname = reviewer_member_document.get('fullname', reviewer)
                content.append(f'<sup class="reviewer">{fullname}</sup>')
            else:
                content.append('<sup class="reviewer">Unassigned</sup>')

            content.append('</th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr></thead>')
            content.append('<tbody></tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_rules_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                   someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            rules = card_document.get('rules', [])
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('rules', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th>')
            if not rules:
                content.append('<sup class="rules">No Rules</sup>')

            content.append('</th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr></thead><tbody>')
            if rules:
                content.append('<tr><td>')

                content.append(f'<table class="view{type}"><tr><th>Rule</th><th>Status</th></tr>')
                for rule_document in rules:
                    content.append('<tr><td><p class="'+fontsize+'left">')
                    components = rule_document['components']
                    inside_condition_block = False
                    inside_success_action_block = False
                    inside_failure_action_block = False
                    for component_no in range(0, len(components), 4):
                        if components[component_no] == 'if':
                            if not inside_condition_block:
                                inside_condition_block = True
                                content.append('<span class="rulekeyword">'+components[component_no]+'</span> ')
                            else:
                                content.append('<br><span class="ruleerror">'+components[component_no]+'</span> ')

                        elif components[component_no] == 'then':
                            if inside_condition_block:
                                inside_condition_block = False
                                inside_success_action_block = True
                                content.append('<br><span class="rulekeyword">'+components[component_no]+'</span> ')
                            else:
                                content.append('<br><span class="ruleerror">'+components[component_no]+'</span> ')

                        elif components[component_no] == 'else':
                            if inside_success_action_block:
                                inside_success_action_block = False
                                inside_failure_action_block = True
                                content.append('<br><span class="rulekeyword">'+components[component_no]+'</span> ')
                            else:
                                content.append('<br><span class="ruleerror">'+components[component_no]+'</span> ')

                        elif components[component_no] == 'and':
                            content.append('<br><span class="rulekeyword">'+components[component_no]+'</span> ')
                        elif components[component_no] == 'or':
                            if inside_success_action_block or inside_failure_action_block:
                                content.append('<br><span class="ruleerror">'+components[component_no]+'</span> ')
                            else:
                                content.append('<br><span class="rulekeyword">'+components[component_no]+'</span> ')

                        else:
                            content.append('<br><span class="ruleerror">'+components[component_no]+'</span> ')

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

                        except:
                            attribute = "?"

                        try:
                            operand = components[component_no+2]
                        except:
                            operand = "?"

                        try:
                            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

                        except:
                            value = "?"

                        if inside_condition_block:
                            if other_card_id:
                                valid = True
                                if not self.cards_collection.find({'id': other_card_id}).count():
                                    valid = False

                                if other_card_project != project:
                                    valid = False

                                if attribute not in self.rule_condition_allowable_card_attributes:
                                    valid = False

                                if valid:
                                    content.append('<span class="ruleattribute">'+other_card_id+'.'+attribute+'</span> ')
                                else:
                                    content.append('<span class="ruleerror">'+other_card_id+'.'+attribute+'</span> ')

                            elif attribute in self.rule_condition_allowable_card_attributes:
                                content.append('<span class="ruleattribute">'+attribute+'</span> ')
                            else:
                                content.append('<span class="ruleerror">'+attribute+'</span> ')

                            if operand in ['<', '<=', '>', '>=', '=', '!=', 'is', 'in', 'nin']:
                                content.append(f'<span class="ruleoperand">{operand}</span> ')
                            else:
                                content.append(f'<span class="ruleerror">{operand}</span> ')

                            if value in ['undefined', 'defined', 'populated', 'unpopulated']:
                                content.append(f'<span class="rulekeyword">{value}</span>')
                            elif isinstance(value, str):
                                if '"' in value and "'" in value:
                                    content.append(f'<span class="rulestring">"""{value}"""</span>')
                                elif '"' in value:
                                    content.append(f'<span class="rulestring">\'{value}\'</span>')
                                else:
                                    content.append(f'<span class="rulestring">"{value}"</span>')

                            elif isinstance(value, list):
                                content.append(f'<span class="rulelist">{value}</span>')
                            elif isinstance(value, int):
                                content.append(f'<span class="ruleint">{value}</span>')
                            elif isinstance(value, float):
                                content.append(f'<span class="rulefloat">{value}</span>')
                            elif isinstance(value, bool):
                                content.append(f'<span class="rulebool">{value}</span>')
                            else:
                                content.append(f'<span class="ruleerror">{value}</span>')

                        elif inside_success_action_block:
                            if other_card_id:
                                valid = True
                                if not self.cards_collection.find({'id': other_card_id}).count():
                                    valid = False

                                if other_card_project != project:
                                    valid = False

                                if attribute not in self.rule_condition_allowable_card_attributes:
                                    valid = False

                                if valid:
                                    content.append('<span class="ruleattribute">'+other_card_id+'.'+attribute+'</span> ')
                                    if operand in ['=', 'is']:
                                        content.append(f'<span class="ruleoperand">{operand}</span> ')
                                        if isinstance(value, str):
                                            if '"' in value and "'" in value:
                                                content.append(f'<span class="rulestring">"""{value}"""</span>')
                                            elif '"' in value:
                                                content.append(f'<span class="rulestring">\'{value}\'</span>')
                                            else:
                                                content.append(f'<span class="rulestring">"{value}"</span>')

                                        elif isinstance(value, int):
                                            content.append(f'<span class="ruleint">{value}</span>')
                                        elif isinstance(value, float):
                                            content.append(f'<span class="rulefloat">{value}</span>')
                                        elif isinstance(value, bool):
                                            content.append(f'<span class="rulebool">{value}</span>')
                                        else:
                                            content.append(f'<span class="ruleerror">{value}</span>')

                                    else:
                                        content.append(f'<span class="ruleerror">{operand}</span> ')
                                        content.append(f'<span class="ruleerror">{value}</span>')

                                else:
                                    content.append('<span class="ruleerror">'+other_card_id+'.'+attribute+'</span> ')
                                    content.append(f'<span class="ruleerror">{operand}</span> ')
                                    content.append(f'<span class="ruleerror">{value}</span>')

                            elif attribute in self.rule_action_allowable_card_attributes:
                                content.append('<span class="ruleattribute">'+attribute+'</span> ')
                                if operand in ['=', 'is']:
                                    content.append(f'<span class="ruleoperand">{operand}</span> ')
                                    if isinstance(value, str):
                                        if '"' in value and "'" in value:
                                            content.append(f'<span class="rulestring">"""{value}"""</span>')
                                        elif '"' in value:
                                            content.append(f'<span class="rulestring">\'{value}\'</span>')
                                        else:
                                            content.append(f'<span class="rulestring">"{value}"</span>')

                                    elif isinstance(value, int):
                                        content.append(f'<span class="ruleint">{value}</span>')
                                    elif isinstance(value, float):
                                        content.append(f'<span class="rulefloat">{value}</span>')
                                    elif isinstance(value, bool):
                                        content.append(f'<span class="rulebool">{value}</span>')
                                    else:
                                        content.append(f'<span class="ruleerror">{value}</span>')

                                else:
                                    content.append(f'<span class="ruleerror">{operand}</span> ')
                                    content.append(f'<span class="ruleerror">{value}</span>')

                            else:
                                content.append('<span class="ruleerror">'+attribute+'</span> ')
                                content.append(f'<span class="ruleerror">{operand}</span> ')
                                content.append(f'<span class="ruleerror">{value}</span>')

                        elif inside_failure_action_block:
                            if other_card_id:
                                valid = True
                                if not self.cards_collection.find({'id': other_card_id}).count():
                                    valid = False

                                if other_card_project != project:
                                    valid = False

                                if attribute not in self.rule_action_allowable_card_attributes:
                                    valid = False

                                if valid:
                                    content.append('<span class="ruleattribute">'+other_card_id+'.'+attribute+'</span> ')
                                    if operand in ['=', 'is']:
                                        content.append('<span class="ruleoperand">'+operand+'</span> ')
                                        if isinstance(value, str):
                                            if '"' in value and "'" in value:
                                                content.append(f'<span class="rulestring">"""{value}"""</span>')
                                            elif '"' in value:
                                                content.append(f'<span class="rulestring">\'{value}\'</span>')
                                            else:
                                                content.append(f'<span class="rulestring">"{value}"</span>')

                                        elif isinstance(value, int):
                                            content.append(f'<span class="ruleint">{value}</span>')
                                        elif isinstance(value, float):
                                            content.append(f'<span class="rulefloat">{value}</span>')
                                        elif isinstance(value, bool):
                                            content.append(f'<span class="rulebool">{value}</span>')
                                        else:
                                            content.append(f'<span class="ruleerror">{value}</span>')

                                    else:
                                        content.append('<span class="ruleerror">'+operand+'</span> ')
                                        content.append('<span class="ruleerror">'+value+'</span>')

                                else:
                                    content.append('<span class="ruleerror">'+other_card_id+'.'+attribute+'</span> ')
                                    content.append('<span class="ruleerror">'+operand+'</span> ')
                                    content.append('<span class="ruleerror">'+value+'</span>')

                            elif attribute in self.rule_action_allowable_card_attributes:
                                content.append('<span class="ruleattribute">'+attribute+'</span> ')
                                if operand in ['=', 'is']:
                                    content.append('<span class="ruleoperand">'+operand+'</span> ')
                                    if isinstance(value, str):
                                        if '"' in value and "'" in value:
                                            content.append('<span class="rulestring">"""'+value+'"""</span>')
                                        elif '"' in value:
                                            content.append(f'<span class="rulestring">\'{value}\'</span>')
                                        else:
                                            content.append(f'<span class="rulestring">"{value}"</span>')

                                    elif isinstance(value, int):
                                        content.append(f'<span class="ruleint">{value}</span>')
                                    elif isinstance(value, float):
                                        content.append(f'<span class="rulefloat">{value}</span>')
                                    elif isinstance(value, bool):
                                        content.append(f'<span class="rulebool">{value}</span>')
                                    else:
                                        content.append(f'<span class="ruleerror">{value}</span>')

                                else:
                                    content.append('<span class="ruleerror">'+operand+'</span> ')
                                    content.append(f'<span class="ruleerror">{value}</span>')

                            else:
                                content.append('<span class="ruleerror">'+attribute+'</span> ')
                                content.append('<span class="ruleerror">'+operand+'</span> ')
                                content.append(f'<span class="ruleerror">{value}</span>')

                        else:
                            if other_card_id:
                                content.append('<span class="ruleerror">'+other_card_id+'.'+attribute+'</span> ')
                            else:
                                content.append('<span class="ruleerror">'+attribute+'</span> ')

                            content.append('<span class="ruleerror">'+operand+'</span> ')
                            content.append('<span class="ruleerror">'+value+'</span>')

                    content.append('</p></td>')
                    content.append('<td><p class="'+fontsize+'">'+rule_document['status']+'</p></td></tr>')

                content.append('</table>')

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

            content.append('</tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_search_kanban_card(self, mode, swimlane_no, doc_id, highlight_attrs_values, projection):
        """comment"""
        content = []
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            description = card_document.get('description', '')
            externalreference = card_document.get('externalreference', '')
            id = card_document.get('id', '')
            lastchanged = card_document.get('lastchanged', '')
            notes = card_document.get('notes', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            reviewer = card_document.get('reviewer', '')
            rootcauseanalysis = card_document.get('rootcauseanalysis', '')
            state = card_document.get('state', '')
            status = card_document.get('status', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            search = ""
            search_mode = session_document.get('search_mode', '')
            highlight_attrs_values = {}
            if search_mode == 'simple':
                [attribute, search] = session_document.get('simple_search', ['', ''])
                if attribute:
                    if attribute in card_document and re.search('(?i)'+search, card_document[attribute]):
                        highlight_attrs_values[attribute] = search
                    
                if search and not highlight_attrs_values:
                    for (attribute, value) in [('title', title), ('description', description),
                                               ('status', status), ('notes', notes),
                                               ('externalreference', externalreference),
                                               ('rootcauseanalysis', rootcauseanalysis)]:
                        if re.search('(?i)'+search, value):
                            highlight_attrs_values[attribute] = search
                    
            elif search_mode == 'advanced':
                potential_highlight_attrs_values = session_document.get('advanced_search', {})
                for attribute in potential_highlight_attrs_values:
                    if card_document.get(attribute, ''):
                        if self.card_attributes_datatypes[attribute] == 'boolean':
                            continue
                        elif self.card_attributes_datatypes[attribute] == 'datetime':
                            value = self.convert_time_to_display_format(card_document[attribute])
                        else:
                            value = card_document[attribute]
                
                        if re.search(f'(?i){search}', value):
                            highlight_attrs_values[attribute] = potential_highlight_attrs_values[attribute]

            if highlight_attrs_values:
                content.append('<div class="card">')
                table_initialise, _ = self.assign_card_table_class('search', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>'+id+'</th><th>')
                content.append('<sup class="lastchanged">'+self.convert_time_to_display_format(lastchanged)+'</sup>')
                content.append('</th></tr><tr><th colspan="3">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead><tbody><tr><td colspan="3"><table>')
                for attribute in highlight_attrs_values:
                    if attribute in card_document and card_document[attribute]:
                        search = highlight_attrs_values[attribute]
                        if self.card_attributes_datatypes[attribute] == 'datetime':
                            value = self.convert_time_to_display_format(card_document[attribute])
                        else:
                            value = str(card_document[attribute])

                        for highlightable_text in [search, search.capitalize(), search.upper(), search.lower()]:
                            if highlightable_text in value:
                                if highlightable_text not in ['high']:
                                    value = value.replace(highlightable_text, f'<b class="highlight">{highlightable_text}</b>')

                        content.append(f'<tr><th valign="top"><p class="{fontsize}"><u>{self.displayable_key(attribute)}</u></p></th>')
                        content.append(f'<td><p class="{fontsize}">{value}</p></td></tr>')

                content.append('</table></td></tr></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_similars_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            words = card_document['title'].lower().split(' ')
            similar_titles = []
            closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
            for other_card_document in self.cards_collection.find({'project': card_document['project'],
                                                                   '_id': {'$ne': ObjectId(doc_id)},
                                                                   'type': card_document['type'],
                                                                   'state': {'$nin': closed_states}}):
                if 'title' in other_card_document:
                    other_words = other_card_document['title'].lower().split(' ')
                    matches = set(words).intersection(set(other_words))
                    if len(matches) >= len(words) / 2:
                        similar_titles.append((other_card_document['title'], other_card_document['id']))

            if similar_titles:

                doc_id, blocked, coowner, coreviewer, deferred, expedite, owner, parent = self.get_card_attribute_values(card_document,
                                                                    ['_id', 'blocked', 'coowner', 'coreviewer', 'deferred', 'expedite', 'owner', 'parent'])
                priority, release, reviewer, severity, state, title, type = self.get_card_attribute_values(card_document,
                                                         ['priority', 'release', 'reviewer', 'severity', 'state', 'title', 'type'])
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(card_document['project'], state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, card_document['project'], state, priority,
                                                        severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('reopened', session_document, card_document, mode, state,
                                                                   type, owner, coowner, reviewer,
                                                                   coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th></th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody><tr><td>')
                if similar_titles:
                    content.append('<h4>Cards With Similar Titles:</h4>')
                    content.append('<ul>')
                    for (similar_title, similar_id) in similar_titles:
                        content.append('<li><a href="/cards/view_card?id='+similar_id+'">'+similar_title+'</a></li>')

                    content.append('</ul>')

                content.append('</td></tr></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_tabbed_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                    someone_else_is_stuck_status, projection):
        """comment"""
        content = []
        epoch = datetime.datetime.utcnow()
        blocked_reason = ""
        hierarchy = False
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            actualcost = Decimal(card_document.get('actualcost', 0))
            actualtime = float(card_document.get('actualtime', 0))
            affectsversion = card_document.get('affectsversion', '')
            blocked = card_document.get('blocked', '')
            blockedhistory = card_document.get('blockedhistory', [])
            blockeduntil = card_document.get('blockeduntil', '')
            category = card_document.get('category', '')
            comments = card_document.get('comments', [])
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            creator = card_document.get('creator', '')
            crmcase = card_document.get('crmcase', '')
            deadline = card_document.get('deadline', '')
            deferred = card_document.get('deferred', '')
            deferreduntil = card_document.get('deferreduntil', '')
            dependsupon = card_document.get('dependsupon', '')
            description = card_document.get('description', '')
            escalation = card_document.get('escalation', '')
            estimatedcost = Decimal(card_document.get('estimatedcost', 0))
            estimatedtime = float(card_document.get('estimatedtime', 0))
            expedite = card_document.get('expedite', '')
            externalhyperlink = card_document.get('externalhyperlink', '')
            externalreference = card_document.get('externalreference', '')
            fixversion = card_document.get('fixversion', '')
            hierarchy = card_document.get('hierarchy', '')
            id = card_document.get('id', '')
            iteration = card_document.get('iteration', '')
            lastchanged = card_document.get('lastchanged', '')
            lastchangedby = card_document.get('lastchangedby', '')
            nextaction = card_document.get('nextaction', '')
            notes = card_document.get('notes', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            resolution = card_document.get('resolution', '')
            reviewer = card_document.get('reviewer', '')
            rootcauseanalysis = card_document.get('rootcauseanalysis', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            statehistory = card_document.get('statehistory', [])
            status = card_document.get('status', '')
            stuck = card_document.get('stuck', '')
            testcases = card_document.get('testcases', [])
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            project_document = self.projects_collection.find_one({'project': project})
            metastate = self.get_corresponding_metastate(project_document, state)
            child_count = self.cards_collection.find({"parent": id}).count()
            if dependsupon:
                dependsupon_count = self.cards_collection.find({"id": dependsupon, 'state': {'$ne': 'closed'}}).count()
            else:
                dependsupon_count = 0

            if parent or child_count or dependsupon_count or affectsversion or fixversion:
                hierarchy = True

            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

            project_document = self.projects_collection.find_one({'project': project})
            workflow_index = project_document.get('workflow_index', {})
            buffer_column_states = workflow_index.get('buffer_column_states', [])
            uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
            condensed_column_states = workflow_index.get('condensed_column_states', [])
            backlog_states = self.get_custom_states_mapped_onto_metastates(['backlog'])
            analysis_states = self.get_custom_states_mapped_onto_metastates(['analysis'])
            analysed_states = self.get_custom_states_mapped_onto_metastates(['analysed'])
            design_states = self.get_custom_states_mapped_onto_metastates(['design'])
            designed_states = self.get_custom_states_mapped_onto_metastates(['designed'])
            closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
            if state in buffer_column_states:
                content.append('<table class="'+type+'buffered">')
                innertable_class = type+'buffered'
            elif metastate == "testing" and member_document and "teammember" in member_document and ((owner and owner == member_document['teammember']) or (coowner and coowner == member_document['teammember'])):
                content.append('<table class="'+type+'ghosted">')
                innertable_class = type+'ghosted'
            # TODO - Need to involve Centric list here!
            elif metastate != "testing" and member_document and "teammember" in member_document and ((reviewer and reviewer == member_document['teammember']) or (coreviewer and coreviewer == member_document['teammember'])):
                content.append('<table class="'+type+'ghosted">')
                innertable_class = type+'ghosted'
            elif self.card_blocked_by_child(card_document):
                content.append('<table class="'+type+'blocked">')
                blocked_reason = 'This '+type+' is blocked by one of its children'
                innertable_class = type+'blocked'
            else:
                content.append('<table class="'+type+'">')
                innertable_class = type

            content.append('<thead><tr><th valign="top">')

            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))

            content.append('</th><th width="30%"><img src="/images/yellowpin16.png" style="float: left;"/> ')
            if type == 'story':
                content.append('<p class="'+fontsize+'">'+type.capitalize()+' <a href="/cards/view_card?id='+id+'" title="View story">'+id+'</a></p>')
            else:
                content.append('<p class="'+fontsize+'">'+type.capitalize()+' <a href="/cards/view_card?id='+id+'" title="View '+type+'">'+id+'</a></p>')

            content.append('</th>')
            content.append('<td class="title" width="70%">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</td></tr>')

            inherited_deadline, inherited_deadline_entity = self.calculate_inherited_deadline(project, release, iteration)

            warning_statements, days_stale = self.assemble_card_warning_statements(statehistory, blocked_reason, blocked, blockedhistory, blockeduntil, deferred, deferreduntil, coowner, coreviewer, deadline, inherited_deadline, inherited_deadline_entity, lastchanged, nextaction, owner, project, reviewer, session_document, someone_else_is_stuck_status, state, stuck, type, resolution)

            if warning_statements:
                content.append('<tr>')
                if days_stale > 90:
                    content.append('<td class="antiquated" colspan="3">')
                else:
                    content.append('<td class="warning" colspan="3">')

                content.append(self.assemble_warning_statements(fontsize, warning_statements, days_stale))
                content.append('</td></tr>')

            content.append('</thead>')

            if "minimised" in mode:
                content.append('<tbody class="minimised'+type+'">')
            else:
                content.append('<tbody>')

            if os.path.exists(self.current_dir+os.sep+'attachments'+os.sep+project+os.sep+id):
                attachments = os.listdir(self.current_dir+os.sep+'attachments'+os.sep+project+os.sep+id)
            else:
                attachments = []

            content.append('<tr><td colspan="3"><div class="tabs" class="ui-tabs ui-widget ui-widget-content ui-corner-all">')

            # Start of tab headings
            content.append('<ul class="tabs" class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all">')
            if status:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Status"><a href="#statustab{id}"><span class="ui-icon ui-icon-note" /></a></li>')

            if description and description != title:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Description"><a href="#descriptiontab{id}"><span class="ui-icon ui-icon-document" /></a></li>')

            if notes:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Notes"><a href="#notestab{id}"><span class="ui-icon ui-icon-note" /></a></li>')

            if project or priority or release or iteration:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Project"><a href="#projecttab{id}"><span class="ui-icon ui-icon-suitcase" /></a></li>')

            if creator or owner or coowner or reviewer or coreviewer:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active"><a href="#personneltab{id}">')
                # Display User Avatar
                avatar_found = False
                if usertypes:
                    for image_file_format in ['.png', '.gif', '.jpg']:
                        if 'owner' in usertypes and owner:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+owner.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+owner.replace(' ', '')+image_file_format+'" height="13" width="13" title="'+owner+'">')
                                avatar_found = True
                                break

                        elif 'coowner' in usertypes and coowner:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+coowner.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+coowner.replace(' ', '')+image_file_format+'" height="13" width="13" title="'+coowner+'">')
                                avatar_found = True
                                break

                        elif 'reviewer' in usertypes and reviewer:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+reviewer.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+reviewer.replace(' ', '')+image_file_format+'" height="13" width="13" title="'+reviewer+'">')
                                avatar_found = True
                                break

                        elif 'coreviewer' in usertypes and coreviewer:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+coreviewer.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+coreviewer.replace(' ', '')+image_file_format+'" height="13" width="13" title="'+coreviewer+'">')
                                avatar_found = True
                                break

                if not avatar_found:
                    content.append('<span class="ui-icon ui-icon-person" title="Personnel" />')

                content.append('</a></li>')

            if hierarchy:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Hierarchy"><a href="#hierarchytab{id}"><span class="ui-icon ui-icon-copy" /></a></li>')

            if estimatedtime or actualtime or len(statehistory) > 1:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Time"><a href="#timetab{id}"><span class="ui-icon ui-icon-clock" /></a></li>')

            if estimatedcost or actualcost:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Cost"><a href="#costtab{id}"><span class="ui-icon ui-icon-calculator" /></a></li>')

            if creator and creator != owner:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Admin"><a href="#admintab{id}"><span class="ui-icon ui-icon-wrench" /></a></li>')

            if testcases:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Test Cases"><a href="#testcasestab{id}"><span class="ui-icon ui-icon-arrowthickstop-1-e" /></a></li>')

            if crmcase or escalation or externalreference or externalhyperlink:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Links"><a href="#linkstab{id}"><span class="ui-icon ui-icon-extlink" /></a></li>')

            if comments:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Comments"><a href="#commentstab{id}"><span class="ui-icon ui-icon-comment" /></a></li>')

            if attachments:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Attachments"><a href="#attachmentstab{id}"><span class="ui-icon ui-icon-folder-collapsed" /></a></li>')

            if rootcauseanalysis:
                content.append(f'<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active" title="Root-Cause Analysis"><a href="#rootcauseanalysistab{id}"><span class="ui-icon ui-icon-alert" /></a></li>')

            content.append('</ul>')
            # End of tab headings

            # Status Tab
            if status:
                content.append(f'<div class="tab_content" id="statustab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                status = self.format_multiline(status)
                content.append('<p class="'+fontsize+'left">'+status+'</p>')
                content.append('</div>')

                if 'showstate' in mode:
                    content.append('<table class="'+innertable_class+'">')
                    content.append('<tr><th>State</th><td><p class="'+fontsize+'left">'+state.capitalize()+'</p></td></tr>')
                    content.append('</table>')

            # Description Tab
            if description and description != title:
                content.append(f'<div class="tab_content" id="descriptiontab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append('<p class="'+fontsize+'left">'+description+'</p>')
                content.append('</div>')

            # Notes Tab
            if notes:
                content.append(f'<div class="tab_content" id="notestab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                notes = self.format_multiline(notes)
                content.append('<p class="'+fontsize+'left">'+notes+'</p>')
                content.append('</div>')

            # Project Tab
            content.append(f'<div class="tab_content" id="projecttab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
            content.append('<table class="'+innertable_class+'">')
            if project or priority or severity:
                quoted_project = urllib.parse.quote_plus(project)
                quoted_priority = urllib.parse.quote_plus(priority)
                content.append('<tr><th>Project</th><td><p class="'+fontsize+'left">'+project+'</p></td></tr>')
                content.append('<tr><th>Priority</th><td><p class="'+fontsize+'left">'+priority.capitalize()+'</p></td></tr>')
                content.append('<tr><th>Severity</th><td><p class="'+fontsize+'left">'+severity.capitalize()+'</p></td></tr>')

            # Release and Iteration
            if release or iteration:
                quoted_release = urllib.parse.quote_plus(release)
                quoted_iteration = urllib.parse.quote_plus(iteration)
                if release == iteration:
                    content.append('<tr><th>Release/Iteration</th><td><p class="'+fontsize+'left">'+release+'</p></td><td></td><td></td></tr>')
                else:
                    content.append('<tr><th>Release</th><td><p class="'+fontsize+'left">'+release+'</p></td><th>Iteration</th><td><p class="'+fontsize+'left">'+iteration+'</p></td></tr>')

            content.append('</table>')
            content.append('</div>')

            # Personnel Tab
            if creator or owner or coowner or reviewer or coreviewer:
                content.append(f'<div class="tab_content" id="personneltab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append('<table class="'+innertable_class+'">')

                creator_owner_heading = False
                coowner_heading = False
                coreviewer_heading = False
                if creator == owner:
                    creator_heading = False
                    owner_heading = False
                    creator_owner_heading = '<th>Creator/Owner</th>'
                else:
                    creator_heading = '<th>Creator</th>'

                if owner == creator:
                    creator_heading = False
                    owner_heading = False
                    creator_owner_heading = '<th>Creator/Owner</th>'
                else:
                    owner_heading = '<th>Owner</th>'

                if coowner:
                    coowner_heading = '<th>Co-Owner</th>'

                reviewer_heading = '<th>Reviewer</th>'
                if coreviewer:
                    coreviewer_heading = '<th>Co-Reviewer</th>'

                content.append('<tr>')
                if creator_owner_heading:
                    content.append(creator_owner_heading)
                else:
                    content.append(creator_heading)
                    content.append(owner_heading)

                if coowner_heading:
                    content.append(coowner_heading)

                content.append(reviewer_heading)
                if coreviewer_heading:
                    content.append(coreviewer_heading)

                content.append('</tr>')

                content.append('<tr>')

                if creator_owner_heading:
                    if owner:
                        content.append('<td><p class="'+fontsize+'">')
                        # Display User Avatar
                        avatar_found = False
                        for image_file_format in ['.png', '.gif', '.jpg']:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+owner.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+owner.replace(' ', '')+image_file_format+'">')
                                avatar_found = True
                                break

                        if not avatar_found:
                            content.append('<img src="/images/avatars/default.jpg">')

                        content.append('<br>'+owner+'</p></td>')
                    elif metastate in ['defined', 'analysis', 'analysed', 'design', 'designed',
                                       'development', 'developed', 'unittesting',
                                       'integrationtesting', 'systemtesting',
                                       'acceptancetesting']:
                        content.append('<td class="warning"><span style="float:left" class="ui-icon ui-icon-alert" title="An owner should be assigned!" /></td>')
                    else:
                        content.append('<td></td>')

                else:
                    if creator:
                        content.append('<td><p class="'+fontsize+'">')
                        # Display User Avatar
                        avatar_found = False
                        for image_file_format in ['.png', '.gif', '.jpg']:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+creator.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+creator.replace(' ', '')+image_file_format+'">')
                                avatar_found = True
                                break

                        if not avatar_found:
                            content.append('<img src="/images/avatars/default.jpg">')

                        content.append('<br>'+creator+'</p></td>')
                    else:
                        content.append('<td></td>')

                    if owner:
                        content.append('<td><p class="'+fontsize+'">')
                        # Display User Avatar
                        avatar_found = False
                        for image_file_format in ['.png', '.gif', '.jpg']:
                            if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+owner.replace(' ', '')+image_file_format):
                                content.append('<img src="/images/avatars/'+owner.replace(' ', '')+image_file_format+'">')
                                avatar_found = True
                                break

                        if not avatar_found:
                            content.append('<img src="/images/avatars/default.jpg">')

                        content.append('<br>'+owner+'</p</td>')
                    elif metastate in ['defined', 'analysis', 'analysed', 'development', 'developed', 'unittesting',
                                       'integrationtesting', 'systemtesting', 'acceptancetesting']:
                        content.append('<td class="warning"><span style="float:left" class="ui-icon ui-icon-alert" title="An owner should be assigned!" /></td>')
                    else:
                        content.append('<td></td>')

                if coowner_heading:
                    content.append('<td><p class="'+fontsize+'">')
                    # Display User Avatar
                    avatar_found = False
                    for image_file_format in ['.png', '.gif', '.jpg']:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+coowner.replace(' ', '')+image_file_format):
                            content.append('<img src="/images/avatars/'+coowner.replace(' ', '')+image_file_format+'">')
                            avatar_found = True
                            break

                    if not avatar_found:
                        content.append('<img src="/images/avatars/default.jpg">')

                    content.append('<br>'+coowner+'</p></td>')

                if reviewer:
                    content.append('<td><p class="'+fontsize+'">')
                    # Display User Avatar
                    avatar_found = False
                    for image_file_format in ['.png', '.gif', '.jpg']:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+reviewer.replace(' ', '')+image_file_format):
                            content.append('<img src="/images/avatars/'+reviewer.replace(' ', '')+image_file_format+'">')
                            avatar_found = True
                            break

                    if not avatar_found:
                        content.append('<img src="/images/avatars/default.jpg">')

                    content.append('<br>'+reviewer+'</p></td>')
                elif metastate in ['unittesting', 'integrationtesting', 'systemtesting', 'acceptancetesting']:
                    content.append('<td class="warning"><span style="float:left" class="ui-icon ui-icon-alert" title="A reviewer should be assigned!" /></td>')
                else:
                    content.append('<td></td>')

                if coreviewer_heading:
                    content.append('<td><p class="'+fontsize+'">')
                    # Display User Avatar
                    avatar_found = False
                    for image_file_format in ['.png', '.gif', '.jpg']:
                        if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+coreviewer.replace(' ', '')+image_file_format):
                            content.append('<img src="/images/avatars/'+coreviewer.replace(' ', '')+image_file_format+'">')
                            avatar_found = True
                            break

                    if not avatar_found:
                        content.append('<img src="/images/avatars/default.jpg">')

                    content.append('<br>'+coreviewer+'</p></td>')

                content.append('</tr></table></div>')

            # Hierarchy Tab
            content.append(f'<div class="tab_content" id="hierarchytab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
            content.append('<table class="'+innertable_class+'">')
            if parent and self.cards_collection.find({'id': parent}).count():
                parent_document = self.cards_collection.find_one({'id': parent})
                parent_id = parent_document.get('id', '')
                parent_state = parent_document.get('state', '')
                parent_title = parent_document.get('title', '')
                parent_type = parent_document.get('type', '')
                content.append('<tr><th>Parent</th><td colspan="3"><table class="embedded'+parent_type+'">')
                content.append('<tr><th>ID</th><th>Title</th><th>State</th></tr>')
                content.append('<tr><th>')
                buttons = self.ascertain_card_menu_items(parent_document, member_document)
                content.append(self.assemble_card_menu(member_document, parent_document, buttons, 'index'))
                content.append('<p class="'+fontsize+'"><a href="/cards/view_card?id='+parent_id+'" title="View '+parent_type+'">'+parent_id+'</a></p></th>')
                if parent_title:
                    content.append('<td><p class="'+fontsize+'">'+parent_title+'</p></td>')
                else:
                    content.append('<td></td>')

                content.append('<td><p class="'+fontsize+'">'+parent_state+'</p></td>')
                content.append('</tr></table></td></tr>')

            if self.cards_collection.find({"parent": id}).count():
                content.append('<tr><th>Children</th><td colspan="3"><table>')
                content.append('<tr><th>ID</th><th>Title</th><th>State</th></tr>')
                for child_document in self.cards_collection.find({"parent": id}):
                    child_id = child_document.get('id', '')
                    child_state = child_document.get('state', '')
                    child_title = child_document.get('title', '')
                    child_type = child_document.get('type', '')
                    content.append('<tr><td class="'+child_type+'" title="'+child_type.capitalize()+'">')
                    buttons = self.ascertain_card_menu_items(child_document, member_document)
                    content.append(self.assemble_card_menu(member_document, child_document, buttons, 'index'))
                    content.append('<p class="'+fontsize+'"><a href="/cards/view_card?id='+child_id+'">'+child_id+'</a></p></td>')
                    content.append('<td><p class="'+fontsize+'">'+child_title+'</p></td>')
                    content.append('<td><p class="'+fontsize+'">'+child_state+'</p></td></tr>')

                content.append('</table></td></tr>')

            if dependsupon:
                closed_states = self.get_custom_states_mapped_onto_metastates(['completed', 'closed'])
                other_document = self.cards_collection.find_one({"id": dependsupon, 'state': {'$nin': closed_states}})
                if other_document:
                    content.append('<tr><th>Depends Upon</th><td colspan="3"><table class="embedded'+type+'"><tr><th><p class="'+fontsize+'">'+dependsupon+'</p></th>')
                    if 'title' in other_document and other_document['title']:
                        content.append('<td><p class="'+fontsize+'">'+other_document['title']+'</p></td>')
                    else:
                        content.append('<td></td>')

                    content.append('<td><p class="'+fontsize+'">'+other_document['state']+'</p></td></tr></table></td></tr>')

            # AffectsVersion and FixVersion
            if affectsversion and fixversion:
                content.append('<tr><th>Affects Version</th><td><p class="'+fontsize+'">'+affectsversion+'</p></td><th>Fix Version</th><td><p class="'+fontsize+'">'+fixversion+'</p></td></tr>')
            elif affectsversion:
                content.append('<tr><th>Affects Version</th><td colspan="3"><p class="'+fontsize+'">'+affectsversion+'</p></td></tr>')
            elif fixversion:
                content.append('<tr><th>Fix Version</th><td colspan="3"><p class="'+fontsize+'">'+fixversion+'</p></td></tr>')

            content.append('</table></div>')

            # Time Tab
            if estimatedtime or actualtime or len(statehistory) > 1:
                unit = "day"
                project_document = self.projects_collection.find_one({'project': project})
                if project_document:
                    unit = project_document.get('unit', 'day')

                content.append(f'<div class="tab_content" id="timetab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')

                if estimatedtime or actualtime:
                    content.append('<table class="'+innertable_class+'">')
                    content.append('<tr>')
                    if estimatedtime and estimatedtime <= 1:
                        et_unit = ' '+unit
                    else:
                        et_unit = ' '+unit+'s'

                    if actualtime and actualtime <= 1:
                        at_unit = ' '+unit
                    else:
                        at_unit = ' '+unit+'s'

                    if estimatedtime and actualtime:
                        if estimatedtime == actualtime:
                            content.append(f'<th><a href="/kanban/times_report">Estimated Time/Actual Time</a></th><td><p class="{fontsize}">{estimatedtime}{et_unit}</p></td><td></td><td></td>')
                        else:
                            content.append(f'<th><a href="/kanban/times_report">Estimated Time</a></th><td><p class="{fontsize}">{estimatedtime}{et_unit}</p></td><th><a href="/kanban/times_report">ActualTime</a></th>')
                            if actualtime > estimatedtime:
                                content.append(f'<td class="warning"><p class="{fontsize}">{actualtime}{at_unit}</p></td>')
                            else:
                                content.append(f'<td><p class="{fontsize}">{actualtime}{at_unit}</p></td>')

                    elif estimatedtime:
                        content.append(f'<th><a href="/kanban/times_report">Estimated Time</a></th><td colspan="3"><p class="{fontsize}">{estimatedtime}{et_unit}</p></td>')
                    elif actualtime:
                        content.append(f'<th><a href="/kanban/times_report">Actual Time</a></th><td colspan="3"><p class="{fontsize}">{actualtime}{at_unit}</p></td>')

                    content.append('</tr></table><p></p>')

                if len(statehistory) > 1:
                    epoch = datetime.datetime.utcnow()
                    content.append('<table class="'+innertable_class+'">')
                    content.append('<tr><th>State</th><th>Date Entered</th><th>Days In State</th><th>Cumulative</th></tr>')
                    cumulative = 0
                    state_metrics = self.get_state_metrics(condensed_column_states, card_document['statehistory'])
                    for backlog_state in backlog_states:
                        backlog = state_metrics.get(backlog_state, 0)
                        if backlog:
                            break

                    for analysis_state in analysis_states:
                        analysis = state_metrics.get(analysis_state, 0)
                        if analysis:
                            break

                    for analysed_state in analysed_states:
                        analysed = state_metrics.get(analysed_state, 0)
                        if analysed:
                            break

                    for design_state in design_states:
                        design = state_metrics.get(design_state, 0)
                        if design:
                            break                         

                    for designed_state in designed_states:
                        designed = state_metrics.get(designed_state, 0)
                        if designed:
                            break                       
                    
                    for closed_state in closed_states:
                        closed = state_metrics.get(closed_state, 0)
                        if closed:
                            break 

                    state_metrics = [backlog, analysis, analysed, closed]
                    textual_states = uncondensed_column_states
                    for sm, state_metric in enumerate(state_metrics):
                        if state_metric:
                            if textual_states[sm] in ['closed']:
                                days_in_state = 0
                            elif sm < len(state_metrics)-1:
                                next_state_metric = 0
                                for sm_next_no, sm_next in enumerate(state_metrics):
                                    if sm_next_no > sm:
                                        if sm_next:
                                            next_state_metric = sm_next
                                            break

                                if next_state_metric:
                                    days_in_state = int((next_state_metric-state_metric)/self.timedelta_day)
                                else:
                                    days_in_state = int((epoch-state_metric)/self.timedelta_day)

                            else:
                                days_in_state = int((epoch-state_metric)/self.timedelta_day)

                            modifiedtime_string = "%4d-%02d-%02d" % (state_metric.year, state_metric.month, state_metric.day)
                            cumulative += days_in_state
                            content.append(f'<tr><td><p class="{fontsize}">{textual_states[sm].capitalize()}</p></td><td><p class="{fontsize}">{modifiedtime_string}</p></td><td align="right"><p class="{fontsize}">{days_in_state}</p></td><td align="right"><p class="{fontsize}">{cumulative}</p></td></tr>')

                    content.append('</table>')

                if nextaction or deadline or lastchanged or inherited_deadline:
                    content.append('<p></p><table class="'+innertable_class+'">')

                    if nextaction:
                        content.append('<tr><th>Next Action</th>')
                        modifiedtime_string = "%4d-%02d-%02d" % (nextaction.year, nextaction.month, nextaction.day)
                        epoch = datetime.datetime.utcnow()
                        if metastate not in ['closed', 'completed', 'acceptancetestingaccepted'] and nextaction < epoch:
                            days_different = int((epoch-nextaction)/self.timedelta_day)
                            if days_different == 0:
                                title_string = 'This '+type+' requires attention today!'
                            elif days_different == 1:
                                title_string = 'This '+type+' required attention yesterday!'
                            else:
                                title_string = 'This '+type+' required attention '+str(days_different)+' days ago!'

                            content.append('<td colspan="3" class="warning"><p class="'+fontsize+'"><span style="float:left" class="ui-icon ui-icon-alert" title="'+modifiedtime_string+'" /> '+modifiedtime_string+'</p></td>')
                        else:
                            content.append('<td><p class="'+fontsize+'">'+modifiedtime_string+'</p></td>')

                        content.append('</tr>')

                    if metastate not in ['closed', 'completed', 'acceptancetestingaccepted']:
                        if deadline:
                            content.append('<tr><th>Deadline</th>')
                            modifiedtime_string = "%4d-%02d-%02d" % (deadline.year, deadline.month, deadline.day)
                            epoch = datetime.datetime.utcnow()
                            if metastate not in ['closed', 'completed', 'acceptancetestingaccepted'] and deadline < epoch:
                                days_different = int((epoch-deadline)/self.timedelta_day)
                                if days_different == 0:
                                    title_string = 'This '+type+' is due today!'
                                elif days_different == 1:
                                    title_string = 'This '+type+' is 1 day overdue!'
                                else:
                                    title_string = 'This '+type+' is '+str(days_different)+' days overdue!'

                                content.append('<td colspan="3" class="warning"><p class="'+fontsize+'"><span style="float:left" class="ui-icon ui-icon-alert" title="'+modifiedtime_string+'" /> '+modifiedtime_string+'</p></td>')
                            else:
                                content.append('<td><p class="'+fontsize+'">'+modifiedtime_string+'</p></td>')

                            content.append('</tr>')
                        elif inherited_deadline:
                            content.append('<tr><th>Inherited Deadline</th>')
                            modifiedtime_string = "%4d-%02d-%02d" % (inherited_deadline.year, inherited_deadline.month, inherited_deadline.day)
                            datetime_now = datetime.datetime.utcnow()
                            if metastate not in ['closed', 'completed', 'acceptancetestingaccepted'] and inherited_deadline < datetime_now:
                                days_different = int((datetime_now-inherited_deadline)/self.timedelta_day)
                                if days_different == 0:
                                    title_string = 'This '+type+' is due today as the '+inherited_deadline_entity+' has finished!'
                                elif days_different == 1:
                                    title_string = 'This '+type+' is 1 day overdue as the '+inherited_deadline_entity+' has finished!'
                                else:
                                    title_string = 'This '+type+' is '+str(days_different)+' days overdue as the '+inherited_deadline_entity+' has finished!'

                                content.append('<td colspan="3" class="warning"><p class="'+fontsize+'"><span style="float:left" class="ui-icon ui-icon-alert" title="'+modifiedtime_string+'" /> '+modifiedtime_string+'</p></td>')
                            else:
                                content.append('<td><p class="'+fontsize+'">'+modifiedtime_string+'</p></td>')

                            content.append('</tr>')

                    # Resolution
                    if resolution:
                        content.append('<tr><th>Resolution</th><td colspan="3"><p class="'+fontsize+'">'+resolution+'</p></td></tr>')

                    # Last Changed
                    if lastchanged:
                        modifiedtime_string = "%4d-%02d-%02d" % (lastchanged.year, lastchanged.month, lastchanged.day)
                        if lastchangedby:
                            content.append('<tr><th>Last Changed</th><td><p class="'+fontsize+'">'+modifiedtime_string+'</p></td><th>Last Changed By</th><td><p class="'+fontsize+'">'+lastchangedby+'</p></td></tr>')
                        else:
                            content.append(f'<tr><th>Last Changed</th><td colspan="3"><p class="{fontsize}">{modifiedtime_string}</p></td></tr>')

                    content.append('</table>')

                content.append('</div>')

            # Cost Tab
            if estimatedcost or actualcost:
                content.append(f'<div class="tab_content" id="costtab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append(f'<table class="{innertable_class}"><tr>')
                if estimatedcost and actualcost:
                    content.append(f'<th><a href="/kanban/costs_report">Estimated Cost</a></th><td><p class="{fontsize}">{estimatedcost}</p></td><th><a href="/kanban/costs_report">Actual Cost</a></th><td><p class="{fontsize}">{actualcost}</p></td>')
                elif estimatedcost:
                    content.append(f'<th><a href="/kanban/costs_report">Estimated Cost</a></th><td colspan="3"><p class="{fontsize}">{estimatedcost}</p></td>')
                elif actualcost:
                    content.append(f'<th><a href="/kanban/costs_report">Actual Cost</a></th><td colspan="3"><p class="{fontsize}">{actualcost}</p></td>')

                content.append('</tr></table></div>')

            # Admin Tab
            if creator and creator != owner:
                content.append(f'<div class="tab_content" id="admintab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append('<table class="'+innertable_class+'">')
                # Added By
                content.append(f'<tr><th>Creator</th><td colspan="3"><p class="{fontsize}">{creator}</p></td></tr>')
                content.append('</table></div>')

            #Test Cases Tab
            if testcases:
                content.append(f'<div class="tab_content" id="testcasestab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append('<table class="'+innertable_class+'"><tr><th>Title</th><th>Description</th><th>State</th></tr>')
                for testcase_document in testcases:
                    if 'description' in testcase_document:
                        content.append('<tr><td><p class="'+fontsize+'">'+testcase_document['title']+'</p></td><td><p class="'+fontsize+'">'+testcase_document['description']+'</p></td><td><p class="'+fontsize+'">'+testcase_document['state']+'</p></td></tr>')
                    else:
                        content.append('<tr><td><p class="'+fontsize+'">'+testcase_document['title']+'</p></td><td></td><td><p class="'+fontsize+'">'+testcase_document['state']+'</p></td></tr>')

                content.append('</table></div>')

            # Links Tab
            if crmcase or escalation or externalreference or externalhyperlink:
                content.append(f'<div class="tab_content" id="linkstab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append('<table class="'+innertable_class+'">')

                if crmcase:
                    content.append('<tr><th>CRM Case</th><td><p class="'+fontsize+'">'+crmcase+'</p></td></tr>')

                if escalation:
                    content.append('<tr><th>Escalation</th><td><p class="'+fontsize+'">'+escalation+'</p></td></tr>')

                if externalreference:
                    content.append('<tr><th>External Reference</th><td><p class="'+fontsize+'">'+externalreference+'</p></td></tr>')

                if externalhyperlink:
                    content.append('<tr><th>External Hyperlink</th><td><p class="'+fontsize+'"><a href="'+externalhyperlink+'">'+externalhyperlink+'</a></p></td></tr>')

                content.append('</table></div>')

            # Comments Tab
            if comments:
                content.append(f'<div class="tab_content" id="commentstab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append('<table class="'+innertable_class+'">')
                for comment_document in comments:
                    comment_class = self.ascertain_comment_class(comment_document, owner, coowner)
                    date_format = self.convert_time_to_display_format(comment_document['datetime'])
                    content.append('<tr><th><sup class="'+comment_class+'">'+comment_document['username']+' on '+date_format+'</sup></th></tr>')
                    modified_comment = self.format_multiline(comment_document['comment'])
                    content.append('<tr><td><div class="'+comment_class+'"><p>'+modified_comment+'</p></div></td></tr>')

                content.append('</table></div>')

            # Attachments Tab
            if attachments:
                content.append(f'<div class="tab_content" id="attachmentstab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                content.append(f'<table class="{innertable_class}">')
                for attachment in attachments:
                    content.append('<tr><td><p class="'+fontsize+'"><a href="/kanban/attachments/'+project+'/'+id+'/'+attachment+'">'+attachment+'</a></p></td></tr>')

                content.append('</table><div id="dropfile">Drop Attachment Here!</div></div>')

            # Root-Cause Analysis Tab
            if rootcauseanalysis:
                content.append(f'<div class="tab_content" id="rootcauseanalysistab{id}" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
                rootcauseanalysis = self.format_multiline(rootcauseanalysis)
                content.append('<p class="'+fontsize+'">'+rootcauseanalysis+'</p>')
                content.append('</div>')

            content.append('</div>') # End of div holding tab headings and dialogs

            content.append('</td></tr>') # End of table cell holding tab dialog

            content.append('<tr><th colspan="3">')
            content.append(self.show_category_in_top_right(project, category))

            if metastate == 'closed':
                if 'resolution' in card_document and card_document['resolution']:
                    content.append('<sup class="resolution">'+card_document['resolution']+'</sup>')

            content.append(self.insert_new_recent_days_old_message(card_document, state, epoch))
            content.append('</th></tr>')

            content.append('</tbody></table></div>')
        else:
            content.append("<p>Document has been deleted</p>")

        return "".join(content)

    def assemble_testcases_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                       someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            testcases = card_document.get('testcases', [])
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if 'minimised' in mode:
                content.append(self.create_minimised_card_div(project, state))
            elif 'ghosted' in mode:
                content.append('<div class="ghostedcard">')
            else:
                content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite,
                                                    blocked, deferred, release))

            table_initialise, _ = self.assign_card_table_class('testcases', session_document, card_document, mode,
                                                               state, type, owner, coowner, reviewer, coreviewer,
                                                               member_document, projection)
            content.append(table_initialise)
            content.append('<thead><tr><th>')
            buttons = self.ascertain_card_menu_items(card_document, member_document)
            content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
            content.append('</th><th>')
            if not testcases:
                content.append('<sup class="testcase">No Test Cases</sup>')

            content.append('</th></tr><tr><th colspan="2">')
            content.append(self.insert_card_title(session_document, title, parent, fontsize))
            content.append('</th></tr></thead>')
            content.append('<tbody>')
            if testcases:
                content.append('<tr><td>')
                content.append(f'<table class="view{type}"><tr><th>Title</th><th>Description</th><th>State</th></tr>')
                for testcase_document in testcases:
                    content.append(f'<tr><td><p class="{fontsize}">{testcase_document["title"]}</p></td>')
                    if 'description' in testcase_document:
                        content.append(f'<td><p class="{fontsize}">{testcase_document["description"]}</p></td>')
                    else:
                        content.append('<td></td>')

                    content.append(f'<td><p class="{fontsize}">{testcase_document["state"]}</p></td></tr>')

                content.append('</table>')
                content.append('</td></tr>')

            content.append('</tbody>')
            content.append('</table>')
            content.append('</div>')

        return "".join(content)

    def assemble_time_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                  someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            actualtime = float(card_document.get('actualtime', 0))
            actualtimehistory = card_document.get('actualtimehistory', [])
            blocked = card_document.get('blocked', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            estimatedtime = float(card_document.get('estimatedtime', 0))
            estimatedtimehistory = card_document.get('estimatedtimehistory', [])
            expedite = card_document.get('expedite', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            if estimatedtime or actualtime:
                project_document = self.projects_collection.find_one({"project": project})
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('time', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                unit = ""
                et_unit = ""
                at_unit = ""
                if project_document and 'unit' in project_document and project_document['unit']:
                    unit = project_document['unit']

                if unit:
                    if estimatedtime and estimatedtime <= 1:
                        et_unit = ' '+unit
                    else:
                        et_unit = ' '+unit+'s'

                    if actualtime and actualtime <= 1:
                        at_unit = ' '+unit
                    else:
                        at_unit = ' '+unit+'s'

                if estimatedtime and actualtime:
                    time_to_go = estimatedtime-actualtime
                    content.append(f'<sup class="time">{estimatedtime}{et_unit} - {actualtime}{at_unit} = {time_to_go}{at_unit}</sup>')
                elif estimatedtime:
                    content.append(f'<sup class="time">{estimatedtime}{et_unit} - ? = ?</sup>')
                elif actualtime:
                    content.append(f'<sup class="time">? - {actualtime}{at_unit} = ?</sup>')

                content.append('</th></tr><tr><th colspan="2">')
                content.append('<p class="'+fontsize+'">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</p></th></tr></thead><tbody>')
                if estimatedtime and actualtime:
                    content.append('<tr><td colspan="2">')
                    content.append(self.insert_card_time_chart(estimatedtime, estimatedtimehistory, actualtime, actualtimehistory))
                    content.append('</td></tr>')

                content.append('</tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_today_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                   someone_else_is_stuck_status, projection):
        """comment"""
        content = []
        search_criteria = {"_id": ObjectId(doc_id)}
        warning_statements = []
        days_stale = 0
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        epoch = datetime.datetime.utcnow()
        fontsize = self.get_font_size(member_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            doc_id = card_document["_id"]
            blocked = card_document.get('blocked', '')
            category = card_document.get('category', '')
            coowner = card_document.get('coowner', '')
            coreviewer = card_document.get('coreviewer', '')
            deferred = card_document.get('deferred', '')
            expedite = card_document.get('expedite', '')
            nextaction = card_document.get('nextaction', '')
            owner = card_document.get('owner', '')
            parent = card_document.get('parent', '')
            priority = card_document.get('priority', '')
            project = card_document.get('project', '')
            release = card_document.get('release', '')
            resolution = card_document.get('resolution', '')
            reviewer = card_document.get('reviewer', '')
            severity = card_document.get('severity', '')
            startby = card_document.get('startby', '')
            state = card_document.get('state', '')
            title = card_document.get('title', '')
            type = card_document.get('type', '')
            project_document = self.projects_collection.find_one({'project': project})
            metastate = self.get_corresponding_metastate(project_document, state)
            if startby and metastate not in ['closed', 'completed', 'acceptancetestingaccepted']:
                time_to_display = startby
                if int(startby) <= epoch + self.timedelta_day:
                    days_different = int((epoch-int(startby))/self.timedelta_day)
                    if days_different == 0:
                        warning_statements.append('Work needs to start on this '+type+' by today!')
                    elif days_different == 1:
                        warning_statements.append('<b class="blink">Work needed to start on this '+type+' by yesterday!</b>')
                    elif days_different > 1:
                        warning_statements.append('<b class="blink">Work needed to start on this '+type+' by '+str(days_different)+' days ago!</b>')

            if nextaction and metastate not in ['closed', 'completed', 'acceptancetestingaccepted']:
                time_to_display = nextaction
                if nextaction <= epoch + self.timedelta_day:
                    days_different = int((epoch-nextaction)/self.timedelta_day)
                    if days_different == 0:
                        warning_statements.append('Action is due on this '+type+' today!')
                    elif days_different == 1:
                        warning_statements.append('<b class="blink">Action was due on this '+type+' yesterday!</b>')
                    elif days_different > 1:
                        warning_statements.append('<b class="blink">Action was due on this '+type+' '+str(days_different)+' days ago!</b>')

            if warning_statements:
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release))

                table_initialise, _ = self.assign_card_table_class('today', session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)

                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                modifiedtime_string = "%4d-%02d-%02d" % (time_to_display.year, time_to_display.month, time_to_display.day)
                content.append('<span class="today" title="Next Action">'+modifiedtime_string+'</span>')
                if metastate == 'closed' and resolution:
                    content.append('<sup class="resolution">'+resolution+'</sup>')

                content.append(self.show_category_in_top_right(project, category))
                content.append(self.insert_new_recent_days_old_message(card_document, state, epoch))
                content.append('</th></tr><tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr><tr>')
                if days_stale > 90:
                    content.append('<td class="antiquated" colspan="2">')
                else:
                    content.append('<td class="warning" colspan="2">')

                content.append(self.assemble_warning_statements(fontsize, warning_statements, days_stale))
                content.append('</td></tr>')

                content.append('</thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_variable_kanban_card(self, kanbanboard, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        fontsize = self.get_font_size(member_document)
        content = []
        epoch = datetime.datetime.utcnow()
        blocked_reason = ""
        days_stale = 0
        search_criteria = {"_id": ObjectId(doc_id)}
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            lastchanged = card_document.get('lastchanged', 0)
            lastchangedby = card_document.get('lastchangedby', '')
            doc_id, blocked, blockedhistory, blockeduntil, coowner, coreviewer, deadline, iteration, nextaction = self.get_card_attribute_values(card_document,
                                 ['_id', 'blocked', 'blockedhistory', 'blockeduntil', 'coowner', 'coreviewer', 'deadline', 'iteration', 'nextaction'])
            owner, parent, priority, project, release, resolution, reviewer = self.get_card_attribute_values(card_document,
                                        ['owner', 'parent', 'priority', 'project', 'release', 'resolution', 'reviewer'])
            severity, state, statehistory, stuck, title, type = self.get_card_attribute_values(card_document,
                                                        ['severity', 'state', 'statehistory', 'stuck', 'title', 'type'])
            crmcase, deferred, deferreduntil, expedite = self.get_card_attribute_values(card_document, ['crmcase', 'deferred', 'deferreduntil', 'expedite'])
            start_date, _, scope = self.get_project_release_iteration_dates(project, release, iteration)
            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', [])
            state_metrics = self.get_state_metrics(condensed_column_states, card_document['statehistory'])
            backlog_states = self.get_custom_states_mapped_onto_metastates(['backlog'])
            for backlog_state in backlog_states:
                backlog = state_metrics.get(backlog_state, 0)
                if backlog:
                    break

            match_found = False
            if kanbanboard == 'AffectsVersion' and card_document.get('affectsversion', ''):
                affectsversion = card_document.get('affectsversion', '')
                table_class = 'affectsversion'
                match_found = True
            elif kanbanboard == 'Class Of Service' and card_document.get('classofservice', ''):
                classofservice = card_document.get('classofservice', '')
                table_class = 'classofservice'
                match_found = True
            elif kanbanboard == 'Co-Owner' and card_document.get('coowner', ''):
                coowner = card_document.get('coowner', '')
                table_class = 'coowner'
                match_found = True
            elif kanbanboard == 'Co-Reviewer' and card_document.get('coreviewer', ''):
                coreviewer = card_document.get('coreviewer', '')
                table_class = 'coreviewer'
                match_found = True
            elif kanbanboard == 'Cost' and (card_document.get('estimatedcost', 0) or card_document.get('actualcost', 0)):
                actualcost = Decimal(card_document.get('actualcost', 0))
                estimatedcost = Decimal(card_document.get('estimatedcost', 0))
                table_class = 'cost'
                match_found = True
            elif kanbanboard == 'Creator' and card_document.get('creator', ''):
                creator = card_document.get('creator', '')
                table_class = 'creator'
                match_found = True
            elif kanbanboard == 'CRM Cases' and card_document.get('crmcase', ''):
                crmcase = card_document.get('crmcase', '')
                table_class = 'crmcase'
                match_found = True
            elif kanbanboard == 'Customer' and card_document.get('customer', ''):
                customer = card_document.get('customer', '')
                table_class = 'customer'
                match_found = True
            elif kanbanboard == 'Difficulty' and card_document.get('difficulty', ''):
                difficulty = card_document.get('difficulty', '')
                table_class = 'difficulty'
                match_found = True
            elif kanbanboard == 'Emotions' and card_document.get('emotion', ''):
                emotion = card_document.get('emotion', '')
                table_class = 'emotions'
                match_found = True
            elif kanbanboard == 'Escalation' and card_document.get('escalation', ''):
                escalation = card_document.get('escalation', '')
                table_class = 'escalation'
                match_found = True
            elif kanbanboard == 'Ext Ref' and card_document.get('externalreference', ''):
                externalreference = card_document.get('externalreference', '')
                table_class = 'external_reference'
                match_found = True
            elif kanbanboard == 'FixVersion' and card_document.get('fixversion', ''):
                fixversion = card_document.get('fixversion', '')
                table_class = 'fixversion'
                match_found = True
            elif kanbanboard == 'Hashtags' and card_document.get('hashtags', ''):
                hashtags = card_document.get('hashtags', '')
                table_class = 'hashtags'
                match_found = True
            elif kanbanboard == 'Identifier' and card_document.get('id', ''):
                id = card_document.get('id', '')
                table_class = 'identifier'
                match_found = True
            elif kanbanboard == 'Iteration' and card_document.get('iteration', ''):
                iteration = card_document.get('iteration', '')
                table_class = 'iteration'
                match_found = True
            elif kanbanboard == 'Last Changed' and card_document.get('lastchanged', 0):
                table_class = 'lastchanged'
                match_found = True
            elif kanbanboard == 'Last Touched' and card_document.get('lasttouched', 0):
                lasttouched = card_document.get('lasttouched', 0)
                table_class = 'lasttouched'
                match_found = True
            elif kanbanboard == 'Next Action' and nextaction:
                table_class = 'nextaction'
                match_found = True
            elif kanbanboard == 'Reassignments' and ((card_document.get('owner', '') and card_document.get('reassignowner', '')) or (card_document.get('coowner', '') and card_document.get('reassigncoowner', '')) or (card_document.get('reviewer', '') and card_document.get('reassignreviewer', '')) or (card_document.get('coreviewer', '') and card_document.get('reassigncoreviewer', ''))):
                reassignowner = card_document.get('reassignowner', '')
                reassigncoowner = card_document.get('reassigncoowner', '')
                reassignreviewer = card_document.get('reassignreviewer', '')
                reassigncoreviewer = card_document.get('reassigncoreviewer', '')
                table_class = 'reassignments'
                match_found = True
            elif kanbanboard == 'Recurring' and card_document.get('recurring', ''):
                table_class = 'recurring'
                match_found = True
            elif kanbanboard == 'Release' and release:
                table_class = 'release'
                match_found = True
            elif kanbanboard == 'Resolution' and card_document.get('resolution', ''):
                resolution = card_document.get('resolution', '')
                table_class = 'resolution'
                match_found = True
            elif kanbanboard == 'Scope Creep' and start_date and backlog > start_date:
                table_class = 'scopecreep'
                match_found = True
            elif kanbanboard == 'Severity' and card_document.get('severity', ''):
                table_class = 'severity'
                match_found = True
            elif kanbanboard == 'Status' and card_document.get('status', ''):
                status = card_document.get('status', '')
                table_class = 'status'
                match_found = True
            elif kanbanboard == 'Subteam' and card_document.get('subteam', ''):
                subteam = card_document.get('subteam', '')
                table_class = 'subteam'
                match_found = True
            elif kanbanboard == 'Yesterday':
                lasttouched = card_document.get('lasttouched', datetime.timedelta())
                focushistory = card_document.get('focushistory', [])
                table_class = 'yesterday'
                yesterday_epoch = epoch-self.timedelta_day
                changed_yesterday = False
                if lastchanged and lastchanged.date() == yesterday_epoch.date():
                    changed_yesterday = True

                touched_yesterday = False
                if lasttouched and lasttouched.date() == yesterday_epoch.date():
                    touched_yesterday = True

                focused_yesterday = False
                if focushistory:
                    latest_focushistory_document = focushistory[-1]
                    lastfocused = latest_focushistory_document['focusend']
                    if lastfocused and lastfocused.date() == yesterday_epoch.date():
                        focused_yesterday = True

                if any([changed_yesterday, touched_yesterday, focused_yesterday]):
                    match_found = True

            if match_found:
                if 'minimised' in mode:
                    content += self.create_minimised_card_div(project, state)
                elif 'ghosted' in mode:
                    content += '<div class="ghostedcard">'
                else:
                    content += self.create_card_div(swimlane_no, doc_id, project, state, priority, severity, expedite, blocked, deferred, release)

                table_initialise, blocked_reason = self.assign_card_table_class(table_class, session_document, card_document, mode, state, type, owner, coowner, reviewer, coreviewer, member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                if kanbanboard == 'AffectsVersion':
                    content.append('<sup class="affectsversion">'+affectsversion+'</sup>')
                elif kanbanboard == 'Class Of Service':
                    content.append('<sup class="classofservice">'+classofservice+'</sup>')
                elif kanbanboard == 'Co-Owner':
                    content.append('<sup class="coowner">'+coowner+'</sup>')
                elif kanbanboard == 'Co-Reviewer':
                    content.append('<sup class="coreviewer">'+coreviewer+'</sup>')
                elif kanbanboard == 'Cost':
                    currency = ""
                    project_document = self.projects_collection.find_one({"project": project})
                    if project_document and 'currency' in project_document and project_document['currency']:
                        currency = project_document['currency']

                    if currency:
                        (_, currency_symbol) = self.currencies[currency]
                    else:
                        currency_symbol = ""

                    if estimatedcost and actualcost:
                        cost_to_go = estimatedcost-actualcost
                        content.append(f'<sup class="time">{currency_symbol}{estimatedcost} - {currency_symbol}{actualcost} = {currency_symbol}{cost_to_go}</sup>')
                    elif estimatedcost:
                        content.append(f'<sup class="time">{currency_symbol}{estimatedcost} - ? = ?</sup>')
                    elif actualcost:
                        content.append(f'<sup class="time">? - {currency_symbol}{actualcost} = ?</sup>')

                elif kanbanboard == 'Creator':
                    content.append('<sup class="creator">'+creator+'</sup>')
                elif kanbanboard == 'CRM Cases':
                    content.append(f'<sup class="crmcase">{crmcase}</sup>')
                elif kanbanboard == 'Customer':
                    content.append(f'<sup class="customer">{customer}</sup>')
                elif kanbanboard == 'Difficulty':
                    content.append(f'<sup class="difficulty">{difficulty.capitalize()}</sup>')
                elif kanbanboard == 'Emotions':
                    content.append(self.insert_emotion_icons(emotion))
                elif kanbanboard == 'Escalation':
                    content.append(f'<sup class="escalation">{escalation}</sup>')
                elif kanbanboard == 'Ext Ref':
                    content.append(f'<sup class="externalreference">{externalreference}</sup>')
                elif kanbanboard == 'FixVersion':
                    content.append(f'<sup class="fixversion">{fixversion}</sup>')
                elif kanbanboard == 'Hashtags':
                    for hashtag in hashtags:
                        content.append(f'<sup class="hashtag">{hashtag}</sup>')

                elif kanbanboard == 'Identifier':
                    content.append(f'<sup class="identifier">{id}</sup>')
                elif kanbanboard == 'Iteration':
                    content.append(f'<sup class="iteration">{iteration}</sup>')
                elif kanbanboard == 'Last Changed':
                    lastchangedby = card_document.get('lastchangedby', '')
                    date_format = self.convert_time_to_display_format(lastchanged)
                    content.append(f'<sup class="lastchanged">{lastchangedby} on {date_format}</sup>')
                elif kanbanboard == 'Last Touched':
                    lasttouchedby = card_document.get('lasttouchedby', '')
                    date_format = self.convert_time_to_display_format(lasttouched)
                    content.append(f'<sup class="lasttouched">{lasttouchedby} on {date_format}</sup>')
                elif kanbanboard == 'Next Action':
                    date_format = self.convert_time_to_display_format(nextaction)
                    content.append(f'<sup class="nextactionconfirmation">{date_format}</sup>')
                elif kanbanboard == 'Reassignments':
                    for user, reason in [(owner, reassignowner),
                                         (coowner, reassigncoowner),
                                         (reviewer, reassignreviewer),
                                         (coreviewer, reassigncoreviewer)]:
                        if user and reason:
                            content.append(f'<sup class="reassign">{user} - {reason}</sup>')

                elif kanbanboard == 'Recurring':
                    content.append('<sup class="recurring">Scheduled to be recurring upon closure</sup>')
                elif kanbanboard == 'Release':
                    content.append('<sup class="release">'+release+'</sup>')
                elif kanbanboard == 'Resolution':
                    content.append('<sup class="resolution">'+resolution+'</sup>')
                elif kanbanboard == 'Scope Creep':
                    hours_ago = int((epoch-lastchanged)/self.timedelta_hour)
                    content.append('<span class="yesterday">Card created after commencement of '+scope+'</span>')
                elif kanbanboard == 'Severity':
                    content.append('<sup class="severity">'+severity.capitalize()+'</sup>')

                # TODO - Should there be entry for Status here?
                elif kanbanboard == 'Subteam':
                    content.append('<sup class="subteam">'+subteam+'</sup>')
                elif kanbanboard == 'Yesterday':
                    if focused_yesterday:
                        latest_focushistory_document = focushistory[-1]
                        minutes_spent = int((latest_focushistory_document['focusend'] - latest_focushistory_document['focusstart']) / self.timedelta_minute)
                        hours_ago = int((epoch - latest_focushistory_document['focusend']) / self.timedelta_hour)
                        content.append(f'<sup class="focus">Last Focus by {latest_focushistory_document["focusby"]} for {minutes_spent} minutes {hours_ago} hours ago!</sup>')
                    elif changed_yesterday:
                        hours_ago = int((epoch-lastchanged)/self.timedelta_hour)
                        content.append(f'<span class="yesterday">Last changed by {lastchangedby} {hours_ago} hours ago</span>')
                    elif touched_yesterday:
                        hours_ago = int((epoch-lasttouched)/self.timedelta_hour)
                        content.append(f'<span class="yesterday">Last touched by {lasttouchedby} {hours_ago} hours ago</span>')

                content.append('</th></tr>')
                content.append('<tr><th colspan="2">')
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr>')
                if kanbanboard == 'Cost':
                    content.append('<tr><td colspan="2">')
                    actualcosthistory = card_document.get('actualcosthistory', [])
                    estimatedcosthistory = card_document.get('estimatedcosthistory', [])
                    content.append(self.insert_card_cost_chart(estimatedcost, estimatedcosthistory, actualcost, actualcosthistory))
                    content.append('</td></tr>')
                elif kanbanboard == 'Status':
                    content.append('<tr><td colspan="2">')
                    modified_status = self.format_multiline(status)
                    content.append(f'<p class="{fontsize}left">{modified_status}</p>')
                    content.append('</td></tr>')

                inherited_deadline, inherited_deadline_entity = self.calculate_inherited_deadline(project, release,
                                                                                                  iteration)
                warning_statements, days_stale = self.assemble_card_warning_statements(statehistory, blocked_reason,
                                                                                       blocked, blockedhistory,
                                                                                       blockeduntil, deferred,
                                                                                       deferreduntil, coowner,
                                                                                       coreviewer, deadline,
                                                                                       inherited_deadline,
                                                                                       inherited_deadline_entity,
                                                                                       lastchanged, nextaction, owner,
                                                                                       project, reviewer,
                                                                                       session_document,
                                                                                       someone_else_is_stuck_status,
                                                                                       state, stuck, type, resolution)
                if warning_statements:
                    content.append('<tr>')
                    if days_stale > 90:
                        content.append('<td class="antiquated" colspan="2">')
                    else:
                        content.append('<td class="warning" colspan="2">')

                    content.append(self.assemble_warning_statements(fontsize, warning_statements, days_stale))
                    content.append('</td></tr>')

                content.append('</thead>')
                content.append('<tbody></tbody>')
                content.append('</table>')
                content.append('</div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode, member_document, projection))

        return "".join(content)

    def assemble_velocity_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                      someone_else_is_stuck_status, projection):
        """Displays the velocity of a card moving across the kanban board"""
        content = []
        epoch = datetime.datetime.utcnow()
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        if self.cards_collection.find(search_criteria).count():
            project, release, iteration = self.get_member_project_release_iteration(member_document)
            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', [])
            card_document = self.cards_collection.find_one(search_criteria)
            project = card_document.get('project', '')
            backlog_states = self.get_custom_states_mapped_onto_metastates(['backlog'])
            statehistory = card_document.get('statehistory', [])
            state_metrics = self.get_state_metrics(condensed_column_states, card_document['statehistory'])
            for backlog_state in backlog_states:
                backlog = state_metrics.get(backlog_state, 0)
                if backlog:
                    break            

            if backlog:
                doc_id = card_document["_id"]
                blocked = card_document.get('blocked', '')
                coowner = card_document.get('coowner', '')
                coreviewer = card_document.get('coreviewer', '')
                deferred = card_document.get('deferred', '')
                expedite = card_document.get('expedite', '')
                owner = card_document.get('owner', '')
                parent = card_document.get('parent', '')
                priority = card_document.get('priority', '')
                release = card_document.get('release', '')
                reviewer = card_document.get('reviewer', '')
                severity = card_document.get('severity', '')
                state = card_document.get('state', '')
                title = card_document.get('title', '')
                card_type = card_document.get('type', '')
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('velocity', session_document,
                                                                   card_document, mode, state,
                                                                   card_type, owner, coowner,
                                                                   reviewer, coreviewer,
                                                                   member_document, projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document,
                                                       buttons, 'index'))
                content.append('</th><th>')
                average_velocity = self.calculate_average_velocity()
                days_since_entering_backlog = int((epoch-backlog)/self.timedelta_day)
                if days_since_entering_backlog <= average_velocity:
                    content.append('<img src="/images/hare.png" title="Hare">')
                else:
                    content.append('<img src="/images/tortoise.png" title="Tortoise">')

                content.append('</th><th>')
                content.append(f'<sup class="new">{days_since_entering_backlog} Days Since Entering Backlog!</sup>')
                content.append('</th></tr><tr><th colspan="3">')
                fontsize = self.get_font_size(member_document)
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead><tbody></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    def assemble_votes_kanban_card(self, usertypes, mode, swimlane_no, doc_id,
                                   someone_else_is_stuck_status, projection):
        """ Comment """
        content = []
        username = self.check_authentication('/kanban')
        search_criteria = {"_id": ObjectId(doc_id)}
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        if self.cards_collection.find(search_criteria).count():
            card_document = self.cards_collection.find_one(search_criteria)
            state = card_document.get('state', '')
            #TODO - What about custom states mapped onto backlog?
            if state == 'backlog':
                doc_id = card_document["_id"]
                blocked = card_document.get('blocked', '')
                category = card_document.get('category', '')
                coowner = card_document.get('coowner', '')
                coreviewer = card_document.get('coreviewer', '')
                deferred = card_document.get('deferred', '')
                expedite = card_document.get('expedite', '')
                owner = card_document.get('owner', '')
                parent = card_document.get('parent', '')
                priority = card_document.get('priority', '')
                project = card_document.get('project', '')
                release = card_document.get('release', '')
                reviewer = card_document.get('reviewer', '')
                severity = card_document.get('severity', '')
                title = card_document.get('title', '')
                type = card_document.get('type', '')
                votes = card_document.get('votes', '')
                if 'minimised' in mode:
                    content.append(self.create_minimised_card_div(project, state))
                elif 'ghosted' in mode:
                    content.append('<div class="ghostedcard">')
                else:
                    content.append(self.create_card_div(swimlane_no, doc_id, project, state,
                                                        priority, severity, expedite, blocked,
                                                        deferred, release))

                table_initialise, _ = self.assign_card_table_class('new', session_document,
                                                                   card_document, mode, state, type,
                                                                   owner, coowner, reviewer,
                                                                   coreviewer, member_document,
                                                                   projection)
                content.append(table_initialise)
                content.append('<thead><tr><th>')
                buttons = self.ascertain_card_menu_items(card_document, member_document)
                content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
                content.append('</th><th>')
                content.append(self.show_category_in_top_right(project, category))
                if votes:
                    if len(votes) == 1:
                        content.append(f'<sup class="votes">{len(votes)} Vote</sup>')
                    else:
                        content.append(f'<sup class="votes">{len(votes)} Votes</sup>')

                content.append('</th></tr><tr><th colspan="2">')
                fontsize = self.get_font_size(member_document)
                content.append(self.insert_card_title(session_document, title, parent, fontsize))
                content.append('</th></tr></thead>')
                content.append('<tbody><tr><td>')
                if username not in votes:
                    content.append(f'<form action="/kanban/vote" method="post"><input type="hidden" name="doc_id" value="{doc_id}"><input type="submit" value="Vote"></form>')

                content.append('</td></tr></tbody></table></div>')
            else:
                content.append(self.assemble_placeholder_card(swimlane_no, card_document, mode,
                                                              member_document, projection))

        return "".join(content)

    @staticmethod
    def assemble_warning_statements(assigned_class, warning_statements, days_stale):
        """Assembles a card's warning statement block"""
        content = []
        content.append('<table class="warning"><tr><td>')
        if days_stale > 14:
            content.append('<img src="/images/banana.png"/> ')
        else:
            content.append('<img src="/images/yellowpin32.png"/> ')

        content.append('</td><td>')
        if assigned_class:
            content.append('<p class="'+assigned_class+'">')
        else:
            content.append('</p>')

        content.append(' | '.join(warning_statements))
        content.append('</p></td></tr></table>')
        return "".join(content)

    def assign_card_table_class(self, kanbanboard, session_document, card_document, mode, state,
                                type, owner, coowner, reviewer, coreviewer, member_document,
                                projection):
        """Ascertain the CSS class for a given card"""
        blocked_reason = ""
        selected_user = ""
        if member_document:
            selected_user = member_document.get('teammember', '')

        project_document = self.projects_collection.find_one({'project': card_document['project']})
        workflow_index = project_document.get('workflow_index', {})
        buffer_column_states = workflow_index.get('buffer_column_states', [])
        metastate = self.get_corresponding_metastate(project_document, state)
        if metastate == 'closed':
            content = '<table class="'+type+'closed">'
            return content, blocked_reason

        if self.card_waiting_for_other_owner_or_reviewer(card_document, projection):
            content = '<table class="'+type+'waiting">'
            return content, blocked_reason

        if self.card_deferred(card_document, projection):
            content = '<table class="'+type+'deferred">'
            return content, blocked_reason

        if 'ghosted' in mode:
            content = '<table class="'+type+'ghosted">'
            return content, blocked_reason

        if state in buffer_column_states:
            content = '<table class="'+type+'buffered">'
            return content, blocked_reason

        if metastate == "testing" and ((owner and owner == selected_user) or (coowner and coowner == selected_user)):
            content = '<table class="'+type+'ghosted">'
            return content, blocked_reason

        if metastate != "testing" and ((reviewer and reviewer == selected_user) or
                                     (coreviewer and coreviewer == selected_user)):
            content = '<table class="'+type+'ghosted">'
            return content, blocked_reason

        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:
                        content = '<table class="'+type+'blocked">'
                        return content, blocked_reason

                else:
                    content = '<table class="'+type+'blocked">'
                    return content, blocked_reason

            else:
                content = '<table class="'+type+'blocked">'
                return content, blocked_reason

        if self.card_blocked_by_child(card_document):
            content = '<table class="'+type+'blocked">'
            blocked_reason = 'This '+type+' is blocked by one of its children'
            return content, blocked_reason

        if (self.card_blocked_by_before_card(card_document) or
                self.card_blocked_by_after_card(card_document)):
            content = '<table class="'+type+'blocked">'
            blocked_reason = f'This {type} is blocked by another card which must be completed first'
            return content, blocked_reason

        if kanbanboard in ['attributes', 'internals']:
            content = '<table class="view'+type+'">'
            return content, blocked_reason

        if 'focusby' in card_document and card_document['focusby']:
            content = '<table class="'+type+' focus">'
        else:
            content = '<table class="'+type+'">'

        return content, blocked_reason

    def calculate_average_velocity(self):
        """Calculates the average velocity of all closed cards currently displayed that began
           in backlog
        """
        epoch = datetime.datetime.utcnow()
        velocities_total = []
        average_velocity = 0
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project, release, iteration = self.get_member_project_release_iteration(member_document)
        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', [])
        states = self.get_custom_states_mapped_onto_metastates(['closed'])
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
        backlog_states = self.get_custom_states_mapped_onto_metastates(['backlog'])
        for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
            state_metrics = self.get_state_metrics(condensed_column_states, card_document['statehistory'])
            for backlog_state in backlog_states:
                backlog = state_metrics.get(backlog_state, 0)
                if backlog:
                    break

            if backlog:
                velocities_total.append((epoch-backlog)/self.timedelta_day)

        if velocities_total:
            average_velocity = int(statistics.mean(velocities_total))

        return average_velocity

    def calculate_child_state_range(self, uncondensed_column_states, condensed_column_states_dict,
                                    states_in_use):
        """Ascertain the lowest and highest states in use by a card's children"""
        lowest_state = len(uncondensed_column_states)
        highest_state = -1
        for state_in_use in states_in_use:
            pos = condensed_column_states_dict[state_in_use]
            if pos < lowest_state:
                lowest_state = pos
            elif pos > highest_state:
                highest_state = pos

        return lowest_state, highest_state

    def calculate_inherited_deadline(self, project, release, iteration):
        """Obtain a card's deadline, inherited from as many of project, release and iteration as possible"""
        inherited_deadline = 0
        inherited_deadline_entity = ""
        if all([project, release, iteration]):
            for project_document in self.projects_collection.find({"project": project,
                                                                   "releases.release": release,
                                                                   "releases.iterations.iteration": iteration
                                                                  }):
                for release_document in project_document['releases']:
                    if release_document['release'] == release:
                        for iteration_document in release_document['iterations']:
                            if iteration_document['iteration'] == iteration:
                                if 'end_date' in iteration_document and iteration_document['end_date']:
                                    inherited_deadline = iteration_document['end_date']
                                    inherited_deadline_entity = "iteration"

                break

        elif project and release:
            for project_document in self.projects_collection.find({"project": project,
                                                                   "releases.release": release}):
                for release_document in project_document['releases']:
                    if release_document['release'] == release:
                        if 'end_date' in release_document and release_document['end_date']:
                            inherited_deadline = release_document['end_date']
                            inherited_deadline_entity = "release"

                break

        elif project:
            for project_document in self.projects_collection.find({"project": project}):
                if project_document and 'end_date' in project_document and project_document['end_date']:
                    inherited_deadline = project_document['end_date']
                    inherited_deadline_entity = "project"

                break

        return inherited_deadline, inherited_deadline_entity

    def calculate_recidivism(self, project, statehistory):
        """Calculate the forward and backward recidivism rates for a given card from its statehistory"""
        forward_count = 0
        backward_count = 0
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        buffer_column_states = workflow_index.get('buffer_column_states', [])
        condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
        for i in range(len(statehistory)-1):
            old_state = statehistory[i]['state']
            if old_state in condensed_column_states_dict:
                old_pos = condensed_column_states_dict[old_state]
                next_state = statehistory[i+1]['state']
                if next_state in condensed_column_states_dict:
                    next_pos = condensed_column_states_dict[next_state]
                    if (old_state not in buffer_column_states) or (next_state not in buffer_column_states):
                        if next_pos > old_pos:
                            forward_count += 1
                        elif next_pos < old_pos:
                            backward_count += 1

        return forward_count, backward_count
        
    def calculate_routine_card_next_action(self, epoch, interval):
        (year, month, day) = epoch.timetuple()[:3]
        next_action = epoch + self.timedelta_day
        day_month_pattern = re.compile('^(\d+)(?:st|nd|rd|th) Day of Month$')
        if interval == 'Daily':
            return next_action
        elif interval == 'Weekdays Only':
            while next_action.isoweekday() > 5:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Weekend Days Only':
            while next_action.isoweekday() < 6:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Twice Weekly':
            next_action += (self.timedelta_day * (7/2))
            return next_action
        elif interval == 'Weekly':
            return epoch + self.timedelta_week
        elif interval == 'Every Monday':
            while next_action.isoweekday() != 1:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Every Tuesday':
            while next_action.isoweekday() != 2:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Every Wednesday':
            while next_action.isoweekday() != 3:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Every Thursday':
            while next_action.isoweekday() != 4:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Every Friday':
            while next_action.isoweekday() != 5:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Every Saturday':
            while next_action.isoweekday() != 6:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Every Sunday':
            while next_action.isoweekday() != 7:
                next_action += self.timedelta_day

            return next_action
        elif interval == 'Twice Monthly':
            return epoch + (self.timedelta_day * 15)
        elif interval == 'Monthly':
            if day <= 28:
                if month < 12:
                    return epoch.replace(month=month+1)
                else:
                    return epoch.replace(year=year+1, month=1)
        
            else:
                return epoch + (self.timedelta_week * 4)

        elif interval == '1st Day of Month':
            if month < 12:
                return epoch.replace(month=month+1, day=1)
            else:
                return epoch.replace(year=year+1, month=1, day=1)

        elif re.search('^(\d+)(?:st|nd|rd|th) Day of Month$', interval):
            results = day_month_pattern.find(interval)
            day_of_action = results[0]
            if day < day_of_action:
                return epoch.replace(day=day_of_action)
            else:
                if month < 12:
                    return epoch.replace(month=month+1, day=day_of_action)
                else:
                    return epoch.replace(year=year+1, month=1, day=day_of_action)

        elif interval == 'First Monday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 1:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 1 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'First Tuesday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 2:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 2 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'First Wednesday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 3:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 3 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'First Thursday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 4:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 4 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'First Friday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 5:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 5 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'First Saturday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 6:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 6 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'First Sunday in Month':
            if day+1 < 8:
                for potential_day in range(day+1, 8):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 7:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 7 and 1 <= day <= 7):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Monday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 1:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 1 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Tuesday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 2:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 2 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Wednesday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 3:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 3 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Thursday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 4:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 4 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Friday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 5:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 5 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Saturday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 6:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 6 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Second Sunday in Month':
            if 8 <= day+1 <= 14:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 7:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 7 and 8 <= day <= 14):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Monday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 1:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 1 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Tuesday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 2:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 2 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Wednesday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 3:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 3 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Thursday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 4:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 4 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Friday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 5:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 5 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Saturday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 6:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 6 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Third Sunday in Month':
            if 15 <= day+1 <= 21:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 7:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 7 and 15 <= day <= 21):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Monday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 1:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 1 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Tuesday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 2:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 2 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Wednesday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 3:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 3 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Thursday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 4:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 4 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Friday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 5:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 5 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Saturday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 6:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 6 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Fourth Sunday in Month':
            if 22 <= day+1 <= 28:
                for potential_day in range(day+1, 15):
                    next_action.replace(day=potential_day)
                    if next_action.isoweekday() == 7:
                        return next_action

            else:
                if month < 12:
                    return next_action.replace(month=month+1, day=1)
                else:
                    return next_action.replace(year=year+1, month=1, day=1)          
        
            while not(next_action.isoweekday() == 7 and 22 <= day <= 28):
                next_action += self.timedelta_day
                (year, month, day) = next_action.timetuple()[:3]
                
            return next_action
        elif interval == 'Quarterly':
            if day <= 28:
                if month <= 9:
                    return epoch.replace(month=month+3)
                else:
                    new_month = abs(9 - month)
                    return epoch.replace(year=year+1, month=new_month)        
            
            else:
                return epoch + (self.timedelta_day * (365 / 4))

        elif interval == 'Half Yearly':
            if day <= 28:
                if month <= 6:
                    return epoch.replace(month=month+6)
                else:
                    new_month = abs(6 - month)
                    return epoch.replace(year=year+1, month=new_month)        
            
            else:
                return epoch + (self.timedelta_day * (365 / 2))

        elif interval == 'Yearly':
            if day <= 28:
                return next_action.replace(year=year+1)
            else:
                return epoch + (self.timedelta_day * 365)

    def card_blocked_by_after_card(self, card_document):
        """Returns blocked status if card's state is greater than that of another it must be completed after"""
        blocked_by_after_card = False
        if 'after' in card_document and card_document['after']:
            project_document = self.projects_collection.find_one({'project': card_document['project']})
            workflow_index = project_document.get('workflow_index', {})
            condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
            state_position = condensed_column_states_dict[card_document['state']]
            for after_card_document in self.cards_collection.find({'id': card_document['after']}):
                after_state_position = condensed_column_states_dict[after_card_document['state']]
                if state_position > after_state_position:
                    blocked_by_after_card = True

                break

        return blocked_by_after_card

    def card_blocked_by_before_card(self, before_card_document):
        """Returns blocked status if card's state is greater than another card which must be completed first"""
        blocked_by_other_card = False
        project_document = self.projects_collection.find_one({'project': before_card_document['project']})
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
        if before_card_document['state'] in condensed_column_states_dict:
            before_state_position = condensed_column_states_dict[before_card_document['state']]
            for other_card_document in self.cards_collection.find({'before': before_card_document['id']}):
                if other_card_document['state'] in condensed_column_states_dict:
                    other_state_position = condensed_column_states_dict[other_card_document['state']]
                    if other_state_position < before_state_position:
                        blocked_by_other_card = True
                        break

        return blocked_by_other_card

    def card_blocked_by_child(self, card_document):
        """Returns True if a card is blocked by one or more of its children at the same or a lower state"""
        blocked_by_child = False
        project_document = self.projects_collection.find_one({'project': card_document['project']})
        workflow_index = project_document.get('workflow_index', {})
        uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
        condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
        child_states = self.cards_collection.find({"parent": card_document['id'],
                                                  "blocksparent": True}).distinct('state')
        lowest_child_state, _ = self.calculate_child_state_range(uncondensed_column_states,
                                                                 condensed_column_states_dict,
                                                                 child_states)
        state_position = -1
        if card_document['state']:
            if card_document['state'] in condensed_column_states_dict:
                state_position = condensed_column_states_dict[card_document['state']]

        if lowest_child_state <= state_position:
            blocked_by_child = True

        return blocked_by_child

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

                else:
                    return True

            else:
                return True

        return False

    def card_search_criteria_met_for_metric(self, specific_card_search_criteria_entries):
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        _, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                 required_states)
        for (attribute, value) in specific_card_search_criteria_entries:
            owner_reviewer_search_criteria[attribute] = value

        if self.cards_collection.find(owner_reviewer_search_criteria).count():
            return True

        return False

    def card_to_bypass_review(self, doc_id):
        """Returns true if a card is set to bypass the testing phase"""
        # TODO - THIS FUNCTION DOES NOT APPEAR TO BE EVER CALLED
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            if 'bypassreview' in card_document and card_document['bypassreview']:
                return True
            else:
                return False

        return False

    def card_waiting_for_other_owner_or_reviewer(self, 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)
        teammember = member_document.get('teammember', '')
        if teammember:
            state = card_document['state']
            _, _, centric, _, _ = self.get_associated_state_information(state)
            if centric == 'Owner':
                owner = card_document.get('owner', '')
                coowner = card_document.get('coowner', '')
                if owner and coowner:
                    ownerstate = card_document.get('ownerstate', '')
                    coownerstate = card_document.get('coownerstate', '')
                    if owner == teammember and ownerstate:
                        return True
                    elif coowner == teammember and coownerstate:
                        return True

            elif centric == 'Reviewer':
                reviewer = card_document.get('reviewer', '')
                coreviewer = card_document.get('coreviewer', '')
                if reviewer and coreviewer:
                    reviewerstate = card_document.get('reviewerstate', '')
                    coreviewerstate = card_document.get('coreviewerstate', '')
                    if reviewer == teammember and reviewerstate:
                        return True
                    elif coreviewer == teammember and coreviewerstate:
                        return True

        return False

    @staticmethod
    def check_authentication(returnurl):
        """Checks that a user is logged on"""
        username = cherrypy.session.get('username', None)
        if not username:
            raise cherrypy.HTTPRedirect("/authenticate?returnurl="+returnurl)

        return username

    @cherrypy.expose
    def component_recidivism_rate(self):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}/recidivism_rate')
        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)
        epoch = datetime.datetime.utcnow()
        project, release, iteration = self.get_member_project_release_iteration(member_document)
        _, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                 required_states)
        recidivism_rates = {}
        for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
            if 'statehistory' in card_document:
                _, backwardcount = self.calculate_recidivism(card_document['project'],
                                                                        card_document['statehistory']
                                                                       )
                if backwardcount in recidivism_rates:
                    recidivism_rates[backwardcount] += 1
                else:
                    recidivism_rates[backwardcount] = 1

        pie_chart = pygal.Pie(style=LightStyle)
        pie_chart.title = self.assemble_chart_title('Recidivism Rate', project, release, iteration, [])
        number_of_documents = 0
        for value in recidivism_rates.values():
            number_of_documents += value

        for key, value in recidivism_rates.items():
            percentage = round((value / number_of_documents) * 100)
            pie_chart.add(str(key), [{'value': value,
                                      'tooltip': str(key)+' : '+str(value)+' : '+str(percentage)+'%'}
                                    ])

        pie_chart.render_to_file(os.path.join(self.current_dir, '..', 'svgcharts', username+'_'+str(int(epoch.timestamp()))+'.svg'))
        content = []
        content.append('<figure>')
        content.append(f'<embed type="image/svg+xml" src="/svgcharts/{username}_{int(epoch.timestamp())}.svg" />')
        content.append('</figure>')
        return "".join(content)

    def convert_time_to_chart_display_format(self, datetime_to_display, number_of_days, division):
        """Converts a time integer to '<month> yyyy' or 'yyyy-mm-dd' format for use in charts"""
        date_format = ""
        year = datetime_to_display.year
        month = datetime_to_display.month
        day = datetime_to_display.day
        if division in [28, 31]:
            textual_month = self.months[month-1]
            date_format = textual_month + ' ' + str(year)
        else:
            date_format = "%4d-%02d-%02d" % (year, month, day)

        return date_format

    @staticmethod
    def convert_time_to_display_format(date):
        """Converts a datetime object to yyyy-mm-dd format"""
        return "%4d-%02d-%02d" % (date.year, date.month, date.day)

    def cookie_handling(self):
        session_id = ""
        cookie = cookies.SimpleCookie()
        member_document = {}
        cookie.load(cherrypy.request.headers['Cookie'])
        session_id = cookie['session_id'].value
        username = cherrypy.session.get('username',  None)
        if not self.sessions_collection.find({"session_id": session_id}).count():
            session_document = {"session_id": session_id, 'lastaccess': datetime.datetime.utcnow(),
                                'recent_cards': [],
                                "username":   username,
                                "firstname":  cherrypy.session.get('firstname', None),
                                "lastname":   cherrypy.session.get('lastname',  None),
                                "ip_address": cherrypy.request.remote.ip,
                                "user_agent": cherrypy.request.headers['User-Agent']}

            if session_document["firstname"] and session_document["lastname"]:
                session_document["fullname"] = session_document["firstname"] + ' ' + session_document["lastname"]
                session_document["displayname"] = session_document["firstname"] + ' ' + session_document["lastname"]
            else:
                session_document["fullname"] = None
                session_document["displayname"] = None

            self.sessions_collection.insert(session_document)

        session_document = self.sessions_collection.find_one({"session_id": session_id})
        if session_document:
            if username and ('username' not in session_document or username != session_document['username']):
                session_document["username"] = username
                self.sessions_collection.save(session_document)

            if self.members_collection.find({"username": session_document["username"]}).count():
                member_document = self.members_collection.find_one({"username": session_document["username"]})
                save_required = False
                for attribute in ['firstname', 'lastname', 'fullname']:
                    if attribute not in member_document:
                        member_document[attribute] = session_document[attribute]
                        save_required = True

                if save_required:
                    self.members_collection.save(member_document)
                    self.save_member_as_json(member_document)

            else:
                member_document = {}
                for attribute in ['username', 'firstname', 'lastname', 'fullname', 'displayname']:
                    member_document[attribute] = session_document[attribute]

                for attribute in ['category', 'columns', 'iteration', 'kanbanboard', 'project',
                                  'release', 'type', 'teammember']:
                    member_document[attribute] = ""

                member_document['project_wips'] = {}
                member_document['projects'] = []
                for project_document in self.projects_collection.find():
                    if ('members' in project_document and project_document['members'] and
                            session_document["username"] in project_document['members']):
                        member_document['projects'].append(project_document['project'])

                self.members_collection.insert(member_document)
                self.save_member_as_json(member_document)

        return session_id

    def count_cards_omitted_by_filter_setting(self, owner_reviewer_search_criteria, attribute,
                                              value):
        content = ""
        with_count = self.cards_collection.find(owner_reviewer_search_criteria).count()
        del owner_reviewer_search_criteria[attribute]
        without_count = self.cards_collection.find(owner_reviewer_search_criteria).count()
        cards_omitted = without_count-with_count
        if cards_omitted > 0:
            return ' <sup title="This setting is causing '+str(cards_omitted)+' cards to be omitted!">'+str(cards_omitted)+'</sup>'

        return content

    def create_card_div(self, swimlane_no, doc_id, project, state, priority, severity, expedite,
                        blocked, deferred, release):
        special = ""
        if expedite:
            special = 'expedite'
        elif blocked:
            special = 'blocked'
        elif deferred:
            special = 'deferred'

        if release:
            release_no = self.get_roadmap_release_number(project, release)
        else:
            release_no = -1

        step_no, step_role, _, _, _ = self.get_associated_state_information(state)
        return f'<div class="card" id="{swimlane_no}###{doc_id}###{step_no}###{step_role}###{state}###{priority}###{severity}###{special}###{release_no}">'
        
    def create_html_select_block(self, name, potential_values, classes=None, id=None, onchange=None, default=None, current_value=None, specials=[]):
        content = []
        content.append('<select')
        if classes:
            content.append(f' class="{classes}"')
        
        if id:
            content.append(f' id="{id}"')
        
        content.append(f' name="{name}"')
        if onchange:
            content.append(f' onchange="{onchange}"')        
        
        content.append('>')
        if default != None:
            # Output this option if default is empty string or has a value
            content.append(f'<option value="">{default}</option>')

        for potential_value in potential_values:
            content.append(f'<option value="{potential_value}"')
            if potential_value == current_value:
                content.append(' selected')

            if 'capitalise' in specials:
                content.append(f'>{potential_value.capitalize()}</option>')
            else:
                content.append(f'>{potential_value}</option>')

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

    def create_minimised_card_div(self, project, state):
        project_document = self.projects_collection.find_one({'project': project})
        metastate = self.get_corresponding_metastate(project_document, state)
        if metastate == 'closed':
            return '<div class="minimisedcardclosed">'
        elif metastate in self.metastates_buffer_list:
            return '<div class="minimisedcardbuffered">'
        else:
            return '<div class="minimisedcard">'

    def cumulative_flow_diagram_chart(self, number_of_days="", division="", rawdatarequired="",
                                      csvrequired=""):
        Kanbanara.check_authentication(f'/{self.component}/cfd')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        number_of_days, division = self.metrics_settings(session_document, number_of_days, division)
        member_document = Kanbanara.get_member_document(self, session_document)
        project, release, iteration = self.get_member_project_release_iteration(member_document)
        content = []
        content.append(self.insert_page_title_and_online_help(session_document,
                                                              'cumulative_flow_diagram_chart',
                                                              'Cumulative Flow Diagram'))
        content.append(self.assemble_chart_buttons('cfd', number_of_days, division, project,
                                                   release, iteration))
        epoch = datetime.datetime.utcnow()
        statistics = []
        width = 600
        height = 260
        content.append(f'<svg width="{width}" height="{height}">')
        content.append('<line x1="30" y1="0" x2="30" y2="260" style="stroke:gray;stroke-width:2"/>')
        content.append('<line x1="0" y1="188" x2="600" y2="188" style="stroke:gray;stroke-width:2"/>')
        day_count = 0
        assigned_colours = {}
        chartstart_epoch = epoch - (self.timedelta_day * number_of_days)
        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 rawdatarequired:
            table = '<table class="unsortable"><tr><th>Date</th>'
            for state in condensed_column_states:
                table += '<th>'+state.capitalize()+'</th>'

            table += '</tr>'

        while day_count < number_of_days:
            day_count += division
            past_epoch = chartstart_epoch + (self.timedelta_day * day_count)
            search_criteria = {}
            if project:
                search_criteria['project'] = project

            if release:
                search_criteria['release'] = release

            if iteration:
                search_criteria['iteration'] = iteration

            day_statistics = [0] * len(condensed_column_states)
            for card_document in self.cards_collection.find(search_criteria):
                state_metrics = self.get_state_metrics(condensed_column_states, card_document['statehistory'])
                for state_no, state in enumerate(reversed(condensed_column_states)):
                    value = state_metrics.get(state, 0)
                    if value and value <= past_epoch:
                        day_statistics[state_no] += 1
                        break
         
            statistics.append(day_statistics[::-1]) # Reverses a copy of day_statistics
            date_format = self.convert_time_to_display_format(past_epoch)
            x_origin = 30 + (day_count * 20)
            y_origin = 250
            content.append(f'<text x="{x_origin}" y="{y_origin}" transform="rotate(-90 {x_origin},{y_origin})" style="font-family: Arial, sans-serif; font-size: 8pt; stroke: black; stroke-width: 1; kerning: auto;">{date_format}</text>')
            if rawdatarequired:
                table += f'<tr><td>{date_format}</td>'
                for day_statistic in reversed(day_statistics):
                    table += f'<td>{day_statistic}</td>'
                
                table += '</tr>'

        if rawdatarequired:
            table += '</table>'

        max_height = 0
        for day_statistics in statistics:
            day_height = 0
            for stateCount in day_statistics:
                day_height += stateCount

            if day_height > max_height:
                max_height = day_height

        card_height = 1
        if (max_height * card_height) < 180:
            potential_card_height = 1
            while (max_height * potential_card_height) < 180:
                potential_card_height += 1

            card_height = potential_card_height-1
        elif max_height > 180:
            potential_card_height = 1
            while (max_height * potential_card_height) > 180:
                potential_card_height -= 0.1

            card_height = potential_card_height

        for scale in range(0, max_height, 5):
            content.append('<text x="10" y="'+str(180-(scale*card_height))+'" style="font-family: Arial, sans-serif; font-size: 8pt; stroke: black; stroke-width: 1; kerning: auto;">'+str(scale)+'</text>')

        column_height = [180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180,
                         180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180]
        for state_no, state in reversed(list(enumerate(condensed_column_states))):
            coordinates = ""
            for day_count, day_statistics in enumerate(statistics):
                if day_count == 0:
                    coordinates += 'M'+str(45+(day_count*20))+','+str(column_height[day_count])+' '
                else:
                    coordinates += 'L'+str(45+day_count*20)+','+str(column_height[day_count])+' '

            for day_count, day_statistics in reversed(list(enumerate(statistics))):
            
                state_day_statistics = day_statistics[state_no]
                day_column_height = column_height[day_count]
            
                coordinates += f'L{45+day_count*20},{day_column_height-(state_day_statistics * card_height)} '
                column_height[day_count] -= (day_statistics[state_no] * card_height)

            if condensed_column_states[state_no] in assigned_colours:
                assigned_colour = assigned_colours[condensed_column_states[state_no]]
            else:
                assigned_colour = self.colours[random.randint(0, len(self.colours)-1)]
                assigned_colours[condensed_column_states[state_no]] = assigned_colour

            content.append(f'<path d="{coordinates}" style="fill:{assigned_colour}; fill-opacity:0.5; stroke:{assigned_colour}; stroke-width:1" title="{condensed_column_states[state_no]}"/>')

        content.append(f'<rect x="0" y="0" width="{width}" height="{height}" style="fill:none; stroke:gray; stroke-width:1;"/>')
        content.append('</svg>')

        if rawdatarequired:
            content.append(table)

        if csvrequired:
            content.append('<p>CSV Output to be completed!</p>')

        return "".join(content)

    @staticmethod
    def dashed_date_to_datetime_object(dashed_date):
        """Converts a dashed date (e.g. 2000-01-01) to its corresponding datetime object"""
        epoch = datetime.timedelta()
        components = dashed_date.split('-')
        if components:
            epoch = datetime.datetime(year=int(components[0]), month=int(components[1]), day=int(components[2]))
        return epoch

    def delete_document_and_descendents(self, username, doc_id):
        """Allows a document and its descendents for a project you are a member of to be deleted"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = self.get_member_document(session_document)
        if member_document and 'projects' in member_document and member_document["projects"]:
            for card_document in self.cards_collection.find({'_id': ObjectId(doc_id),
                                                             'project': {'$exists': True}
                                                            }):
                if self.project_in_projects(card_document['project'], member_document["projects"]):
                    for child_card_document in self.cards_collection.find({'parent': card_document['id']}):
                        self.delete_document_and_descendents(username, child_card_document['_id'])

                    card_document['deleted'] = True
                    self.save_card_as_json(card_document)
                    self.deletions_collection.insert(card_document)
                    self.cards_collection.remove({'_id': ObjectId(doc_id)})
                    self.add_recent_activity_entry((datetime.datetime.utcnow(), username,
                                                    doc_id, 'deleted'))
                    break

    def dictionary_as_json(self, mode, dictionary, indent):
        """comment"""
        # TODO - Use json library function to do this. Do I need this function anymore?
        if '_id' in dictionary:
            del dictionary['_id']

        content = []
        indent_string = ""
        for _ in range(indent):
            if mode == 'html':
                indent_string += '&nbsp;'
            elif mode == 'file':
                indent_string += ' '

        if mode == 'html':
            content.append(indent_string + '{<br>')
        elif mode == 'file':
            content.append(indent_string + '{\n')

        number_of_keys = len(dictionary.keys())
        key_count = 0
        for key, value in dictionary.items():
            if key != '_id':
                key_count += 1
                content.append(self.key_value_pair_as_json(mode, number_of_keys, key_count, key,
                                                           value, indent+2))

        if mode == 'html':
            content.append(indent_string + '}')
        elif mode == 'file':
            content.append(indent_string + '}\n')

        return "".join(content)

    def displayable_key(self, key):
        """Returns the displayable version of a card attribute's name"""
        if key in self.attribute_display_names:
            return self.attribute_display_names[key]
        else:
            return key.capitalize()

    @staticmethod
    def display_chart(username, epoch):
        """Comment"""
        content = []
        content.append('<table class="sidebyside"><tr><td width="20%"></td><td width="60%">')
        content.append('<figure>')
        content.append(f'<embed type="image/svg+xml" src="/svgcharts/{username}_{int(epoch.timestamp())}.svg" />')
        content.append('</figure>')
        content.append('</td><td width="20%"></td></tr></table>')
        return "".join(content)

    def filter_bar(self, destination):
        """Assemble the filter bar"""
        content = []
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        _, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        selected_filter_bar_components = member_document.get('filterbarcomponents', {})
        selectable_projects = self.get_member_projects(member_document)
        content.append('<h1 id="filter">')
        content.append('<table class="filter"><thead><tr>')
        for (attribute, value) in [('kanbanboard', ''), ('swimlanes', ''), ('classofservice', ''),
                                   ('type', ''), ('columns', ''), ('teammember', ''),
                                   ('project', project), ('subteam', ''),
                                   ('release', ''), ('iteration', ''),
                                   ('severity', ''), ('priority', ''),
                                   ('card', ''), ('category', ''),
                                   ('hashtag', ''), ('customer', ''),
                                   ('fontsize', '')]:
            if attribute and attribute in self.filter_bar_component_statistics:
                (heading, status) = self.filter_bar_component_statistics[attribute]
            else:
                heading = ""
                status = ""

            if (not attribute) or (not status or status == 'static') or (attribute not in selected_filter_bar_components) or (attribute in selected_filter_bar_components and selected_filter_bar_components[attribute]):
                content.append('<th>'+heading)
                if project and attribute not in ['project']:
                    if attribute and attribute in owner_reviewer_search_criteria and owner_reviewer_search_criteria[attribute]:
                        content.append(self.count_cards_omitted_by_filter_setting(owner_reviewer_search_criteria, attribute, value))

                content.append('</th>')

        content.append('</tr></thead>')
        content.append('<tbody><tr>')
        content.append('<form class="filter" action="/filters/submit_filter" method="post">')
        content.append(f'<input type="hidden" name="destination" value="{destination}">')

        content.append('<td>')
        # Kanban Boards
        selectablekanbanboards = self.kanbanboards
        content.append('<select id="filterbarkanbanboard" class="filter" name="kanbanboard" onchange="this.form.submit()">')
        selectedkanbanboard = ""
        if member_document and 'kanbanboard' in member_document and member_document["kanbanboard"]:
            selectedkanbanboard = member_document["kanbanboard"]

        for optgroup_label in ['full', 'placeholder']:
            content.append('<optgroup label="'+optgroup_label.capitalize()+'">')
            for (selectablekanbanboard, full_or_placeholder, selectable_kanbanboard_title) in selectablekanbanboards:
                if full_or_placeholder == optgroup_label:
                    if selectablekanbanboard == selectedkanbanboard:
                        if selectable_kanbanboard_title:
                            content.append('<option value="'+selectablekanbanboard+'" title="'+selectable_kanbanboard_title+'" selected>'+selectablekanbanboard+'</option>')
                        else:
                            content.append('<option value="'+selectablekanbanboard+'" selected>'+selectablekanbanboard+'</option>')

                    else:
                        if selectable_kanbanboard_title:
                            content.append('<option value="'+selectablekanbanboard+'" title="'+selectable_kanbanboard_title+'">'+selectablekanbanboard+'</option>')
                        else:
                            content.append('<option value="'+selectablekanbanboard+'">'+selectablekanbanboard+'</option>')

            content.append('</optgroup>')

        content.append('</select>')
        content.append('</td>')

        # Swim Lanes
        if 'swimlanes' not in selected_filter_bar_components or ('swimlanes' in selected_filter_bar_components and selected_filter_bar_components['swimlanes']):
            content.append('<td>')
            content.append('<select id="filterbarswimlanes" class="filter" name="swimlanes" onchange="this.form.submit()">')
            content.append('<option value="">None</option>')
            if member_document and "swimlanes" in member_document:
                for (selectable_swim_lanes_displayname, selectable_swim_lanes_attribute) in self.swim_lanes_attributes:
                    if selectable_swim_lanes_attribute == member_document["swimlanes"]:
                        content.append(f'<option value="{selectable_swim_lanes_attribute}" selected>{selectable_swim_lanes_displayname}</option>')
                    else:
                        content.append(f'<option value="{selectable_swim_lanes_attribute}">{selectable_swim_lanes_displayname}</option>')

            else:
                for (selectable_swim_lanes_displayname, selectable_swim_lanes_attribute) in self.swim_lanes_attributes:
                    content.append('<option value="'+selectable_swim_lanes_attribute+'">'+selectable_swim_lanes_displayname+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Class Of Service
        if 'classofservice' not in selected_filter_bar_components or ('classofservice' in selected_filter_bar_components and selected_filter_bar_components['classofservice']):
            content.append('<td>')
            content.append('<select id="filterbarclassofservice" class="filter" name="classofservice" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            selectable_classes_of_service = self.classes_of_service + ['Unassigned']
            if member_document and "classofservice" in member_document and member_document["classofservice"] in selectable_classes_of_service:
                for selectable_class_of_service in selectable_classes_of_service:
                    if selectable_class_of_service == member_document["classofservice"]:
                        content.append('<option value="'+selectable_class_of_service+'" selected>'+selectable_class_of_service+'</option>')
                    else:
                        content.append('<option value="'+selectable_class_of_service+'">'+selectable_class_of_service+'</option>')

            else:
                for selectable_class_of_service in selectable_classes_of_service:
                    content.append('<option value="'+selectable_class_of_service+'">'+selectable_class_of_service+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Types
        content.append('<td>')
        selectable_types = ['epic', 'feature', 'story', 'enhancement', 'defect', 'task', 'test', 'bug', 'transient']
        content.append('<select id="filterbartype" class="filter" name="card_type" onchange="this.form.submit()">')
        content.append('<option value="">All</option>')
        if member_document and "type" in member_document and member_document["type"] in selectable_types:
            for selectable_type in selectable_types:
                if selectable_type == member_document["type"]:
                    content.append('<option class="'+selectable_type+'" value="'+selectable_type+'" selected>'+selectable_type.capitalize()+'</option>')
                else:
                    content.append('<option class="'+selectable_type+'" value="'+selectable_type+'">'+selectable_type.capitalize()+'</option>')

        else:
            for selectable_type in selectable_types:
                content.append('<option class="'+selectable_type+'" value="'+selectable_type+'">'+selectable_type.capitalize()+'</option>')

        content.append('</select>')
        content.append('</td>')

        # Columns
        content.append('<td>')
        content.append('<select id="filterbarcolumns" class="filter" name="columns" onchange="this.form.submit()">')
        content.append('<option value="">All</option>')
        selected_column_range = ""
        if member_document and "columns" in member_document and member_document["columns"]:
            selected_column_range = member_document["columns"]

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

        condensed_column_names = workflow_index.get('condensed_column_names', [])
        for bs, begin_column in enumerate(condensed_column_names):
            optgroup_shown = False
            for es, end_column in enumerate(condensed_column_names):
                if es > bs and not (begin_column==condensed_column_names[0] and end_column==condensed_column_names[-1]):
                    if not optgroup_shown:
                        content.append('<optgroup label="'+begin_column+'-...">')
                        optgroup_shown = True

                    potential_column_range = begin_column+'-'+end_column
                    if selected_column_range == potential_column_range:
                        content.append('<option value="'+potential_column_range+'" selected>'+potential_column_range.replace(' ','')+'</option>')
                    else:
                        content.append('<option value="'+potential_column_range+'">'+potential_column_range.replace(' ','')+'</option>')

            if optgroup_shown:
                content.append('</optgroup>')

        content.append('</select></td>')

        # User
        content.append('<td>')
        selectable_users = set()
        fullname = member_document.get('fullname', '')
        username = member_document.get('username', '')
        if fullname and username:
            selectable_users.add((fullname, username))

        if project:
            if project_document:
                project_member_documents = project_document.get('members', [])
            else:
                project_member_documents = []
            
            for project_member_document in project_member_documents:
                for othermember_document in self.members_collection.find({'username': project_member_document['username']}):
                    selectable_users.add((othermember_document['fullname'], othermember_document['username']))

        else:
            for othermember_document in self.members_collection.find({'username': {'$nin': ['', [], None]},
                                                                      'projects': {'$nin': ['', [], None]}}):
                for selectable_project in selectable_projects:
                    if self.project_in_projects(selectable_project, othermember_document['projects']):
                        selectable_users.add((othermember_document['fullname'], othermember_document['username']))

        selectable_users = list(selectable_users)
        selectable_users.sort()
        selectable_users.append(('Unassigned', 'Unassigned'))
        content.append('<select id="filterbaruser" class="filter" name="teammember" onchange="this.form.submit()">')
        content.append('<option value="">All</option>')
        for selectable_fullname, selectable_username in selectable_users:
            if 'teammember' in member_document and selectable_username == member_document["teammember"]:
                content.append('<option value="'+selectable_username+'" selected>'+selectable_fullname+'</option>')
            else:
                content.append('<option value="'+selectable_username+'">'+selectable_fullname+'</option>')

        content.append('</select></td>')

        # Project
        content.append('<td>')
        content.append('<select id="filterbarproject" class="filter" name="project" onchange="this.form.submit()">')
        content.append('<option value="">None</option>')
        selectable_projects.sort(key=str.lower)
        for selectable_project in selectable_projects:
            if selectable_project == project:
                content.append('<option value="'+selectable_project+'" selected>'+selectable_project+'</option>')
            else:
                content.append('<option value="'+selectable_project+'">'+selectable_project+'</option>')

        content.append('</select></td>')

        # Subteam
        if 'subteam' not in selected_filter_bar_components or ('subteam' in selected_filter_bar_components and selected_filter_bar_components['subteam']):
            content.append('<td>')
            content.append('<select id="filterbarsubteam" class="filter" name="subteam" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            selectable_subteams = self.projects_collection.find({'project': {'$in': selectable_projects}}).distinct('subteams.subteam')
            selectable_subteams.sort()
            selectable_subteams.append('Unassigned')
            for selectable_subteam in selectable_subteams:
                if 'subteam' in member_document and member_document["subteam"] and selectable_subteam == member_document["subteam"]:
                    content.append('<option value="'+selectable_subteam+'" selected>'+selectable_subteam+'</option>')
                else:
                    content.append('<option value="'+selectable_subteam+'">'+selectable_subteam+'</option>')

            content.append('</select></td>')

        # Release
        if 'release' not in selected_filter_bar_components or ('release' in selected_filter_bar_components and selected_filter_bar_components['release']):
            content.append('<td>')
            selectable_releases = []
            if selectable_projects:
                for project_document in self.projects_collection.find({'project': {'$in': selectable_projects}}):
                    if 'releases' in project_document:
                        for release_document in project_document['releases']:
                            release = release_document['release']
                            release_start_date = 0
                            release_end_date = 0
                            if 'start_date' in release_document and release_document['start_date']:
                                release_start_date = release_document['start_date']

                            if 'end_date' in release_document and release_document['end_date']:
                                release_end_date = release_document['end_date']

                            if self.get_release_status(project_document['project'], release, release_start_date, release_end_date) != 'Closed':
                                if release not in selectable_releases:
                                    selectable_releases.append(release)

                selectable_releases.sort()

            selectable_releases.append('Unassigned')
            content.append('<select id="filterbarrelease" class="filter" name="release" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            for selectable_release in selectable_releases:
                warning_class = ''
                selected = ''
                if selectable_release != 'Unassigned' and 'project' in member_document and member_document["project"]:
                    count = self.cards_collection.find({"project": member_document["project"],
                                                       "release": selectable_release}).count()
                    if not count:
                        warning_class = ' class="warning"'


                if 'release' in member_document and selectable_release == member_document["release"]:
                    selected = ' selected'

                content.append('<option'+warning_class+' value="'+selectable_release+'"'+selected+'>'+selectable_release+'</option>')

            content.append('</select>')
            content.append('</td>')

        if 'iteration' not in selected_filter_bar_components or ('iteration' in selected_filter_bar_components and selected_filter_bar_components['iteration']):
            content.append('<td>')
            # Iteration
            selectable_iterations = []
            if selectable_projects:
                for project_document in self.projects_collection.find({'project': {'$in': selectable_projects}}):
                    if 'releases' in project_document:
                        for release_document in project_document['releases']:
                            if 'iterations' in release_document:
                                for iteration_document in release_document['iterations']:
                                    iteration = iteration_document['iteration']
                                    iteration_start_date = 0
                                    iteration_end_date = 0
                                    if 'start_date' in iteration_document and iteration_document['start_date']:
                                        iteration_start_date = int(iteration_document['start_date'])

                                    if 'end_date' in iteration_document and iteration_document['end_date']:
                                        iteration_end_date = int(iteration_document['end_date'])

                                    if (self.get_iteration_status(project_document['project'],
                                            release_document['release'], iteration,
                                            iteration_start_date, iteration_end_date) != 'Closed'):
                                        if iteration not in selectable_iterations:
                                            selectable_iterations.append(iteration)

                selectable_iterations.sort()

            selectable_iterations.append('Unassigned')
            content.append('<select id="filterbariteration" class="filter" name="iteration" onchange="this.form.submit()">')
            if (member_document and 'iteration' in member_document and
                    member_document["iteration"] in selectable_iterations):
                content.append('<option value="">All</option>')
                for selectable_iteration in selectable_iterations:
                    warning_class = ''
                    selected = ''
                    if selectable_iteration != 'Unassigned' and (('project' in member_document and member_document["project"] in selectable_projects) or ("release" in member_document and member_document["release"] in selectable_releases)):
                        query = {}
                        if "project" in member_document and member_document["project"] in selectable_projects:
                            query['project'] = member_document["project"]

                        if "release" in member_document and member_document["release"] in selectable_releases:
                            query['release'] = member_document["release"]

                        query['iteration'] = selectable_iteration
                        count = self.cards_collection.find(query).count()
                        if not count:
                            warning_class = ' class="warning"'

                    if selectable_iteration == member_document["iteration"]:
                        selected = ' selected'

                    content.append('<option'+warning_class+' value="'+selectable_iteration+'"'+selected+'>'+selectable_iteration+'</option>')

            else:
                content.append('<option value="" selected>All</option>')
                for selectable_iteration in selectable_iterations:
                    content.append('<option value="'+selectable_iteration+'">'+selectable_iteration+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Severity
        if 'severity' not in selected_filter_bar_components or ('severity' in selected_filter_bar_components and selected_filter_bar_components['severity']):
            content.append('<td>')
            content.append('<select id="filterbarseverity" class="filter" name="severity" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            if member_document and "severity" in member_document and member_document["severity"] in self.severities:
                for selectable_severity in self.severities:
                    if selectable_severity == member_document["severity"]:
                        content.append('<option class="'+selectable_severity+'" value="'+selectable_severity+'" selected>'+selectable_severity.capitalize()+'</option>')
                    else:
                        content.append('<option class="'+selectable_severity+'" value="'+selectable_severity+'">'+selectable_severity.capitalize()+'</option>')

            else:
                for selectable_severity in self.severities:
                    content.append('<option class="'+selectable_severity+'" value="'+selectable_severity+'">'+selectable_severity.capitalize()+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Priority
        if ('priority' not in selected_filter_bar_components or
                ('priority' in selected_filter_bar_components and
                selected_filter_bar_components['priority'])):
            content.append('<td>')
            content.append('<select id="filterbarseverity" class="filter" name="priority" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            if (member_document and "priority" in member_document and
                    member_document["priority"] in self.priorities):
                for selectable_priority in self.priorities:
                    if selectable_priority == member_document["priority"]:
                        content.append('<option class="'+selectable_priority+'" value="'+selectable_priority+'" selected>'+selectable_priority.capitalize()+'</option>')
                    else:
                        content.append('<option class="'+selectable_priority+'" value="'+selectable_priority+'">'+selectable_priority.capitalize()+'</option>')

            else:
                for selectable_priority in self.priorities:
                    content.append('<option class="'+selectable_priority+'" value="'+selectable_priority+'">'+selectable_priority.capitalize()+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Card
        if 'card' not in selected_filter_bar_components or ('card' in selected_filter_bar_components and selected_filter_bar_components['card']):
            content.append('<td>')
            selectable_card_ids = self.cards_collection.find(owner_reviewer_search_criteria).distinct('id')
            content.append('<select id="filterbarcard" class="filter" name="card" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            selected_card = member_document.get("card", '')
            selectable_card_ids_with_children = []
            selectable_card_ids_without_children = []
            for selectable_card_id in selectable_card_ids:
                if self.cards_collection.find({'parent': selectable_card_id}).count():
                    selectable_card_ids_with_children.append(selectable_card_id)
                else:
                    selectable_card_ids_without_children.append(selectable_card_id)

            for (label, selectable_card_ids) in [('With Children', selectable_card_ids_with_children),
                                                 ('Without Children', selectable_card_ids_without_children)]:
                if selectable_card_ids:
                    content.append('<optgroup label="'+label+'">')
                    #selectable_card_ids.sort()
                    for selectable_card_id in selectable_card_ids:
                        if selectable_card_id == selected_card:
                            content.append('<option value="'+selectable_card_id+'" selected>'+selectable_card_id+'</option>')
                        else:
                            content.append('<option value="'+selectable_card_id+'">'+selectable_card_id+'</option>')

                    content.append('</optgroup>')

            content.append('</select>')
            content.append('</td>')

        # Categories
        if 'category' not in selected_filter_bar_components or ('category' in selected_filter_bar_components and selected_filter_bar_components['category']):
            content.append('<td>')
            selectable_categories = []
            selectable_category_documents = self.projects_collection.find({'project': {'$in': selectable_projects}}).distinct('categories')
            for selectable_category_document in selectable_category_documents:
                if 'category' in selectable_category_document:
                    if 'colour' in selectable_category_document:
                        selectable_categories.append((selectable_category_document['category'],
                                                      selectable_category_document['colour']
                                                     ))
                    else:
                        selectable_categories.append((selectable_category_document['category'],''))

            selectable_categories.append(('Unassigned', ''))
            content.append('<select id="filterbarcategory" class="filter" name="category" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            if member_document and "category" in member_document:
                for (selectable_category, selectable_colour) in selectable_categories:
                    if selectable_category != "":
                        content.append('<option')
                        if selectable_colour:
                            content.append(' style="background-color:'+selectable_colour+'"')

                        content.append(' value="'+selectable_category+'"')
                        if selectable_category == member_document["category"]:
                            content.append(' selected')

                        content.append('>'+selectable_category+'</option>')

            else:
                for (selectable_category, selectable_colour) in selectable_categories:
                    if selectable_category != "":
                        content.append('<option')
                        if selectable_colour:
                            content.append(' style="background-color:'+selectable_colour+'"')

                        content.append(' value="'+selectable_category+'"')
                        content.append('>'+selectable_category+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Hashtag
        if 'hashtag' not in selected_filter_bar_components or ('hashtag' in selected_filter_bar_components and selected_filter_bar_components['hashtag']):
            content.append('<td>')
            selectable_hashtags = self.cards_collection.find({'project': {'$in': selectable_projects}}).distinct('hashtags')
            selectable_hashtags.sort()
            content.append('<select id="filterbarhashtag" class="filter" name="hashtag" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            for selectable_hashtag in selectable_hashtags:
                content.append('<option value="'+selectable_hashtag+'"')
                if member_document and "hashtag" in member_document:
                    if selectable_hashtag == member_document["hashtag"]:
                        content.append(' selected')

                content.append('>'+selectable_hashtag+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Customer
        if 'customer' not in selected_filter_bar_components or ('customer' in selected_filter_bar_components and selected_filter_bar_components['customer']):
            content.append('<td>')
            selectable_customers = self.cards_collection.find({'project': {'$in': selectable_projects}}).distinct('customer')
            selectable_customers.append('Unassigned')
            content.append('<select id="filterbarcustomer" class="filter" name="customer" onchange="this.form.submit()">')
            content.append('<option value="">All</option>')
            for selectable_customer in selectable_customers:
                content.append('<option value="'+selectable_customer+'"')
                if member_document and "customer" in member_document:
                    if selectable_customer == member_document["customer"]:
                        content.append(' selected')

                content.append('>'+selectable_customer+'</option>')

            content.append('</select>')
            content.append('</td>')

        # Font Size
        if 'fontsize' not in selected_filter_bar_components or ('fontsize' in selected_filter_bar_components and selected_filter_bar_components['fontsize']):
            content.append('<td>')
            selectable_font_sizes = ['huge', 'large', 'normal', 'small', 'minute']
            content.append('<select id="filterbarfontsize" class="filter" name="fontsize" onchange="this.form.submit()">')
            if member_document and "fontsize" in member_document and member_document["fontsize"] in selectable_font_sizes:
                for selectable_font_size in selectable_font_sizes:
                    if selectable_font_size == member_document["fontsize"]:
                        content.append(f'<option class="{selectable_font_size}" value="{selectable_font_size}" selected>{selectable_font_size.capitalize()}</option>')
                    else:
                        content.append(f'<option class="{selectable_font_size}" value="{selectable_font_size}">{selectable_font_size.capitalize()}</option>')

            else:
                for selectable_font_size in selectable_font_sizes:
                    if selectable_font_size == 'normal':
                        content.append(f'<option class="{selectable_font_size}" value="{selectable_font_size}" selected>{selectable_font_size.capitalize()}</option>')
                    else:
                        content.append(f'<option class="{selectable_font_size}" value="{selectable_font_size}">{selectable_font_size.capitalize()}</option>')

            content.append('</select>')
            content.append('</td>')

        content.append('<td><input class="filter" type="submit" value="Filter"></form></td>')
        content.append('<td><form action="/filters/reset_filter" method="post"><input class="button" type="submit" value="Reset"></form>')
        content.append('</td></tr></tbody></table></h1>')
        return "".join(content)

    def find_all_hierarchical_ids(self, id, hierarchical_ids):
        """Gets the IDs of all cards descendents of given card ID"""
        # TODO - Do I need to pass hierarchical_ids into this function?
        card_document = self.cards_collection.find_one({'id': id})
        if card_document:
            for childcard_document in self.cards_collection.find({'parent': card_document['id']}):
                hierarchical_ids.append(childcard_document['id'])
                hierarchical_ids = self.find_all_hierarchical_ids(childcard_document['id'], hierarchical_ids)

            return hierarchical_ids

    def footer(self, title=""):
        """Assemble the HTML footer for a page"""
        datetime_now = datetime.datetime.utcnow()
        content = []
        content.append('<div id="footer_container"><div id="footer_contents">')
        content.append(f'Copyright &copy; Rebecca Shalfield 2013-2017 | <a class="footer" href="/admin/contactus">Contact Us</a> | Version {self.major}.{self.minor}.{self.revision}.{self.build} ({self.date}) | ')
        no_of_current_sessions = self.sessions_collection.find({'lastaccess': {'$gt': datetime_now-self.timedelta_hour}}).count()
        if no_of_current_sessions == 1:
            content.append(f'{no_of_current_sessions} user currently online')
        else:
            content.append(f'{no_of_current_sessions} users currently online')

        content.append('<a href="#" class="back-to-top">Back to Top</a>')
        content.append('</div></div>')
        if title == 'Status Report':
            content.append('<script type="text/javascript" src="/scripts/report.js"></script>')

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

    @staticmethod
    def format_multiline(modified_value):
        for phrase in ['\r\n', '\r', '\n']:
            modified_value = modified_value.replace(phrase, '<br>')

        return modified_value
        
    def generate_drop_js_script(self, swimlane_no, step_no, step_role, priority_or_special):
        if os.path.exists(self.current_dir+os.sep+'scripts'+os.sep+'autogenerated'+os.sep+'swimlane'+str(swimlane_no)+'step'+str(step_no)+step_role+priority_or_special+'.js'):
            return ""

        content = []
        #content.append('<script type="text/javascript">')
        content.append("$('#swimlane"+str(swimlane_no)+"step"+str(step_no)+step_role+priority_or_special+"""').droppable({
    accept      : "div",
    tolerance   : "pointer",
    hoverClass  : "dragdrop"
});\n\n""")
        content.append("$('#swimlane"+str(swimlane_no)+"step"+str(step_no)+step_role+priority_or_special+'''\').on("drop", function(e, ui){
    var id = "";
    idstate = ui.draggable.attr("id");
    [swimlane, id, stepno, steprole, state, original_priority, severity, special, release_no] = idstate.split('###');''')
        if priority_or_special == 'expedite':
            content.append("\n    $('#swimlane"+str(swimlane_no)+"step"+str(step_no)+step_role+priority_or_special+'''\').html('<img src="/images/ajax-loader.gif">').load('/kanban/dropped_on_step_expedite', {step_no:\''''+str(step_no)+'''\', step_role:\''''+step_role+'''\', doc_id:id});''')
        elif priority_or_special == 'header':
            content.append('''\n    if (special === 'expedite') {
        $('#swimlane'''+str(swimlane_no)+"step"+str(step_no)+step_role+'''expedite\').html('<img src="/images/ajax-loader.gif">').load('/kanban/dropped_on_step_expedite', {step_no:\''''+str(step_no)+'''\', step_role:\''''+step_role+'''\',  doc_id:id});
    } else {
        $('#swimlane'+swimlane+'step'''+str(step_no)+step_role+'''\'+original_priority).html('<img src="/images/ajax-loader.gif">').load('/kanban/dropped_on_step_priority', {swimlane_no:\''''+str(swimlane_no)+"', step_no:'"+str(step_no)+"', step_role:'"+step_role+'''\', priority:original_priority, doc_id:id});
    }''')
        else:
            content.append("\n    $('#swimlane"+str(swimlane_no)+"step"+str(step_no)+step_role+priority_or_special+'''\').html('<img src="/images/ajax-loader.gif">').load('/kanban/dropped_on_step_priority', {swimlane_no:\''''+str(swimlane_no)+"', step_no:'"+str(step_no)+"', step_role:'"+step_role+"\', priority:\'"+priority_or_special+"\', doc_id:id});")

        content.append('''\n    if (special === 'expedite') {
        $('#swimlane'+swimlane+'step'+stepno+steprole+special).html('<img src="/images/ajax-loader.gif">').load('/kanban/step_'+steprole+'_'+special, {swimlane_no:swimlane, step_no:stepno});
    } else {
        $('#swimlane'+swimlane+'step'+stepno+steprole+original_priority).html('<img src="/images/ajax-loader.gif">').load('/kanban/step_'+steprole+'_'+original_priority, {swimlane_no:swimlane, step_no:stepno});
    }
});''')

        if int(swimlane_no) == 0 and priority_or_special in ['medium', 'all']:
            content.append('''\n\n$('#togglestep'''+str(step_no)+step_role+'''').click(function() {
    if (($('#swimlane0step'''+str(step_no)+step_role+'''medium').is(':visible')) || ($('#swimlane0step'''+str(step_no)+step_role+'''all').is(':visible'))) {
        $('#step'''+str(step_no)+step_role+'''entrycriteria').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''expedite').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''all').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''critical').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''high').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''medium').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''low').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''pull').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''waiting').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''blocked').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''deferred').hide();
        $('#swimlane0step'''+str(step_no)+step_role+'''ghosted').hide();
        $('#step'''+str(step_no)+step_role+'''exitcriteria').hide();
        $('#togglestep'''+str(step_no)+step_role+'''').val('Show');
    } else {
        $('#step'''+str(step_no)+step_role+'''entrycriteria').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''expedite').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''all').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''critical').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''high').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''medium').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''low').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''pull').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''waiting').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''blocked').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''deferred').show();
        $('#swimlane0step'''+str(step_no)+step_role+'''ghosted').show();
        $('#step'''+str(step_no)+step_role+'''exitcriteria').show();
        $('#togglestep'''+str(step_no)+step_role+'''').val('Hide');
    }
});''')

        #content.append('</script>')
        content = "".join(content)
        filepath = self.current_dir+os.sep+'scripts'+os.sep+'autogenerated'+os.sep+'swimlane'+str(swimlane_no)+'step'+str(step_no)+step_role+priority_or_special+'.js'
        epoch = datetime.datetime.utcnow()
        while filepath in self.file_locks and self.file_locks[filepath] > epoch - self.timedelta_minute:
            True

        self.file_locks[filepath] = datetime.datetime.utcnow()
        op = open(filepath, 'w')
        op.write(content)
        op.close()
        del self.file_locks[filepath]
        return content

    def get_associated_state_information(self, state):
        """Return the associated step and step role for a given state"""
        associated_step_no = ""
        state_found = False
        step_role = ""
        centric = ""
        preceding_state = ""
        next_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)
        project = member_document.get('project', '')
        for project_document in self.projects_collection.find({'project': project, 'workflow': {'$exists': True}}):
            workflow = project_document['workflow']
            for step_no, step_document in enumerate(workflow):
                if state_found:
                    if not next_state:
                        if 'maincolumn' in step_document:
                            maincolumn_document = step_document['maincolumn']
                            next_state = maincolumn_document['name']
                        elif 'counterpartcolumn' in step_document:
                            counterpartcolumn_document = step_document['counterpartcolumn']
                            next_state = counterpartcolumn_document['name']
                        elif 'buffercolumn' in step_document:
                            buffercolumn_document = step_document['buffercolumn']
                            next_state = buffercolumn_document['name']

                else:
                    if 'counterpartcolumn' in step_document:
                        counterpartcolumn_document = step_document['counterpartcolumn']
                        if counterpartcolumn_document['state'] == state:
                            state_found = True
                            associated_step_no = step_no
                            step_role = 'counterpart'
                            centric = counterpartcolumn_document['centric']
                        else:
                            preceding_state = counterpartcolumn_document['state']

                    elif 'maincolumn' in step_document:
                        maincolumn_document = step_document['maincolumn']
                        if maincolumn_document['state'] == state:
                            state_found = True
                            associated_step_no = step_no
                            step_role = 'main'
                            centric = maincolumn_document['centric']
                        else:
                            preceding_state = maincolumn_document['state']

                    if not state_found:
                        if 'buffercolumn' in step_document:
                            buffercolumn_document = step_document['buffercolumn']
                            if buffercolumn_document['state'] == state:
                                state_found = True
                                associated_step_no = step_no
                                step_role = 'buffer'
                                centric = buffercolumn_document['centric']
                            else:
                                preceding_state = buffercolumn_document['state']

        return associated_step_no, step_role, centric, preceding_state, next_state

    def get_busy_order_user_distincts(self, fullnames_and_usernames, project, release, iteration):
        busy_order_user_distincts = []
        all_states = self.get_custom_states_mapped_onto_metastates(['defined', 'analysis', 'analysed', 'design',
                                                                    'designed', 'development', 'developed',
                                                                    'unittesting', 'unittestingaccepted',
                                                                    'integrationtesting', 'integrationtestingaccepted',
                                                                    'systemtesting', 'systemtestingaccepted',
                                                                    'acceptancetesting', 'acceptancetestingaccepted'])
        search_criteria = {'state': {'$in': all_states}}
        if project:
            search_criteria['project'] = project

        if release:
            search_criteria['release'] = release

        if iteration:
            search_criteria['iteration'] = iteration

        for fullname_distinct, usernameDistinct in fullnames_and_usernames:
            user_values = []
            search_criteria["$or"] = [{"owner": usernameDistinct}, {"coowner":usernameDistinct},
                                      {"reviewer": usernameDistinct}, {"coreviewer": usernameDistinct}]

            # TODO - Need to return transient count
            for type in [{'$in': ['epic', 'feature', 'story', 'enhancement', 'defect', 'task', 'test', 'bug', 'transient']},
                         'epic', 'feature', 'story', 'enhancement', 'defect', 'task', 'test', 'bug', 'transient']:
                search_criteria['type'] = type
                count = self.cards_collection.find(search_criteria).count()
                if type in ['epic', 'feature', 'story', 'enhancement', 'defect', 'task', 'test', 'bug', 'transient'] and not count:
                    count = 0

                user_values.append(count)

            user_values.append(fullname_distinct)
            user_values.append(usernameDistinct)
            busy_order_user_distincts.append(user_values)

        busy_order_user_distincts.sort()
        return busy_order_user_distincts

    @staticmethod
    def get_card_attribute_values(card_document, attributes):
        """Get selected attribute values from a card"""
        # TODO - This function could be expanded to cater for all document types
        values = []
        for attribute in attributes:
            value = None
            if attribute in ['_id']:
                value = card_document.get(attribute, None)
            elif attribute in ['deadline', 'lastchanged', 'nextaction']:
                value = card_document.get(attribute, 0)
            elif attribute in ['statehistory']:
                value = card_document.get(attribute, [])
            else:
                value = card_document.get(attribute, '')

            values.append(value)

        return values

    def get_corresponding_metastate(self, project_document, state):
        """Returns the metastate associated with a custom state"""
        metastate = state
        if state not in self.metastates_list:
            custom_states = project_document.get('customstates', {})
            if custom_states and state in custom_states:
                metastate = custom_states[state]

        return metastate

    def get_custom_states_mapped_onto_metastates(self, metastates):
        all_states = []
        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', '')
        for project_document in self.projects_collection.find({'project': project}):
            custom_states = project_document.get('customstates', {})
            for metastate in metastates:
                custom_state_found = False
                for custom_state, metastate_mapped_to in custom_states.items():
                    if metastate_mapped_to == metastate:
                        all_states.append(custom_state)
                        custom_state_found = True
                        break

                if not custom_state_found:
                    all_states.append(metastate)

        return all_states

    def get_displayable_columns(self):
        """Obtain the list of columns selected to be displayed by the user via the filter bar"""
        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:
            project_document = self.projects_collection.find_one({'project': member_document['project']})
            if project_document:
                workflow_index = project_document.get('workflow_index', {})
            else:
                workflow_index = {}

            uncondensed_column_names = workflow_index.get('uncondensed_column_names', [])
            uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
            condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
            if 'columns' in member_document and member_document['columns']:
                (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)
                    start_state = uncondensed_column_states[start_column_pos]
                    end_state   = uncondensed_column_states[end_column_pos]
                    start_pos = condensed_column_states_dict[start_state]
                    end_pos = condensed_column_states_dict[end_state]
                    condensed_column_names  = [x for x in uncondensed_column_names[start_pos:end_pos+1] if x != '']
                    condensed_column_states = [x for x in uncondensed_column_states[start_pos:end_pos+1] if x != '']
                    return condensed_column_names, condensed_column_states
                except:
                    condensed_column_names = workflow_index.get('condensed_column_names', [])
                    condensed_column_states = workflow_index.get('condensed_column_states', [])
                    return condensed_column_names, condensed_column_states

            else:
                condensed_column_names = workflow_index.get('condensed_column_names', [])
                condensed_column_states = workflow_index.get('condensed_column_states', [])
                return condensed_column_names, condensed_column_states

        else:
            return [], []

    def get_document_count(self, state, priorities=[]):
        """comment"""
        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})
        project_wips = member_document.get('project_wips', {})
        personal_wip_document = project_wips.get(project, {})
        owner_search_criteria, reviewer_search_criteria, _ = self.get_filtered_search_criteria(session_document, [state])
        if priorities:
            owner_search_criteria['priority']    = {'$in': priorities}
            reviewer_search_criteria['priority'] = {'$in': priorities}

        for attribute in ['blocked', 'deferred']:
            owner_search_criteria[attribute]    = {"$in": ['', None]}
            reviewer_search_criteria[attribute] = {"$in": ['', None]}

        step_no, step_role, _, _, next_state = self.get_associated_state_information(state)
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        global_wips = project_document.get('global_wips', {})

        if condensed_column_states and state != condensed_column_states[-1]:
            min_wip_limit = 0
            if not ('enforcewiplimits' in global_wips and global_wips['enforcewiplimits']):
                min_wip_limit = personal_wip_document.get('step'+str(step_no)+step_role+'minwip', 0)

            if not min_wip_limit:
                wip_grouping = ""
                wip_scope = ""
                wip_limit = ""
                if 'step'+str(step_no)+step_role+'minwip' in global_wips and global_wips['step'+str(step_no)+step_role+'minwip'] != "" and int(global_wips['step'+str(step_no)+step_role+'minwip']) > -1:
                    min_wip_limit += int(global_wips['step'+str(step_no)+step_role+'minwip'])
                else:
                    wip_grouping = global_wips.get('wip_grouping', "")
                    wip_scope = global_wips.get('wip_scope', "")
                    wip_limit = global_wips.get('wip_limit', "")
                    if wip_grouping == 'Whole Team':
                        search_criteria = {'project': project, 'type': {'$in': ['story']}}
                        if wip_scope == 'Whole Board':
                            first_and_last_states = self.get_custom_states_mapped_onto_metastates(['untriaged', 'triaged', 'backlog', 'completed', 'closed'])
                            search_criteria['state'] = {'$nin': first_and_last_states}
                            project_record_count = self.cards_collection.find(search_criteria).count()
                            no_of_applicable_columns = len(condensed_column_states) - 2
                            min_wip_limit += (int(wip_limit)-project_record_count)/no_of_applicable_columns
                        elif wip_scope == 'Column':
                            search_criteria['state'] = state
                            project_record_count = self.cards_collection.find(search_criteria).count()
                            min_wip_limit += int(wip_limit)-project_record_count
                        elif wip_scope == 'Estimate':
                            min_wip_limit += int(wip_limit)

                    elif wip_grouping == 'Team Member':
                        if wip_scope == 'Whole Board':
                            min_wip_limit += int(wip_limit)
                        elif wip_scope == 'Column':
                            min_wip_limit += int(wip_limit)
                        elif wip_scope == 'Estimate':
                            min_wip_limit += int(wip_limit)

            max_wip_limit = 0
            if not ('enforcewiplimits' in global_wips and global_wips['enforcewiplimits']):
                if 'step'+str(step_no)+'sharedmaxwip' in personal_wip_document and personal_wip_document['step'+str(step_no)+'sharedmaxwip']:
                    if personal_wip_document['step'+str(step_no)+'sharedmaxwip'] == -1:
                        max_wip_limit = int(personal_wip_document['step'+str(step_no)+'sharedmaxwip'])
                    else:
                        next_state_step_no, _, _, _, _ = self.get_associated_state_information(next_state)
                        if next_state_step_no == step_no:
                            owner_search_criteria['state'] = next_state
                        else:
                            owner_search_criteria['state'] = state

                        counterpart_count = self.cards_collection.find(owner_search_criteria).count()
                        owner_search_criteria['state'] = state
                        max_wip_limit = int(personal_wip_document['step'+str(step_no)+'sharedmaxwip']) - counterpart_count
                        if max_wip_limit < 1:
                            max_wip_limit = 1

                elif 'step'+str(step_no)+step_role+'maxwip' in personal_wip_document:
                    max_wip_limit = int(personal_wip_document['step'+str(step_no)+step_role+'maxwip'])

            if not max_wip_limit:
                wip_grouping = ""
                wip_scope = ""
                wip_limit = ""
                project_document = self.projects_collection.find_one({'project': project})
                if project_document:
                    if 'step'+str(step_no)+step_role+'maxwip' in global_wips and global_wips['step'+str(step_no)+step_role+'maxwip'] != "" and int(global_wips['step'+str(step_no)+step_role+'maxwip']) > -1:
                        max_wip_limit += int(global_wips['step'+str(step_no)+step_role+'maxwip'])
                    else:
                        if 'wip_grouping' in global_wips:
                            wip_grouping = global_wips['wip_grouping']

                        if 'wip_scope' in global_wips:
                            wip_scope = global_wips['wip_scope']

                        if 'wip_limit' in global_wips:
                            wip_limit = global_wips['wip_limit']

                        if wip_grouping == 'Whole Team':
                            search_criteria = {'project': project, 'type': 'story'}
                            if wip_scope == 'Whole Board':
                                custom_states = self.get_custom_states_mapped_onto_metastates(['untriaged', 'triaged',
                                                                                               'backlog', 'closed'])
                                search_criteria['state'] = {'$nin': custom_states}
                                project_record_count = self.cards_collection.find(search_criteria).count()
                                applicable_columns = []
                                for metastate in ['analysis', 'design', 'development', 'unittesting',
                                                  'integrationtesting', 'systemtesting', 'acceptancetesting']:
                                    custom_states = self.get_custom_states_mapped_onto_metastates([metastate])
                                    for custom_state in custom_states:
                                        if custom_state in condensed_column_states:
                                            applicable_columns.append(metastate)
                                            break

                                no_of_applicable_columns = len(applicable_columns)
                                max_wip_limit += (int(wip_limit)-project_record_count)/no_of_applicable_columns
                            elif wip_scope == 'Column':
                                search_criteria['state'] = state
                                project_record_count = self.cards_collection.find(search_criteria).count()
                                max_wip_limit += int(wip_limit)-project_record_count
                            elif wip_scope == 'Estimate':
                                max_wip_limit += int(wip_limit)

                        elif wip_grouping == 'Team Member':
                            if wip_scope == 'Whole Board':
                                max_wip_limit += int(wip_limit)
                            elif wip_scope == 'Column':
                                max_wip_limit += int(wip_limit)
                            elif wip_scope == 'Estimate':
                                max_wip_limit += int(wip_limit)

            if not max_wip_limit:
                max_wip_limit = -1

        else:
            min_wip_limit = ""
            if not ('enforcewiplimits' in global_wips and global_wips['enforcewiplimits']):
                max_wip_limit = personal_wip_document.get('closedwip', '1 month')
            else:
                max_wip_limit = global_wips.get('closedwip', '1 month')

        owner_count = self.cards_collection.find(owner_search_criteria).count()
        reviewer_count = self.cards_collection.find(reviewer_search_criteria).count()
        return owner_count, reviewer_count, min_wip_limit, max_wip_limit

    def get_filtered_search_criteria(self, session_document, states):
        """comment"""
        projects = []
        users = []
        member_document = Kanbanara.get_member_document(self, session_document)
        if member_document:
            if member_document and 'projects' in member_document and member_document["projects"]:
                for project_document in member_document["projects"]:
                    if 'project' in project_document and project_document["project"]:
                        projects.append(project_document["project"])

            if 'username' in member_document and member_document['username']:
                users.append(member_document['username'])

            for project in projects:
                for document in self.members_collection.find({}):
                    if 'projects' in document and self.project_in_projects(project, document['projects']):
                        if 'username' in document and document['username'] not in users:
                            users.append(document['username'])

        owner_search_criteria          = {"state": {'$in': states}}
        reviewer_search_criteria       = {"state": {'$in': states}}
        owner_reviewer_search_criteria = {"state": {'$in': states}}
        subteam = member_document.get('subteam', '')
        if subteam:
            if subteam == 'Unassigned':
                owner_search_criteria["subteam"]          = {"$in": ["", None]}
                reviewer_search_criteria["subteam"]       = {"$in": ["", None]}
                owner_reviewer_search_criteria["subteam"] = {"$in": ["", None]}
            else:
                owner_search_criteria["subteam"]          = member_document["subteam"]
                reviewer_search_criteria["subteam"]       = member_document["subteam"]
                owner_reviewer_search_criteria["subteam"] = member_document["subteam"]

        category = member_document.get('category', '')
        if category:
            if category == 'Unassigned':
                owner_search_criteria["category"] = {"$in": ["", None]}
                reviewer_search_criteria["category"] = {"$in": ["", None]}
                owner_reviewer_search_criteria["category"] = {"$in": ["", None]}
            else:
                owner_search_criteria["category"] = member_document["category"]
                reviewer_search_criteria["category"] = member_document["category"]
                owner_reviewer_search_criteria["category"] = member_document["category"]

        classofservice = member_document.get('classofservice', '')
        if classofservice:
            if classofservice == 'Unassigned':
                owner_search_criteria["classofservice"] = {"$in": ["", None]}
                reviewer_search_criteria["classofservice"] = {"$in": ["", None]}
                owner_reviewer_search_criteria["classofservice"] = {"$in": ["", None]}
            else:
                owner_search_criteria["classofservice"] = member_document["classofservice"]
                reviewer_search_criteria["classofservice"] = member_document["classofservice"]
                owner_reviewer_search_criteria["classofservice"] = member_document["classofservice"]

        if "customer" in member_document and member_document["customer"] != "":
            if member_document["customer"] == 'Unassigned':
                owner_search_criteria["customer"] = {"$in": ["", None]}
                reviewer_search_criteria["customer"] = {"$in": ["", None]}
                owner_reviewer_search_criteria["customer"] = {"$in": ["", None]}
            else:
                owner_search_criteria["customer"] = member_document["customer"]
                reviewer_search_criteria["customer"] = member_document["customer"]
                owner_reviewer_search_criteria["customer"] = member_document["customer"]

        if "hashtag" in member_document and member_document["hashtag"] != "":
            owner_search_criteria["hashtags"] = member_document["hashtag"]
            reviewer_search_criteria["hashtags"] = member_document["hashtag"]
            owner_reviewer_search_criteria["hashtags"] = member_document["hashtag"]

        if "type" in member_document and member_document["type"] != "":
            owner_search_criteria["type"] = member_document["type"].lower()
            reviewer_search_criteria["type"] = member_document["type"].lower()
            owner_reviewer_search_criteria["type"] = member_document["type"].lower()

        if "teammember" in member_document and member_document["teammember"]:
            if member_document["teammember"] == 'Unassigned':
                owner_search_criteria["owner"] = {"$in": ["", None]}
                reviewer_search_criteria["reviewer"] = {"$in": ["", None]}
                owner_reviewer_search_criteria["$or"] = [{"owner":    {"$in": ["", None]}},
                                                         {"reviewer": {"$in": ["", None]}}]
            else:
                owner_search_criteria["$or"] = [{"owner":   member_document["teammember"]},
                                                {"coowner": member_document["teammember"]}]
                reviewer_search_criteria["$or"] = [{"reviewer":   member_document["teammember"]},
                                                   {"coreviewer": member_document["teammember"]}]
                owner_reviewer_search_criteria["$or"] = [{"owner":      member_document["teammember"]},
                                                         {"coowner":    member_document["teammember"]},
                                                         {"reviewer":   member_document["teammember"]},
                                                         {"coreviewer": member_document["teammember"]}]

        else:
            owner_search_criteria["$or"] = [{"owner": {"$in": users}},
                                            {"coowner": {"$in": users}}]
            reviewer_search_criteria["$or"] = [{"reviewer": {"$in": users}},
                                               {"coreviewer": {"$in": users}}]
            owner_reviewer_search_criteria["$or"] = [{"owner": {"$in": users}},
                                                     {"coowner": {"$in": users}},
                                                     {"reviewer": {"$in": users}},
                                                     {"coreviewer": {"$in": users}}]

        if 'project' in member_document and member_document["project"]:
            owner_search_criteria["project"] = member_document["project"]
            reviewer_search_criteria["project"] = member_document["project"]
            owner_reviewer_search_criteria["project"] = member_document["project"]
        else:
            owner_search_criteria["project"] = {"$in":projects}
            reviewer_search_criteria["project"] = {"$in":projects}
            owner_reviewer_search_criteria["project"] = {"$in":projects}

        if 'release' in member_document and member_document["release"] != "":
            if member_document["release"] == 'Unassigned':
                owner_search_criteria["release"] = {"$in": ["", None]}
                reviewer_search_criteria["release"] = {"$in": ["", None]}
                owner_reviewer_search_criteria["release"] = {"$in": ["", None]}
            else:
                owner_search_criteria["release"] = member_document["release"]
                reviewer_search_criteria["release"] = member_document["release"]
                owner_reviewer_search_criteria["release"] = member_document["release"]

        if 'iteration' in member_document and member_document["iteration"] != "":
            if member_document["iteration"] == 'Unassigned':
                owner_search_criteria["iteration"] = {"$in": ["", None]}
                reviewer_search_criteria["iteration"] = {"$in": ["", None]}
                owner_reviewer_search_criteria["iteration"] = {"$in": ["", None]}
            else:
                owner_search_criteria["iteration"] = member_document["iteration"]
                reviewer_search_criteria["iteration"] = member_document["iteration"]
                owner_reviewer_search_criteria["iteration"] = member_document["iteration"]

        return owner_search_criteria, reviewer_search_criteria, owner_reviewer_search_criteria

    @staticmethod
    def get_font_size(member_document):
        """Return the member's fontsize setting"""
        return member_document.get('fontsize', 'normal')

    def get_iteration_status(self, project, release, iteration, start_date=0, end_date=0):
        """Get the 'Pending', 'Closing', 'Closed' or 'In Progress' status of an iteration"""
        status = ""
        epoch = datetime.datetime.utcnow()
        closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
        if start_date and start_date > epoch:
            status = 'Pending'
        elif end_date and end_date < epoch:
            if self.cards_collection.find({'project': project, 'release': release, 'iteration': iteration,
                                          'state': {'$nin': closed_states}}).count():
                status = 'Closing'
            else:
                status = 'Closed'

        elif start_date and end_date and start_date < epoch < end_date:
            status = 'In Progress'
        else:
            status = 'Unknown'

        return status

    def get_member_document(self, session_document):
        """Returns member document with same username as given session document"""
        member_document = {}
        if session_document and 'username' in session_document and session_document['username']:
            member_document = self.members_collection.find_one({"username": session_document['username']})

        return member_document

    @staticmethod
    def get_member_projects(member_document):
        """Returns a sorted list of unique projects a user is a member of"""
        projects = set()
        if member_document and 'projects' in member_document and member_document["projects"]:
            for project_document in member_document["projects"]:
                if 'project' in project_document and project_document['project']:
                    projects.add(project_document['project'])

        projects = list(projects)
        projects.sort()
        return projects

    def get_member_project_release_iteration(self, member_document):
        """Return the project, release and iteration for a given member"""
        project = ''
        release = ''
        iteration = ''
        if member_document:
            project = member_document.get('project', '')
            release = member_document.get('release', '')
            iteration = member_document.get('iteration', '')

        return project, release, iteration

    def get_page_component(self, page):
        return self.page_component_mappings[page]

    def get_projection_day_labels(self):
        day_count = 0
        day_labels = []
        while day_count < 7:
            future_epoch = datetime.datetime.utcnow() + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_display_format(future_epoch)
            day_of_week = self.day_of_week[future_epoch.weekday()]
            if day_count == 0:
                day_labels.append('Today')
            elif day_count == 1:
                day_labels.append('Tomorrow')
            else:
                day_labels.append(date_format+' ('+day_of_week+')')

            day_count += 1

        return day_labels

    def get_project_members(self, projects):
        """comment"""
        fullnames_and_usernames = set()
        for member_document in self.members_collection.find({'fullname': {"$exists": True, '$nin': ['', None]},
                                                            'username': {"$exists": True, '$nin': ['', None]},
                                                            'projects.project': {'$in': projects}}):
            fullnames_and_usernames.add((member_document['fullname'],member_document['username']))

        fullnames_and_usernames = list(fullnames_and_usernames)
        fullnames_and_usernames.sort()
        return fullnames_and_usernames

    def get_project_next_card_number(self, project, type):
        project_document = self.projects_collection.find_one({"project": project})
        if type == 'transient' or ('role' in project_document and project_document['role'] == 'slave'):
            random_uuid = uuid.uuid4().hex
            id = project+'-'+str(random_uuid)
            return id
        else:
            if project in self.next_card_numbers:
                next_card_number = self.next_card_numbers[project]
            else:
                next_card_number = project_document.get("nextcardnumber", 1)

            id = project+'-'+str(next_card_number)
            while self.cards_collection.find({'id': id}).count():
                next_card_number += 1
                id = project+'-'+str(next_card_number)

            self.next_card_numbers[project] = next_card_number + 1
            project_document["nextcardnumber"] = next_card_number + 1
            self.projects_collection.save(project_document)
            self.save_project_as_json(project_document)
            return id

    def get_project_release_iteration_dates(self, project, release, iteration):
        start_date = datetime.timedelta()
        end_date   = datetime.timedelta()
        scope      = ""
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if release and iteration:
                if 'releases' in project_document:
                    for release_document in project_document['releases']:
                        if release == release_document['release']:
                            if 'iterations' in release_document:
                                for iteration_document in release_document['iterations']:
                                    if iteration == iteration_document['iteration']:
                                        if 'start_date' in iteration_document and iteration_document['start_date']:
                                            start_date = iteration_document['start_date']

                                        if 'end_date' in iteration_document and iteration_document['end_date']:
                                            end_date = iteration_document['end_date']

                                        scope = project+':'+release+':'+iteration
                                        break

            elif release:
                if 'releases' in project_document:
                    for release_document in project_document['releases']:
                        if release == release_document['release']:
                            if 'start_date' in release_document and release_document['start_date']:
                                start_date = release_document['start_date']

                            if 'end_date' in release_document and release_document['end_date']:
                                end_date = release_document['end_date']

                            scope = project+':'+release
                            break

            else:
                if 'start_date' in project_document and project_document['start_date']:
                    start_date = project_document['start_date']

                if 'end_date' in project_document and project_document['end_date']:
                    end_date = project_document['end_date']

                scope = project

        return start_date, end_date, scope

    def get_release_status(self, project, release, start_date=datetime.timedelta(),
                           end_date=datetime.timedelta()):
        """Get the 'Pending', 'Closing', 'Closed' or 'In Progress' status of a release"""
        status = ""
        epoch = datetime.datetime.utcnow()
        closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
        if start_date and start_date > epoch:
            status = 'Pending'
        elif end_date and end_date < epoch:
            if self.cards_collection.find({'project': project, 'release': release,
                                          'state': {'$nin': closed_states}}).count():
                status = 'Closing'
            else:
                status = 'Closed'

        elif start_date and end_date and start_date <= epoch <= end_date:
            status = 'In Progress'
        else:
            status = 'Unknown'

        return status

    def get_roadmap_release_number(self, project, release):
        release_no = -1
        selectable_releases = []
        for project_document in self.projects_collection.find({'project': project}):
            if 'releases' in project_document:
                for release_document in project_document['releases']:
                    release_name = release_document['release']
                    release_start_date = datetime.timedelta()
                    release_end_date = datetime.timedelta()
                    if 'start_date' in release_document and release_document['start_date']:
                        release_start_date = release_document['start_date']

                    if 'end_date' in release_document and release_document['end_date']:
                        release_end_date = release_document['end_date']

                    if self.get_release_status(project, release_name, release_start_date, release_end_date) != 'Closed':
                        if (release_start_date, release_end_date, release_name) not in selectable_releases:
                            selectable_releases.append((release_start_date, release_end_date, release_name))

        selectable_releases.sort()
        for candidate_release_no, (_, _, candidate_release) in enumerate(selectable_releases):
            if candidate_release == release:
                return candidate_release_no

        return release_no

    def get_state_metrics(self, states, statehistory):
        """Returns a card's state metrics"""
        processed = []
        state_metrics = {}
        for state in states:
            state_metrics[state] = datetime.timedelta()

        for statehistory_document in reversed(statehistory):
            if 'state' in statehistory_document:
                state = statehistory_document['state']
                if state in states:
                    if not state_metrics[state] and 'datetime' in statehistory_document:
                        state_metrics[state] = statehistory_document['datetime']
                        processed.append(state)

            if len(processed) == len(states):
                break

        return state_metrics

    def get_teammember_avatar(self, username):
        for member_document in self.members_collection.find({'username': username}):
            existing_avatar = member_document.get('avatar', '')
            if not existing_avatar or not os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+existing_avatar):
                for image_file_format in ['.png', '.gif', '.jpg']:
                    if os.path.exists(self.current_dir+os.sep+'images'+os.sep+'avatars'+os.sep+username+image_file_format):
                        existing_avatar = username+image_file_format
                        break

        return existing_avatar

    def get_user_absence_dates(self, username):
        """Obtains the start and end dates of another user's absence"""
        absence_start_date = datetime.timedelta()
        absence_end_date   = datetime.timedelta()
        other_member_document = self.members_collection.find_one({'username': username})
        if other_member_document:
            absence_start_date = other_member_document.get('absencestartdate', datetime.timedelta())
            absence_end_date   = other_member_document.get('absenceenddate',   datetime.timedelta())

        return absence_start_date, absence_end_date

    @staticmethod
    def getstatusoutput(cmd):
        """Return (status, output) of executing cmd in a shell."""
        mswindows = (sys.platform == "win32")
        if not mswindows:
            cmd = '{ ' + cmd + '; }'

        pipe = os.popen(cmd + ' 2>&1', 'r')
        text = pipe.read()
        sts = pipe.close()
        if sts is None: sts = 0
        if text[-1:] == '\n': text = text[:-1]
        return sts, text

    def header(self, page, title):
        """comment"""
        session_id = self.cookie_handling()
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        username = session_document.get('username', '')
        member_document = self.get_member_document(session_document)
        autorefresh = True
        if self.user_kanban_board_settings and username in self.user_kanban_board_settings:
            kanban_board_settings = self.user_kanban_board_settings[username]
            if 'swimlane_values' in kanban_board_settings and kanban_board_settings['swimlane_values']:
                autorefresh = False

        theme = member_document.get('theme', 'kanbanara')
        if not theme.lower().startswith('kanbanara'):
            if not os.path.exists(os.path.join(self.current_dir, 'css', 'themes', theme+'.css')):
                theme = 'kanbanara'
                member_document['theme'] = theme
                self.members_collection.save(member_document)

        content = []
        header_path = os.path.join(self.current_dir, '..', 'templates', 'header.tpl')
        content.append(Template(filename=header_path).render(instance=self.instance, title=title, page=page, theme=theme, session_document=session_document, autorefresh=autorefresh))
        _, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        no_of_documents = self.cards_collection.find(owner_reviewer_search_criteria).count()
        content.append(f'<b id="cards"><span class="fa fa-sticky-note-o fa-lg"></span>&nbsp;{no_of_documents} Cards</b>')
        content.append('<button id="refresh" value="Refresh" onclick="location.reload(true);" /><span class="fa fa-recycle fa-lg"></span>&nbsp;Refresh</button>')
        content.append(f'<form id="help" action="/admin/help" method="post"><input type="hidden" name="component" value="{self.component}"><input type="hidden" name="destination" value="{page}">')
        if session_document and 'help' in session_document and session_document["help"] == "enabled":
            content.append('<button type="submit" value="Help Off" title="Online help is currently enabled!"><span class="fa fa-info-circle fa-lg"></span>&nbsp;Help&nbsp;Off</button>')
        else:
            content.append('<button type="submit" value="Help On" title="Online help is currently disabled!"><span class="fa fa-info-circle fa-lg"></span>&nbsp;Help&nbsp;On</button>')

        content.append('</form> ')

        if username:
            member_document = self.members_collection.find_one({"username": username})
            if member_document and 'firstname' in member_document and member_document['firstname']:
                content.append('<b id="welcome">Welcome '+member_document['firstname']+'!</b>')
            else:
                content.append('<b id="welcome">Welcome!</b>')

            content.append('<form id="logoff" action="/members/logoff" method="post"><button type="submit" value="Logoff"><span class="fa fa-sign-out fa-lg"></span>&nbsp;Logoff</button></form>')        

        content.append('</div>') # End of Header Button Bar

        content.append('</div>')
        content.append('<h1 id="javascriptdisabled">This web site will only function correctly with JavaScript enabled!</h1>')
        return "".join(content)

    def insert_card_cost_chart(self, estimatedcost, estimatedcosthistory, actualcost,
                               actualcosthistory):
        """Assembles a cost chart for inclusion within a card on cost kanban board"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        epoch = datetime.datetime.utcnow()
        oldest_epoch = epoch
        for estimatedcost_document in estimatedcosthistory:
            if estimatedcost_document['datetime'] < oldest_epoch:
                oldest_epoch = estimatedcost_document['datetime']

        for actualcost_document in actualcosthistory:
            if actualcost_document['datetime'] < oldest_epoch:
                oldest_epoch = actualcost_document['datetime']

        number_of_days = int((epoch-oldest_epoch)/self.timedelta_day)
        day_count = 0
        day_labels = []
        day_values = []
        while day_count < number_of_days:
            day_count += 1
            past_epoch = oldest_epoch + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_display_format(past_epoch)
            day_labels.append(date_format)
            current_estimate = estimatedcost
            current_actual = actualcost
            for estimatedcost_document in estimatedcosthistory:
                if estimatedcost_document['datetime'].date() < past_epoch.date():
                    current_estimate = estimatedcost_document['estimatedcost']

            for actualcost_document in actualcosthistory:
                if actualcost_document['datetime'].date() < past_epoch.date():
                    current_actual = actualcost_document['actualcost']

            day_value = int(current_estimate - current_actual)
            day_values.append(day_value)

        chart = pygal.Line(x_label_rotation=90, style=LightStyle)
        chart.x_labels = day_labels
        chart.add('', day_values)
        chart.render_to_file(self.current_dir+os.sep+'svgcharts'+os.sep+username+'_'+str(int(epoch.timestamp()))+'.svg')
        return self.display_chart(username, epoch)

    def insert_card_time_chart(self, estimatedtime, estimatedtimehistory, actualtime,
                               actualtimehistory):
        """Assembles a time chart for inclusion within a card on time kanban board"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        epoch = datetime.datetime.utcnow()
        oldest_epoch = epoch
        for estimatedtime_document in estimatedtimehistory:
            if estimatedtime_document['datetime'] < oldest_epoch:
                oldest_epoch = estimatedtime_document['datetime']

        for actualtime_document in actualtimehistory:
            if actualtime_document['datetime'] < oldest_epoch:
                oldest_epoch = actualtime_document['datetime']

        number_of_days = int((epoch-oldest_epoch)/self.timedelta_day)
        day_count = 0
        day_labels = []
        day_values = []
        while day_count < number_of_days:
            day_count += 1
            past_epoch = oldest_epoch + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_display_format(past_epoch)
            day_labels.append(date_format)
            current_estimate = estimatedtime
            current_actual = actualtime
            for estimatedtime_document in estimatedtimehistory:
                if estimatedtime_document['datetime'].date() < past_epoch.date():
                    current_estimate = estimatedtime_document['estimatedtime']

            for actualtime_document in actualtimehistory:
                if actualtime_document['datetime'].date() < past_epoch.date():
                    current_actual = actualtime_document['actualtime']

            day_value = int(current_estimate - current_actual)
            day_values.append(day_value)

        chart = pygal.Line(x_label_rotation=90, style=LightStyle)
        chart.x_labels = day_labels
        chart.add('', day_values)
        chart.render_to_file(os.path.join(self.current_dir, '..', 'svgcharts', username+'_'+str(int(epoch.timestamp()))+'.svg'))
        return self.display_chart(username, epoch)

    def insert_card_title(self, session_document, title, parent, fontsize):
        """Inserts its title into a card"""
        content = []
        content.append('<p class="'+fontsize+'">')
        hierarchical_title = self.assemble_card_hierarchical_title(title, parent)
        if session_document and 'search' in session_document and session_document['search']:
            hierarchical_title = hierarchical_title.replace(session_document['search'],
                                                            '<b class="search">'+session_document['search']+'</b>')

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

    def insert_emotion_icons(self, emotion):
        emotion_icon = self.emotions[emotion]
        return '<sup class="emotion" title="'+emotion.capitalize()+'"><img src="/images/smileys/'+emotion_icon+'"></sup>'

    def insert_marquee(self, username):
        """Inserts the marquee"""
        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 = []
        content.append('<div class="marquee">')
        personal_marquee = []
        project = member_document.get('project', '')
        valid_recent_activities = []
        datetime_now = datetime.datetime.utcnow()
        for project_document in self.projects_collection.find({'project': project}):
            if 'announcements' in project_document and project_document['announcements']:
                for announcement_document in project_document['announcements']:
                    start_date = announcement_document.get('startdate', datetime.timedelta())
                    end_date   = announcement_document.get('enddate',   datetime.timedelta())
                    if start_date and end_date:
                        if start_date < datetime_now < end_date:
                            personal_marquee.append(announcement_document['announcement'])

            break

        _, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        owner_reviewer_search_criteria['broadcast'] = {'$exists': True, '$nin': ['', [], None]}
        for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
            personal_marquee.append('<a href="/cards/view_card?id='+card_document['id']+'">'+card_document['id']+'</a>: '+card_document['broadcast'])

        # TODO - Get the recent activities part of the marquee working again
        for (past_time, any_username, doc_id, mode) in self.recent_activities:
            if past_time+self.timedelta_day >= datetime_now:
                if any_username != username:
                    if self.cards_collection.find({"_id": ObjectId(doc_id),
                                                   'project': project
                                                  }).count():
                        recent_activity = self.assemble_recent_activity_entry(past_time,
                                                                              any_username, doc_id,
                                                                              mode)
                        if recent_activity:
                            personal_marquee.append(recent_activity)

                valid_recent_activities.append((past_time, any_username, doc_id, mode))

        self.recent_activities = valid_recent_activities
        for pm_no, personal_marquee_entry in enumerate(personal_marquee):
            content.append('<span class="fa fa-comment-o fa-lg"></span>&nbsp;'+personal_marquee_entry)
            if pm_no+1 < len(personal_marquee):
                content.append('&nbsp;' * 10)

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

    def insert_new_recent_days_old_message(self, card_document, state, epoch):
        """Comment"""
        content = []
        project_document = self.projects_collection.find_one({'project': card_document['project']})
        workflow_index = project_document.get('workflow_index', {})
        buffer_column_states = workflow_index.get('buffer_column_states', [])
        # TODO - This function has a confusing name!
        if state in card_document and card_document[state] and card_document[state] > epoch - self.timedelta_day:
            content.append('<sup class="new" title="Newly arrived in this state">New!</sup>')
        elif ('lastchanged' in card_document and card_document['lastchanged'] and
              card_document['lastchanged'] > epoch - self.timedelta_day):
            content.append('<sup class="new">Updated!</sup>')
        elif (state == 'backlog' or state in buffer_column_states) and state in card_document and card_document[state]:
            start_epoch = card_document[state]
            days_in_state = int((epoch - start_epoch)/self.timedelta_day)
            if days_in_state:
                if days_in_state == 1:
                    content.append(f'<sup class="days_in_state">{days_in_state} Day</sup>')
                else:
                    content.append(f'<sup class="days_in_state">{days_in_state} Days</sup>')

        return "".join(content)

    def insert_page_title_and_online_help(self, session_document, page, title):
        content = []
        content.append('<h2 class="pagetitle">')
        if page in self.fonticons and self.fonticons[page]:
            content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

        content.append(title)
        help_status = session_document.get('help', '')
        help_found = False
        if help_status == 'enabled':
            help_path = os.path.join(self.current_dir, '..', 'onlinehelp', page+'.html')
            help_found = os.path.exists(help_path)
            if help_found:
                content.append(' <sup class="help" title="Online help is available for this page">Help</sup>')

        content.append('</h2>')
        if help_found:
            with open(help_path, "r") as handle:
                helptext = handle.read()
                content.append('<div class="onlinehelp">')
                content.append(helptext)
                content.append('</div>')

        return "".join(content)

    def isfloat(self, value):
        try:
            float(value)
            return True
        except ValueError:
            return False

    def json_type_handler(self, value):
        if isinstance(value, datetime.datetime):
            return value.isoformat()
        elif isinstance(value, bson.objectid.ObjectId):
            return str(value)

        raise TypeError('Unknown type')

    def key_value_pair_as_json(self, mode, number_of_keys, key_count, key, value, indent):
        """comment"""
        content = []
        indent_string = ""
        if mode == 'html':
            indent_string = '&nbsp;' * indent
            content.append(indent_string + '"<b class="key">'+key+'</b>": ')
        elif mode == 'file':
            indent_string = ' ' * indent
            content.append(indent_string + '"'+key+'":')

        if isinstance(value, dict):
            if mode == 'html':
                content.append(f'<b class="dictionary">{value}</b>')
            elif mode == 'file':
                content.append(str(value))

        elif isinstance(value, float):
            if mode == 'html':
                content.append(f'<b class="float">{value}</b>')
            elif mode == 'file':
                content.append(str(value))

        elif isinstance(value, int):
            if mode == 'html':
                content.append(f'<b class="integer">{value}</b>')
            elif mode == 'file':
                content.append(str(value))

        elif isinstance(value, list):
            if mode == 'html':
                content.append('[<br>')
            elif mode == 'file':
                content.append('[\n')

            for i, val in enumerate(value):
                if isinstance(val, dict):
                    content.append(self.dictionary_as_json(mode, val, indent+2))
                else:
                    if mode == 'html':
                        content.append(indent_string + '"<b class="string">'+str(val)+'</b>"')
                    elif mode == 'file':
                        content.append(indent_string + '"'+str(val)+'"')

                if i < len(value)-1:
                    content.append(',')

                if mode == 'html':
                    content.append('<br>')
                elif mode == 'file':
                    content.append('\n')

            content.append(indent_string + ']')
        else:
            try:
                if mode == 'html':
                    content.append(f'"<b class="string">{value}</b>"')
                elif mode == 'file':
                    content.append(f'"{value}"')

            except:
                if mode == 'html':
                    content.append('"<b class="string">!!!ERROR!!!</b>"')
                elif mode == 'file':
                    content.append('"!!!ERROR!!!"')

        if key_count < number_of_keys:
            content.append(',')

        if mode == 'html':
            content.append('<br>')
        elif mode == 'file':
            content.append('\n')

        return "".join(content)

    def menubar(self):
        """ 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)
        username = member_document.get('username', '')
        content.append('<div id="menubarcontainer">')
        content.append('<ul class="menubar">')
        content.append('<li>')
        content.append('<a href="/kanban"><span class="fa fa-binoculars fa-lg"></span>&nbsp;Visualisations</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Visualisations</h3></div>')

        content.append('<div class="megamenucolumn">')
        content.append('<div class="megamenu_heading">Kanban Board</div>')
        content.append('<ul>')
        day_labels = self.get_projection_day_labels()
        for day_no, day_label in enumerate(day_labels):
            content.append(f'<li><a class="submenu" href="/{self.get_page_component("index")}/index?projection={day_no}">')
            if 'index' in self.fonticons and self.fonticons['index']:
                content.append(f'<span class="fa fa-{self.fonticons["index"]} fa-lg"></span>&nbsp;')

            content.append(day_label)
            content.append('</a></li>')

        content.append('</ul></div>')

        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (entry, page) in [('List View', 'listview'), ('Standup', 'standup'), ('Dashboard', 'dashboard'),
                              ('Backlog Sorter', 'backlogsorter'), ('Team Members', 'teammembers'),
                              ('Timeline', 'timeline'), ('Roadmap', 'roadmap'),
                              ('Pair Programming', 'pair_programming')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append(f'<span class="fa fa-{self.fonticons[page]} fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul></div>')

        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (entry, page) in [('Activity Stream', 'activity_stream'),
                              ('Diary', 'diary'), ('Time Sheet', 'timesheet'), ('Wallboard', 'wallboard'),
                              ('Release Kick Off', 'releasekickoff'),
                              ('Retrospective', 'retrospective'), ('JSON View', 'jsonview')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append(f'<span class="fa fa-{self.fonticons[page]} fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul></div>')

        content.append('</div>')
        content.append('</li>')

        content.append('<li>')
        fonticon_code = '<span class="fa fa-object-group fa-lg"></span>&nbsp;'
        if 'project' in member_document and member_document['project']:
            project = member_document['project']
            content.append(f'<a href="/{self.get_page_component("update_project")}/update_project?project={member_document["project"]}">{fonticon_code}Projects</a>')
        else:
            project = ""
            content.append(f'<a href="#">{fonticon_code}Projects</a>')

        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Projects</h3></div>')

        content.append('<div class="megamenucolumn">')
        content.append(f'<div class="megamenu_heading">Project \'{project}\'</div>')
        content.append('<ul>')
        global_wips = {}
        if project:
            project_document = self.projects_collection.find_one({'project': project})
            if project_document:
                global_wips = project_document.get('global_wips', {})

            content.append(f'<li><a class="submenu" href="/{self.get_page_component("update_project")}/update_project?project={project}">{fonticon_code}Update</a></li>')
            project_card_count = self.cards_collection.find({'project': project}).count()
            if project_card_count:
                content.append(f'<li><a class="submenu" href="/{self.get_page_component("backup_project")}/backup_project?project={project}">{fonticon_code}Backup</a></li>')
                content.append(f'<li><i class="disabled" title="{project_card_count} cards are still associated with this project!">{fonticon_code}Restore</i></li>')
                content.append(f'<li><i class="disabled" title="{project_card_count} cards are still associated with this project!">{fonticon_code}Delete</i></li>')
            else:
                content.append(f'<li><i class="disabled" title="This project has no cards!">{fonticon_code}Backup</i></li>')
                content.append(f'<li><a class="submenu" href="/{self.get_page_component("restore_project")}/restore_project?project={project}">{fonticon_code}Restore</a></li>')
                content.append(f'<li><a class="submenu" href="/{self.get_page_component("delete_project")}/delete_project?project={project}">{fonticon_code}Delete</a></li>')

        content.append('</ul>')
        content.append('</div>')

        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("add_project")}/add_project">{fonticon_code}Add Project</a></li>')
        content.append('</ul>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("synchronise_member_projects")}/synchronise_member_projects">{fonticon_code}Synchronise</a></li>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("relink")}/relink">{fonticon_code}Relink</a></li>')
        content.append('</ul></div>')

        content.append('</div>')
        content.append('</li>')

        content.append('<li><a href="#"><span class="fa fa-sticky-note-o fa-lg"></span>&nbsp;Cards</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Cards</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (entry, page) in [('Add Epic', 'add_card?type=epic&project='+project),
                              ('Add Feature', 'add_card?type=feature&project='+project),
                              ('Add Story', 'add_card?type=story&project='+project),
                              ('Add Enhancement', 'add_card?type=enhancement&project='+project),
                              ('Add Defect', 'add_card?type=defect&project='+project),
                              ('Add Transient', 'add_card?type=transient&project='+project)]:

            if project:
                content.append(f'<li><a class="submenu" href="/{self.get_page_component("add_card")}/{page}">')
            else:
                content.append('<li><i class="disabled">')

            actual_page = page.split('?')[0]
            if actual_page in self.fonticons and self.fonticons[actual_page]:
                content.append('<span class="fa fa-'+self.fonticons[actual_page]+' fa-lg"></span>&nbsp;')

            if 'project' in member_document and member_document['project']:
                content.append(entry+'</a></li>')
            else:
                content.append(entry+'</i></li>')

        content.append('</ul></div>')
        content.append('<div class="megamenucolumn">')
        
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("routine_card_manager")}/routine_card_manager">{fonticon_code}Routine Card Manager</a></li>')
        content.append('</ul>')        
        
        content.append('<div class="megamenu_heading">Recent Cards</div>')
        content.append('<ul>')
        for recent_card_id in session_document['recent_cards']:
            if self.cards_collection.find({'id': recent_card_id}).count():
                title_attr = ""
                card_document = self.cards_collection.find_one({'id': recent_card_id})
                title = card_document.get('title', '')
                if title:
                    title_attr = ' title="'+title+'"'

                content.append(f'<li><a class="submenu" href="/{self.get_page_component("view_card")}/view_card?id={recent_card_id}"{title_attr}>{fonticon_code}{recent_card_id}</a></li>')

        content.append('</ul></div>')
        content.append('</div></li>')
        content.append('<li>')
        fonticon_code = '<span class="fa fa-bar-chart fa-lg"></span>&nbsp;'
        content.append('<a href="#">'+fonticon_code+'Metrics</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Metrics</h3></div>')
        halfway = int(len(self.metrics)/2)
        metrics_first = self.metrics[:halfway]
        metrics_second = self.metrics[halfway:]

        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("which_metric")}/which_metric">{fonticon_code}Which Metric</a></li>')
        content.append('</ul><hr class="divider"><ul>')
        for (page, textual, specific_project_search_criteria_entries, specific_card_search_criteria_entries) in metrics_first:
            card_valid = self.card_search_criteria_met_for_metric(specific_card_search_criteria_entries)
            project_valid = self.project_search_criteria_met_for_metric(specific_project_search_criteria_entries)
            if card_valid and project_valid:
                content.append(f'<li><a href="/{self.get_page_component(page)}/{page}">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(textual+'</a></li>')
            else:
                content.append('<li><i class="disabled">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(textual+'</i></li>')

        content.append('</ul></div>')

        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (page, textual, specific_project_search_criteria_entries, specific_card_search_criteria_entries) in metrics_second:
            card_valid = self.card_search_criteria_met_for_metric(specific_card_search_criteria_entries)
            project_valid = self.project_search_criteria_met_for_metric(specific_project_search_criteria_entries)
            if card_valid and project_valid:
                content.append(f'<li><a href="/{self.get_page_component(page)}/{page}">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(textual+'</a></li>')
            else:
                content.append('<li><i class="disabled">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(textual+'</i></li>')

        content.append('</ul></div>')
        content.append('</div></li>')
        content.append('<li><a href="#"><span class="fa fa-file-text-o fa-lg"></span>&nbsp;Reports</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Reports</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<div class="megamenu_heading">Built-In Reports</div>')
        content.append('<ul>')
        for (entry, page) in [('Costs Report', 'costs_report'), ('Status Report', 'statusreport'),
                              ('Times Report', 'times_report'), ('Escalation Report', 'escalation_report'),
                              ('Root-Cause Analysis Report', 'rootcauseanalysis_report'),
                              ('Backlog Trend', 'backlog_trend')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<div class="megamenu_heading">Custom Reports</div>')
        reports = member_document.get('reports', [])
        if reports:
            content.append('<ul>')
            for report_document in reports:
                content.append(f'<li><a href="/{self.get_page_component("report")}/report?reportname={report_document["reportname"]}">{report_document["reportname"]}</a></li>')

            content.append('</ul>')
        else:
            content.append('<p>There are no custom reports</p>')

        content.append('<hr class="divider"><ul>')
        for (entry, page) in [('Report Generator', 'report_generator'),
                              ('Report Manager', 'report_manager')
                             ]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul></div>')
        content.append('</div></li>')
        content.append('<li>')
        fonticon_code = '<span class="fa fa-filter fa-lg"></span>&nbsp;'
        content.append('<a href="#">'+fonticon_code+'Filters</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Filters</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("save_current_filter")}/save_current_filter">{fonticon_code}Save Current Filter</a></li>')
        content.append('</ul><hr class="divider"><ul>')
        if 'filters' in member_document:
            for saved_filter in member_document['filters']:
                content.append(f'<li><a class="submenu" href="/{self.get_page_component("reload_filter")}/reload_filter?filter={saved_filter["filtername"]}">{fonticon_code}Reload \'{saved_filter["filtername"]}\'</a></li>')

        content.append('</ul><hr class="divider"><ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("filter_manager")}/filter_manager">{fonticon_code}Filter Manager</a></li>')
        content.append('</ul></div></div></li>')
        content.append('<li><a href="#"><span class="fa fa-download fa-lg"></span>&nbsp;Export</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Export</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<div class="megamenu_heading">JSON</div>')
        content.append('<ul>')
        for (entry, page) in [('Session', 'session_as_json'), ('Member', 'member_as_json')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append(f'<span class="fa fa-{self.fonticons[page]} fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        if member_document and 'project' in member_document and member_document['project']:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component("project_as_json")}/project_as_json?project={member_document["project"]}">')
            if 'project_as_json' in self.fonticons and self.fonticons['project_as_json']:
                content.append('<span class="fa fa-'+self.fonticons['project_as_json']+' fa-lg"></span>&nbsp;')

            content.append('Project</a></li>')

        content.append(f'<li><a class="submenu" href="/{self.get_page_component("export_cards_as_json")}/export_cards_as_json" title="Export the cards of the currently selected project(s) in JSON format">')
        if 'export_cards_as_json' in self.fonticons and self.fonticons['export_cards_as_json']:
            content.append('<span class="fa fa-'+self.fonticons['export_cards_as_json']+' fa-lg"></span>&nbsp;')

        content.append('Cards</a></li>')
        content.append('</ul></div></div>')
        content.append('</li>')

        content.append('<li><a href="#"><span class="fa fa-user fa-lg"></span>&nbsp;Settings</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Settings</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (entry, page) in [('Personal WIP Limits', 'personal_work_in_progress_limits')]:
            if global_wips and 'enforcewiplimits' in global_wips and global_wips['enforcewiplimits']:
                content.append('<li><i class="disabled" title="Disabled due to global WIP limits being enforced">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(entry+'</i></li>')
            else:
                content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(entry+'</a></li>')

        for (entry, page) in [('Dashboard', 'dashboard_settings'), ('Filter Bar Components', 'filter_bar_components'),
                              ('Listview', 'listview_settings'), ('Themes', 'themes'), ('Absence', 'absence'), ('Avatar', 'avatar')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul></div></div></li>')

        content.append('<li><a href="#"><span class="fa fa-wrench fa-lg"></span>&nbsp;Admin</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Admin</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (entry, page) in [('Members', 'members'), ('Projects', 'projects'),
                              ('Sessions', 'sessions'), ('Tokens', 'tokens')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("theme_creator")}/theme_creator"><span class="fa fa-photo fa-lg"></span>&nbsp;Theme Creator</li>')
        content.append('</ul>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        content.append(f'<li><a class="submenu" href="/{self.get_page_component("errors_log")}/errors_log"><span class="fa fa-wrench fa-lg"></span>&nbsp;Errors Log</li>')
        content.append('</ul>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        for (entry, page) in [('Database Backup', 'database_backup'), ('Database Restore', 'database_restore'),
                              ('Database Delete', 'database_delete'), ('Database Rebuild', 'database_rebuild'),
                              ('Database Relink', 'database_relink')]:
            content.append(f'<li><a href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        for (entry, page) in [('JIRA CSV Import', 'jira_csv_import'),
                              ('Upgrade', 'upgrade')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        for (entry, page) in [('Libraries', 'libraries'), ('Licences', 'licences')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul>')

        content.append('</div>')

        content.append('</div></li>')

        content.append('<li><a href="#"><span class="fa fa-file-pdf-o fa-lg"></span>&nbsp;Documentation</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Docs</h3></div>')
        content.append('<div class="megamenucolumn">')
        content.append('<ul>')
        for (entry, format) in [('HTML', 'html'), ('EPUB', 'epub')]:
            content.append(f'<li><a class="submenu" href="/docs/build/{format}/index.html">')
            if format in self.fonticons and self.fonticons[format]:
                content.append('<span class="fa fa-'+self.fonticons[format]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul></div></div>')
        content.append('</li>')
        
        content.append('<li><a href="#"><span class="fa fa-search fa-lg"></span>&nbsp;Search</a>')
        content.append('<div class="megamenu">')
        content.append('<div class="megamenusidebar"><h3 class="vertical-text">Search</h3></div>')
        content.append('<div class="megamenucolumn">')

        content.append('<div id="searchcontainer"><form id="searchform" action="/search/search" method="post"> ')
        content.append('<select name="attribute">')
        content.append('<option value="">All</option>')
        selected_attribute = ""
        if session_document and 'attribute' in session_document and session_document["attribute"] != "":
            selected_attribute = session_document["attribute"]

        for attribute in self.searchable_attributes:
            if attribute == selected_attribute:
                content.append('<option value="'+attribute+'" selected>'+self.displayable_key(attribute)+'</option>')
            else:
                content.append('<option value="'+attribute+'">'+self.displayable_key(attribute)+'</option>')

        content.append('</select> ')

        if session_document and 'search' in session_document and session_document["search"] != "":
            content.append('<input id="search" type="text" name="search" value="'+session_document["search"]+'">')
        else:
            content.append('<input id="search" type="text" name="search" placeholder="Search">')
        content.append(' <button type="submit" value="Search"><span class="fa fa-search fa-lg"></span>&nbsp;Search</button></form></div>')
        content.append('<p><br><br><br></p>')
        content.append('<hr class="divider">')
        content.append('<ul>')
        for (entry, page) in [('Advanced Search', 'advanced_search')]:
            content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
            if page in self.fonticons and self.fonticons[page]:
                content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

            content.append(entry+'</a></li>')

        content.append('</ul>')        
        
        
        content.append('</div></div>')
        content.append('</li>')

        if os.path.exists(os.path.join(self.current_dir, '..', 'linting')):
            content.append('<li><a href="#"><span class="fa fa-file-pdf-o fa-lg"></span>&nbsp;KSF</a>')
            content.append('<div class="megamenu">')
            content.append('<div class="megamenusidebar"><h3 class="vertical-text">KSF</h3></div>')
            content.append('<div class="megamenucolumn">')
            content.append('<ul>')
            for (entry, page) in [('Linting', 'linting')]:
                content.append(f'<li><a class="submenu" href="/{self.get_page_component(page)}/{page}">')
                if page in self.fonticons and self.fonticons[page]:
                    content.append('<span class="fa fa-'+self.fonticons[page]+' fa-lg"></span>&nbsp;')

                content.append(entry+'</a></li>')

            content.append('</ul></div></div>')
            content.append('</li>')        
        
        content.append('</ul></div>')

        content.append(self.insert_marquee(username))
        return "".join(content)

    def metrics_settings(self, session_document, number_of_days, division):
        if number_of_days:
            number_of_days = int(number_of_days)
        elif 'metrics_number_of_days' in session_document and session_document['metrics_number_of_days']:
            number_of_days = session_document['metrics_number_of_days']
        else:
            number_of_days = 28

        if division:
            division = int(division)
        elif 'metrics_division' in session_document and session_document['metrics_division']:
            division = session_document['metrics_division']
        else:
            division = 1

        session_document['metrics_number_of_days'] = number_of_days
        session_document['metrics_division'] = division
        self.sessions_collection.save(session_document)
        return number_of_days, division

    @staticmethod
    def project_in_projects(project, projects):
        """Ascertain whether project is related to one of a list of project documents"""
        for project_document in projects:
            if 'project' in project_document and project_document['project'] == project:
                return True

        return False

    def project_search_criteria_met_for_metric(self, specific_project_search_criteria_entries):
        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, _, _ = self.get_member_project_release_iteration(member_document)
        search_criteria = {'project': project}
        for (attribute, value) in specific_project_search_criteria_entries:
            search_criteria[attribute] = value

        # TODO - NEED TO DEAL WITH BUDGET WITHIN RELEASE AND ITERATION SUBDOCUMENTS!!!!
        if self.projects_collection.find(search_criteria).count():
            return True

        return False

    def read_administrator_ini_file(self):
        """Reads the administrator.ini file and extracts username and password information"""
        with open(os.path.join(self.current_dir, '..', 'administrator.ini'), 'r') as handle:
            data = handle.read()
            for (setting, regex) in [
                    ('admin_username', r'(?i)username\s?=\s?(\S+)'),
                    ('admin_password', r'(?i)password\s?=\s?(\S+)'),
                    ('smtp_server_host', r'(?i)smtp_server_host\s?=\s?([\w\.]+)'),
                    ('smtp_server_password', r'(?i)smtp_server_password\s?=\s?([\w\.]+)'),
                    ('smtp_server_port', r'(?i)smtp_server_port\s?=\s?(\d+)'),
                    ('smtp_server_username', r'(?i)smtp_server_username\s?=\s?([\w\.]+)')]:
                pattern = re.compile(regex)
                results = pattern.findall(data)
                if results:
                    self.kanbanara_settings[setting] = results[0]

    def read_kanbanara_ini_file(self):
        """Reads the kanbanara.ini file and extracts settings"""
        for path in [os.path.join(self.current_dir, '..', 'kanbanara.ini'),
                     os.path.join(self.current_dir, 'kanbanara.ini'),
                    ]:
            if os.path.exists(path):
                with open(path, 'r') as handle:
                    data = handle.read()
                    for (setting, regex) in [
                            ('instance', r'(?i)instance\s?=\s?([\w\. ]+)')]:
                        pattern = re.compile(regex)
                        results = pattern.findall(data)
                        if results:
                            self.kanbanara_settings[setting] = results[0]
                            
                break

    def read_mongodb_ini_file(self):
        """Reads the common mongodb.ini file and extracts host, port,
           username, password and bindir information
        """
        mongodb_host = 'localhost'
        mongodb_port = 27017
        mongodb_username = ""
        mongodb_password = ""
        if os.name == 'nt':
            mongodb_bindir = r'C:\Program Files\MongoDB\bin'
        else:
            mongodb_bindir = '/usr/local/bin'

        if os.path.exists(os.path.join(self.current_dir, 'mongodb.ini')):
            path = os.path.join(self.current_dir, 'mongodb.ini')
        else:
            path = os.path.join(self.current_dir, '..', 'mongodb.ini')

        with open(path, 'r') as handle:
            settings = handle.read()
            mongodb_host_pattern = re.compile(r'(?i)host\s?=\s?(\S+)')
            results = mongodb_host_pattern.findall(settings)
            if results:
                mongodb_host = results[0]

            mongodb_port_pattern = re.compile(r'(?i)port\s?=\s?(\d+)')
            results = mongodb_port_pattern.findall(settings)
            if results:
                mongodb_port = int(results[0])

            mongodb_username_pattern = re.compile(r'(?i)username[ ]*=[ ]*(\w+)')
            results = mongodb_username_pattern.findall(settings)
            if results:
                mongodb_username = results[0]

            mongodb_password_pattern = re.compile(r'(?i)password[ ]*=[ ]*(\w+)')
            results = mongodb_password_pattern.findall(settings)
            if results:
                mongodb_password = results[0]

            mongodb_bindir_pattern = re.compile(r'(?i)bindir\s?=\s?([\S ]+)')
            results = mongodb_bindir_pattern.findall(settings)
            if results:
                mongodb_bindir = results[0]

        return mongodb_host, mongodb_port, mongodb_username, mongodb_password, mongodb_bindir

    def read_version_ini_file(self):
        """Reads the version.ini file and extracts versioning information"""
        major = 0
        minor = 0
        revision = 0
        build = 0
        date = ""
        with open(os.path.join(self.current_dir, 'version.ini'), 'r') as handle:
            settings = handle.read()
            major_pattern = re.compile(r'(?i)major\s?=\s?(\d+)')
            results = major_pattern.findall(settings)
            if results:
                major = int(results[0])
                
            minor_pattern = re.compile(r'(?i)minor\s?=\s?(\d+)')
            results = minor_pattern.findall(settings)
            if results:
                minor = int(results[0])

            revision_pattern = re.compile(r'(?i)revision\s?=\s?(\d+)')
            results = revision_pattern.findall(settings)
            if results:
                revision = int(results[0])

            build_pattern = re.compile(r'(?i)build\s?=\s?(\d+)')
            results = build_pattern.findall(settings)
            if results:
                build = int(results[0])

            date_pattern = re.compile(r'(?i)date\s?=\s?(\d{4}\-\d{2}\-\d{2})')
            results = date_pattern.findall(settings)
            if results:
                date = results[0]

        return major, minor, revision, build, date

    def save_card_as_json(self, document):
        # TODO - Reimplement as separate launchable subprocess
        json = self.dictionary_as_json('file', document, 0)
        project_folder = os.path.join(self.current_dir, '..', 'database', 'projects', document['project'])
        if not os.path.exists(project_folder):
            os.mkdir(project_folder)

        if not os.path.exists(project_folder+os.sep+'cards'):
            os.mkdir(project_folder+os.sep+'cards')

        filepath = project_folder+os.sep+'cards'+os.sep+document['id']+'.json'
        epoch = datetime.datetime.utcnow()
        while filepath in self.file_locks and self.file_locks[filepath] > epoch - self.timedelta_minute:
            True

        self.file_locks[filepath] = datetime.datetime.utcnow()
        try:
            with open(filepath, 'w') as handle:
                try:
                    handle.write(json)
                except:
                    True

                del self.file_locks[filepath]
                
        except PermissionError:
            True

    def save_member_as_json(self, member_document):
        # TODO - Reimplement as separate launchable subprocess
        if 'username' in member_document and member_document['username']:
            json = self.dictionary_as_json('file', member_document, 0)
            filepath = os.path.join(self.current_dir, '..', 'database', 'members', member_document['username']+'.json')
            epoch = datetime.datetime.utcnow()
            while filepath in self.file_locks and self.file_locks[filepath] > epoch - self.timedelta_minute:
                True

            self.file_locks[filepath] = datetime.datetime.utcnow()
            with open(filepath, 'w') as handle:
                handle.write(json)
                del self.file_locks[filepath]

    def save_project_as_json(self, document):
        # TODO - Reimplement as separate launchable subprocess
        if not os.path.exists(os.path.join(self.current_dir, '..', 'database', 'projects', document['project'])):
            os.mkdir(os.path.join(self.current_dir, '..', 'database', 'projects', document['project']))

        filepath = os.path.join(self.current_dir, '..', 'database', 'projects', document['project'], 'project.json')
        epoch = datetime.datetime.utcnow()
        while filepath in self.file_locks and self.file_locks[filepath] > epoch - self.timedelta_minute:
            True

        self.file_locks[filepath] = datetime.datetime.utcnow()
        try:
            with open(filepath, 'w') as handle:
                for project_document in self.projects_collection.find_one({'project': document['project']}):
                    json_dict = self.dictionary_as_json('file', project_document, 0)
                    handle.write(json_dict)
                    break

                del self.file_locks[filepath]
                
        except PermissionError:
            del self.file_locks[filepath]

    def show_category_in_top_right(self, project, category):
        content = []
        colour = ""
        project_document = self.projects_collection.find_one({'project': project})
        if 'categories' in project_document:
            for category_document in project_document['categories']:
                if category_document['category'] == category:
                    colour = category_document.get('colour', '')
                    break

        if category:
            content.append('<sup class="category"')
            if colour:
                content.append(' style="background-color:%s"' % colour)

            content.append('>'+category+'</sup>')

        return "".join(content)

    @cherrypy.expose
    def step_main_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)]
        main_column = step_document['maincolumn']
        state = main_column['state']
        centric = main_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}mainexpedite"><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, 'main', 'expedite')
        content.append(f'<script type="text/javascript" src="/kanban/scripts/autogenerated/swimlane-1step{step_no}mainexpedite.js"></script>')
        return "".join(content)
        
    @staticmethod
    def update_card_history(username, document, attribute, new_value):
        epoch = datetime.datetime.utcnow()
        history_document = {'datetime': epoch,'username': username}
        existing_value = document.get(attribute,'')
        if existing_value:
            if new_value:
                if existing_value != new_value:
                    history_document['mode']      = 'update'
                    history_document['attribute'] = attribute
                    history_document['value']     = new_value
                    return history_document

            else:
                history_document['mode']      = 'remove'
                history_document['attribute'] = attribute
                return history_document

        elif new_value:
            history_document['mode']      = 'add'
            history_document['attribute'] = attribute
            history_document['value']     = new_value
            return history_document

        return

    @staticmethod
    def username_in_members(username, members):
        """Ascertain whether username is related to one of a list of member documents"""
        for member_document in members:
            if 'username' in member_document and member_document['username'] == username:
                return True

        return False

    def validate_rule(self, project, components):
        '''Returns whether a rule is valid or not'''
        status = 'Valid'
        if len(components) < 8:
            return "Invalid"

        if len(components) % 4:
            return "Invalid"

        for component in components:
            if not component:
                return "Invalid"

        inside_condition_block      = False
        inside_success_action_block = False
        inside_failure_action_block = False
        for component_no in range(0, len(components), 4):
            if components[component_no] == 'if':
                if component_no == 0:
                    inside_condition_block = True
                else:
                    return "Invalid"

            elif components[component_no] == 'then':
                if inside_condition_block:
                    inside_condition_block = False
                    inside_success_action_block = True
                else:
                    return "Invalid"

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

            other_card_id = ""
            other_card_project = ""
            attribute = components[component_no+1]
            if '.' in attribute:
                [other_card_id, attribute] = attribute.split('.')
                if not self.cards_collection.find({'id': other_card_id}).count():
                    return "Invalid"

                [other_card_project, _] = other_card_id.split('-')
                if other_card_project != project:
                    return "Invalid"

            operand   = components[component_no+2]
            value     = components[component_no+3]

            if component_no == 0:
                if components[component_no] != 'if':
                    return "Invalid"

            elif components[component_no] not in ['and', 'or', 'then', 'else']:
                return "Invalid"

            if inside_condition_block:
                if attribute in self.rule_condition_allowable_card_attributes:
                    if operand in ['=', '!=', '>', '<', '>=', '<=']:
                        if value.startswith("'''") and value.endswith("'''"):
                            continue
                        elif value.startswith('"""') and value.endswith('"""'):
                            continue
                        elif value.startswith("'") and value.endswith("'"):
                            continue
                        elif value.startswith('"') and value.endswith('"'):
                            continue
                        elif value.isdigit():
                            continue
                        elif self.isfloat(value):
                            continue
                        else:
                            return "Invalid"

                    elif operand in ['is']:
                        if value in ['populated', 'unpopulated']:
                            continue
                        elif value.lower() in ['true', 'false', 'enabled', 'disabled']:
                            continue
                        else:
                            return "Invalid"

                    elif operand in ['in', 'nin']:
                        if value.startswith("'''") and value.endswith("'''"):
                            continue
                        elif value.startswith('"""') and value.endswith('"""'):
                            continue
                        elif value.startswith("'") and value.endswith("'"):
                            continue
                        elif value.startswith('"') and value.endswith('"'):
                            continue
                        elif value.startswith("[") and value.endswith("]"):
                            continue
                        else:
                            return "Invalid"

                    else:
                        return "Invalid"

                else:
                    return "Invalid"

            elif inside_success_action_block:
                if attribute not in self.rule_action_allowable_card_attributes:
                    return "Invalid"

                if operand not in ['=', 'is']:
                    return "Invalid"

            elif inside_failure_action_block:
                if attribute not in self.rule_action_allowable_card_attributes:
                    return "Invalid"

                if operand not in ['=', 'is']:
                    return "Invalid"

        return status
