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

import datetime
from decimal import Decimal
import logging
import os
from random import randint
import urllib.parse

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


class Cards(Kanbanara):

    @cherrypy.expose
    def add_child(self, parent, card_type, title, state):
        """Allows a child card to be added to a card via the view card page"""
        if all([parent, card_type, title, state]):
            username = Kanbanara.check_authentication(f'/{self.component}')
            parent_document = self.cards_collection.find_one({'id': parent})
            card_document = {'id': self.get_project_next_card_number(parent_document['project'], card_type),
                             'mode': 'dynamic',
                             'parent': parent,
                             'type': card_type,
                             'title': title,
                             'state': state,
                             'creator': username,
                             'lastchanged': datetime.datetime.utcnow(),
                             'lastchangedby': username}

            for attribute in ['coowner', 'coreviewer', 'crmcase', 'iteration', 'owner', 'priority',
                              'project', 'release', 'reviewer', 'severity']:
                if attribute in parent_document and parent_document[attribute]:
                    card_document[attribute] = parent_document[attribute]

            self.cards_collection.save(card_document)

        raise cherrypy.HTTPRedirect("/cards/view_card?id="+parent, 302)
        
    @cherrypy.expose
    def add_routine_card(self, project, title, state, priority, severity, interval):
        if project and title and interval:
            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()
            next_action = self.calculate_routine_card_next_action(epoch, interval)
            routine_document = {'project': project, 'title': title,
                                'state': state, 'priority': priority,
                                'severity': severity, 'interval': interval,
                                'last_action': 0, 'next_action': next_action}
            if 'routine_cards' in member_document:
                routine_cards = member_document['routine_cards']
                routine_cards.append(routine_document)
                member_document['routine_cards'] = routine_cards
            else:
                member_document['routine_cards'] = [routine_document]
                
            self.members_collection.save(member_document)

        raise cherrypy.HTTPRedirect("/cards/routine_card_manager", 302)

    def amalgamate_child_numerical_values(self, card_id, attribute):
        """Amalgamate estimated/actual cost/time from the children of a given card"""
        collated_child_values = []
        for child_card_document in self.cards_collection.find({'parent': card_id,
                                                               attribute: {'$exists': True,
                                                                           '$nin': [[], '', None]
                                                                          }
                                                              }):
            collated_child_values.append(child_card_document[attribute])

        return sum(collated_child_values)

    @cherrypy.expose
    def delete_routine_card(self, project, title):
        if project and title:
            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)
            modified_routine_cards = []
            routine_cards = member_document['routine_cards']
            for routine_card in routine_cards:
                if not (routine_card['project'] == project and routine_card['title'] == title):
                    modified_routine_cards.append(routine_card)

            member_document['routine_cards'] = modified_routine_cards
            self.members_collection.save(member_document)

        raise cherrypy.HTTPRedirect("/cards/routine_card_manager", 302)

    @cherrypy.expose
    def move_card_to_top(self, doc_id):
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            session_id = Kanbanara.cookie_handling(self)
            session_document = self.sessions_collection.find_one({"session_id": session_id})
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                     [card_document['state']])
            current_position = card_document.get('position', 0)
            owner_reviewer_search_criteria['type'] = card_document['type']
            owner_reviewer_search_criteria['position'] = {'$lt': current_position}
            positions = self.cards_collection.find(owner_reviewer_search_criteria).distinct('position')
            if positions:
                positions.sort()
                if positions[0] > 1:
                    new_position = int(positions[0] / 2)
                    card_document['position'] = new_position
                    self.cards_collection.save(card_document)
                    self.save_card_as_json(card_document)

            break

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

    @cherrypy.expose
    def move_card_up(self, doc_id):
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            session_id = Kanbanara.cookie_handling(self)
            session_document = self.sessions_collection.find_one({"session_id": session_id})
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                     [card_document['state']])
            current_position = card_document.get('position', 0)
            owner_reviewer_search_criteria['type'] = card_document['type']
            owner_reviewer_search_criteria['position'] = {'$lt': current_position}
            positions = self.cards_collection.find(owner_reviewer_search_criteria).distinct('position')
            if len(positions) > 1:
                positions.sort(reverse=True)
                distance = positions[0] - positions[1]
                new_position = int(positions[0] - (distance / 2))
                card_document['position'] = new_position
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

            break

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

    @cherrypy.expose
    def move_card_down(self, doc_id):
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            session_id = Kanbanara.cookie_handling(self)
            session_document = self.sessions_collection.find_one({"session_id": session_id})
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                     [card_document['state']])
            current_position = card_document.get('position', 0)
            owner_reviewer_search_criteria['type'] = card_document['type']
            owner_reviewer_search_criteria['position'] = {'$gt': current_position}
            positions = self.cards_collection.find(owner_reviewer_search_criteria).distinct('position')
            if len(positions) > 1:
                positions.sort()
                distance = positions[1] - positions[0]
                new_position = int(positions[0] + (distance / 2))
                card_document['position'] = new_position
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)

            break

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

    @cherrypy.expose
    def move_card_to_bottom(self, doc_id):
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            session_id = Kanbanara.cookie_handling(self)
            session_document = self.sessions_collection.find_one({"session_id": session_id})
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                     [card_document['state']])
            current_position = card_document.get('position', 0)
            owner_reviewer_search_criteria['type'] = card_document['type']
            owner_reviewer_search_criteria['position'] = {'$gt': current_position}
            positions = self.cards_collection.find(owner_reviewer_search_criteria).distinct('position')
            if positions:
                positions.sort(reverse=True)
                if positions[0] > 1:
                    new_position = int(positions[0] + (99999 - positions[0] / 2))
                    card_document['position'] = new_position
                    self.cards_collection.save(card_document)
                    self.save_card_as_json(card_document)

            break

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

    def adjust_card_state(self, card_document):
        """Automatically moves a card into preceding buffer state if its selected state has
           reached its WIP limit
        """
        project = card_document['project']
        current_state = card_document['state']
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        if current_state != condensed_column_states[-1]:
            main_column_states = workflow_index['main_column_states']
            counterpart_column_states = workflow_index['counterpart_column_states']
            condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
            metastate = self.get_corresponding_metastate(project_document, current_state)
            if metastate in main_column_states or metastate in counterpart_column_states:
                _, _, centric, preceding_state, _ = self.get_associated_state_information(current_state)
                owner_count, reviewer_count, _, max_wip_limit = self.get_document_count(current_state, [])
                if max_wip_limit != -1:
                    current_state_pos = condensed_column_states_dict[current_state]
                    if current_state_pos > 0:
                        buffer_column_states = workflow_index.get('buffer_column_states', [])
                        if preceding_state in buffer_column_states:
                            if centric in ['Owner', 'Co-Owner'] and owner_count >= max_wip_limit:
                                card_document['state'] = preceding_state
                            elif centric == 'Reviewer' and reviewer_count >= max_wip_limit:
                                card_document['state'] = preceding_state

        return card_document

    @cherrypy.expose
    def card_as_json(self, doc_id):
        """Displays a card in JSON format"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if member_document and 'projects' in member_document and member_document["projects"]:
            card_document = self.cards_collection.find_one({'_id': ObjectId(doc_id),
                                                            'project': {'$exists': True}})
            if card_document and self.project_in_projects(card_document['project'],
                                                          member_document["projects"]):
                content = []
                content.append(Kanbanara.header(self, "card_as_json","Card As JSON"))
                content.append(Kanbanara.filter_bar(self, 'card_as_json'))
                content.append(Kanbanara.menubar(self))
                content.append(self.insert_page_title_and_online_help(session_document,
                                                                      "card_as_json",
                                                                      "Card As JSON"))
                content.append('<p class="json">')
                content.append(self.dictionary_as_json('html', card_document, 0))
                content.append('</p>')
                content.append(Kanbanara.footer(self))
                return "".join(content)
            else:
                raise cherrypy.HTTPRedirect("/kanban/index", 302)

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

    @cherrypy.expose
    def card_internals(self, doc_id):
        """Allows all the attributes of an individual card to be viewed"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        content = []
        content.append(Kanbanara.header(self, 'card_internals', "Card Internals"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        coowner, owner = self.get_card_attribute_values(card_document, ['coowner', 'owner'])
        content.append(self.insert_page_title_and_online_help(session_document, 'card_internals',
                                                              "Card Internals"))
        content.append('<table class="sortable"><tr><td>')
        buttons = self.ascertain_card_menu_items(card_document, member_document)
        content.append(self.assemble_card_menu(member_document, card_document, buttons, 'index'))
        content.append('</td><th>Internal</th><th>Data Type</th><th>External</th></tr>')
        for key, value in sorted(card_document.items()):
            content.append('<tr><th valign="top">'+key+'</th>')
            content.append('<td>%s</td>' % value)
            content.append('<td>'+type(value).__name__+'</td><td>')
            if key in ['status']:
                modified_status = self.format_multiline(value)
                content.append(modified_status)
            elif key in ['blockeduntil', 'deadline', 'deferreduntil', 'hiddenuntil',
                         'lastchanged', 'nextaction']:
                content.append(self.convert_time_to_display_format(value))
            elif key == 'externalhyperlink':
                content.append('<a href="'+value+'">'+value+'</a>')
            elif key == 'comments':
                content.append('<table>')
                for comment_document in value:
                    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>')
                    content.append('<sup class="'+comment_class+'">'+comment_document['username']+' on '+date_format+'</sup>')
                    content.append('</th></tr>')
                    modified_comment = self.format_multiline(comment_document['comment'])
                    content.append('<tr><td><p class="'+comment_class+'">'+modified_comment+'</p></td></tr>')

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

        content.append('</table>')
        content.append('</div>')
        content.append('<script type="text/javascript" src="/scripts/kanban.js"></script>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    @cherrypy.expose
    def delete_card(self, doc_id, destination=""):
        """Allows a Kanban card to be deleted"""
        content = []
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content.append(Kanbanara.header(self, "delete_card","Delete Card"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, "delete_card", "Delete Card"))
        content.append('<table width="50%"><tr><td>')
        content.append(self.assemble_kanban_card(['owner', 'coowner'], ['display'], -1, doc_id, False, 0))
        content.append('</td></tr>')
        count = self.count_descendents(doc_id, 0)
        if count:
            content.append('<tr><td>')
            content.append(f'<p class="warning">Warning: {count} descendent cards will also be deleted!</p>')
            content.append('</td></tr>')

        content.append('<tr><td><form action="/cards/submit_delete_card" method="post">')
        content.append(f'<input type="hidden" name="doc_id" value="{doc_id}">')
        content.append(f'<input type="hidden" name="destination" value="{destination}">')
        content.append('<input type="submit" value="Confirm Card Deletion"></form>')
        content.append('<input type="button" value="Cancel" onclick="window.location=\'/kanban\'" />')
        content.append('</td></tr></table></div>')
        content.append('<script type="text/javascript" src="/scripts/kanban.js"></script>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    @cherrypy.expose
    def expedite(self, doc_id):
        """Expedites the given card"""
        Kanbanara.check_authentication(f'/{self.component}')
        Kanbanara.cookie_handling(self)
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            card_document['expedite'] = True
            self.cards_collection.save(card_document)
            break

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

    @staticmethod
    def extract_hashtags(attribute):
        """Extracts all hashtags (words prepended by #) from a given string"""
        hashtags = [word for word in attribute.split(' ') if word.startswith('#')]
        return hashtags

    @cherrypy.expose
    def give_focus(self, doc_id):
        """Gives focus to a particular card"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            card_document['focusby']    = username
            card_document['focusstart'] = datetime.datetime.utcnow()
            self.cards_collection.save(card_document)
            break

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

    @cherrypy.expose
    def inline_edit(self, docidattr, value):
        """Called when a card attribute is edited inline"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        (doc_id, attribute) = docidattr.split(':::')
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            value = value.replace('&lt;', '<')
            value = value.replace('&gt;', '>')
            value = value.replace('\n', ' ')
            value = value.strip()
            card_document[attribute] = value
            card_document['lastchanged'] = datetime.datetime.utcnow()
            card_document['lastchangedby'] = username
            card_document['lasttouched'] = datetime.datetime.utcnow()
            card_document['lasttouchedby'] = username
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)
            break

        return self.modify_for_display(value)

    @staticmethod
    def modify_for_display(value):
        """Converts an inline edited string back to its displayable HTML form"""
        value = value.replace('<', '&lt;')
        value = value.replace('>', '&gt;')
        return value

    @cherrypy.expose
    def push(self, doc_id):
        """Allows a card to be pushed to the next state"""
        Kanbanara.check_authentication(f'/{self.component}')
        Kanbanara.cookie_handling(self)
        for document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            current_state = document['state']
            project_document = self.projects_collection.find_one({'project': document['project']})
            current_metastate = self.get_corresponding_metastate(project_document, current_state)
            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_dict = workflow_index.get('condensed_column_states_dict', {})
            current_state_pos = condensed_column_states_dict[current_state]
            if uncondensed_column_states[current_state_pos+1] in buffer_column_states:
                buffer_state = uncondensed_column_states[current_state_pos+1]
                next_state = uncondensed_column_states[current_state_pos+2]
            else:
                next_state = uncondensed_column_states[current_state_pos+1]

            if current_metastate in ["backlog", "analysis"]:
                owner_count, _, _, max_wip_limit = self.get_document_count(next_state, [])
                if (max_wip_limit == -1) or (owner_count < max_wip_limit):
                    document['state'] = next_state
                    # OPERATION DYNAMIC WORKFLOW
                    document[next_state] = datetime.datetime.utcnow()
                else:
                    document['state'] = buffer_state
                    # OPERATION DYNAMIC WORKFLOW
                    document[buffer_state] = datetime.datetime.utcnow()

            elif current_metastate == "development":
                _, reviewer_count, _, max_wip_limit = self.get_document_count(next_state, [])
                if (max_wip_limit == -1) or (reviewer_count < max_wip_limit):
                    document['state'] = next_state
                    # OPERATION DYNAMIC WORKFLOW
                    document[next_state] = datetime.datetime.utcnow()
                else:
                    document['state'] = buffer_state
                    # OPERATION DYNAMIC WORKFLOW
                    document[buffer_state] = datetime.datetime.utcnow()

            elif current_metastate in ['unittesting', 'integrationtesting',
                                       'systemtesting', 'acceptancetesting']:
                owner_count, _, _, max_wip_limit = self.get_document_count(next_state, [])
                if (max_wip_limit == -1) or (owner_count < max_wip_limit):
                    document['state'] = next_state
                    # OPERATION DYNAMIC WORKFLOW
                    document[next_state] = datetime.datetime.utcnow()

            elif current_metastate == "acceptancetestingaccepted":
                document['state'] = next_state
                # OPERATION DYNAMIC WORKFLOW
                document[next_state] = datetime.datetime.utcnow()

            testcases = document.get('testcases', [])
            next_metastate = self.get_corresponding_metastate(project_document, document['state'])
            if (next_metastate in ['acceptancetestingaccepted', 'completed', 'closed'] and
                    not self.all_testcases_accepted(testcases)):
                document['state'] = current_state
                # OPERATION DYNAMIC WORKFLOW
                document['testing'] = datetime.datetime.utcnow()
            elif next_metastate == 'closed':
                for attribute in ['nextaction', 'position', 'reopened']:
                    if attribute in document:
                        del document[attribute]

            self.cards_collection.save(document)
            break

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

    @cherrypy.expose
    def unblock_card(self, doc_id):
        """Renders a blocked card unblocked on the kanban board"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            if 'blocked' in card_document:
                del card_document['blocked']

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

            if 'blockedhistory' in card_document:
                blockedhistory = card_document['blockedhistory']
            else:
                blockedhistory = []

            blockedhistory.append({'action': 'unblocked',
                                   'datetime': datetime.datetime.utcnow(),
                                   'username': username})
            card_document['blockedhistory'] = blockedhistory
            self.cards_collection.save(card_document)
            break

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

    @cherrypy.expose
    def unhide_card(self, doc_id):
        """Returns a hidden card back to being shown"""
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            if 'hiddenuntil' in card_document:
                del card_document['hiddenuntil']

            self.cards_collection.save(card_document)
            break

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

    @cherrypy.expose
    def reopen_card(self, doc_id):
        """Reopens a closed card and places it back in an earlier state"""
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            for metastate in ['defined', 'analysis', 'design', 'development', 'backlog', 'triaged']:
                custom_states = self.get_custom_states_mapped_onto_metastates([metastate])
                if custom_states:
                    card_document['state'] = custom_states[0]
                    card_document['reopened'] = True
                    if 'resolution' in card_document:
                        del card_document['resolution']

                    self.cards_collection.save(card_document)
                    raise cherrypy.HTTPRedirect("/kanban/index", 302)

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

    @cherrypy.expose
    def recurring(self, doc_id):
        """Sets the recurring flag on a card allowing it to be cloned upon closure"""
        Kanbanara.check_authentication(f'/{self.component}')
        Kanbanara.cookie_handling(self)
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            card_document['recurring'] = True
            self.cards_collection.save(card_document)
            break

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

    @cherrypy.expose
    def submit_delete_card(self, doc_id, destination=""):
        """Allows the delete card form to be submitted to force a card and
           its descendents to be deleted
        """
        username = Kanbanara.check_authentication(f'/{self.component}')
        self.delete_document_and_descendents(username, doc_id)
        if destination:
            raise cherrypy.HTTPRedirect("/kanban/"+destination, 302)
        else:
            raise cherrypy.HTTPRedirect("/kanban/index", 302)

    @cherrypy.expose
    def take_focus(self, doc_id):
        """Takes focus away from a currently-focused card"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        focusby = card_document.get('focusby', '')
        focusstart = card_document.get('focusstart', 0)
        if focusby and focusstart and focusby == username:
            focushistory_document = {'focusby': focusby,
                                     'focusstart': focusstart,
                                     'focusend': datetime.datetime.utcnow()}

            if 'focushistory' in card_document:
                card_document['focushistory'].append(focushistory_document)
            else:
                card_document['focushistory'] = [focushistory_document]

            for attribute in ['focusby', 'focusstart']:
                if attribute in card_document:
                    del card_document[attribute]

            self.cards_collection.save(card_document)

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

    @cherrypy.expose
    def touch(self, doc_id):
        """Called when a user wishes to update the last touched status of a card"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        Kanbanara.cookie_handling(self)
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id)}):
            card_document['lasttouched']   = datetime.datetime.utcnow()
            card_document['lasttouchedby'] = username
            self.cards_collection.save(card_document)
            break

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

    @cherrypy.expose
    def unexpedite(self, doc_id):
        """Allows an expedited card to be unexpedited"""
        Kanbanara.check_authentication(f'/{self.component}')
        Kanbanara.cookie_handling(self)
        for card_document in self.cards_collection.find({"_id": ObjectId(doc_id),
                                                         'expedite': {'$exists': True}}):
            del card_document['expedite']
            self.cards_collection.save(card_document)
            break

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

    def update_recent_cards(self, session_document, card_id):
        """Updates the Recent Cards menu on the menubar with the latest ten cards viewed"""
        recent_cards = session_document.get('recent_cards', [])
        modified_recent_cards = [recent_card for recent_card in recent_cards
                                 if self.cards_collection.find({'id': recent_card}).count()]
        if card_id not in modified_recent_cards:
            recent_cards = [card_id] + modified_recent_cards

        session_document['recent_cards'] = recent_cards[:10]
        self.sessions_collection.save(session_document)

    @cherrypy.expose
    def vote(self, doc_id):
        """Allows a user to vote for a particular card"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        Kanbanara.cookie_handling(self)
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        if 'votes' in card_document:
            if not username in card_document['votes']:
                card_document['votes'].append(username)

        else:
            card_document['votes'] = [username]

        for attribute in ['lastchanged', 'lasttouched']:
            card_document[attribute] = datetime.datetime.utcnow()

        for attribute in ['lastchangedby', 'lasttouchedby']:
            card_document[attribute] = username

        self.cards_collection.save(card_document)
        raise cherrypy.HTTPRedirect("/kanban/index", 302)

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

        Kanbanara.__init__(self)

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

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

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

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

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

        # 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 'sessions' collection
        self.sessions_collection = kanbanara_db['sessions']
        for attribute in ['session_id']:
            self.sessions_collection.create_index(attribute, unique=True, background=True)

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

        # Connect to 'members' collection
        self.members_collection = 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 'cards' collection
        self.cards_collection = kanbanara_db['cards']

        self.recent_activities = []

        self.resolutions = ['Abandoned', 'Pending Release', 'Released', "Won't fix"]

    @cherrypy.expose
    def submit_add_card(self, doc_id="", actualcost=0, actualcostexplanation="", actualtime="",
                        actualtimeexplanation="", affectsversion="", after="", artifacts="",
                        before="", blocked="", blockeduntil="", blocksparent=False, broadcast="",
                        bypassreview=False, category="", classofservice="", comment="", coowner="",
                        coreviewer="", creator="", crmcase="", customer="", deadline="",
                        deferred="", deferreduntil="", dependsupon="", description="",
                        destination="", difficulty="", emotion="", escalation="", estimatedcost=0,
                        estimatedcostexplanation="", estimatedtime=0, estimatedtimeexplanation="",
                        externalhyperlink="", externalreference="", fixversion="", help=False,
                        hiddenuntil="", id="",iteration="", kanbanboard="", mode="", nextaction="",
                        notes="", owner="", parent="", priority="", project="", question="",
                        recurring="", release="", resolution="", reviewer="", rootcauseanalysis="",
                        rule0="", ruleusage0="", severity="", startby="", state="", states="",
                        status="", stuck="", subteam="", tags="", testcasetitle0="",
                        testcasedescription0="", testcasestate0="", title="", type="", user="", **kwargs):
        """Allows the add card page form to be submitted"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        epoch = datetime.datetime.utcnow()

        expedite = False
        lastchanged = epoch
        lastchangedby = username
        lasttouched = epoch
        lasttouchedby = username

        focushistory = []
        focusby = ""
        focusstart = ""

        hierarchy = ""
        reassigncoowner = ""
        reassigncoreviewer = ""
        reassignowner = ""
        reassignreviewer = ""

        votes = []
        history = []
        for (attribute, value) in [('affectsversion', affectsversion), ('after', after),
                                   ('before', before), ('blocked', blocked),
                                   ('blockeduntil', blockeduntil), ('blocksparent', blocksparent),
                                   ('bypassreview', bypassreview), ('category', category),
                                   ('coowner', coowner), ('coreviewer', coreviewer),
                                   ('crmcase', crmcase), ('customer', customer),
                                   ('deadline', deadline), ('deferred', deferred),
                                   ('deferreduntil', deferreduntil), ('dependsupon', dependsupon),
                                   ('description', description), ('difficulty', difficulty),
                                   ('emotion', emotion), ('escalation', escalation),
                                   ('externalhyperlink', externalhyperlink),
                                   ('externalreference', externalreference),
                                   ('fixversion', fixversion), ('hiddenuntil', hiddenuntil),
                                   ('iteration', iteration), ('nextaction', nextaction),
                                   ('notes', notes), ('owner', owner), ('parent', parent),
                                   ('priority', priority), ('question', question),
                                   ('reassigncoowner', reassigncoowner),
                                   ('reassigncoreviewer', reassigncoreviewer),
                                   ('reassignowner', reassignowner),
                                   ('reassignreviewer', reassignreviewer), ('recurring', recurring),
                                   ('release', release), ('resolution', resolution),
                                   ('reviewer', reviewer), ('rootcauseanalysis', rootcauseanalysis),
                                   ('severity', severity), ('classofservice', classofservice),
                                   ('startby', startby), ('state', state), ('status', status),
                                   ('stuck', stuck), ('title', title)]:
            history_document = self.update_card_history(username, {}, attribute, value)
            if history_document:
                history.append(history_document)

        ownerstate      = ""
        coownerstate    = ""
        reviewerstate   = ""
        coreviewerstate = ""

        rules = []
        if rule0:
            components, rule_status = self.parse_rule(project, rule0)
            rules = [{'rule': rule0, 'components': components, 'usage': ruleusage0,
                      'status': rule_status}]

        if artifacts:
            artifacts = artifacts.replace('\r\n', ':::')
            artifacts = artifacts.replace('\r',   ':::')
            artifacts = artifacts.replace('\n',   ':::')
            artifacts = artifacts.split(':::')

        testcases = []
        if all([testcasetitle0, testcasedescription0, testcasestate0]):
            testcases = [{'title': testcasetitle0, 'description': testcasedescription0,
                          'state': testcasestate0}]
        elif testcasetitle0 and testcasestate0:
            testcases = [{'title': testcasetitle0, 'state': testcasestate0}]

        if startby:
            startby = self.dashed_date_to_datetime_object(startby)

        if nextaction:
            nextaction = self.dashed_date_to_datetime_object(nextaction)

        if deadline:
            deadline = self.dashed_date_to_datetime_object(deadline)

        id = self.get_project_next_card_number(project, type)

        comments = []
        if comment:
            comments = [{'comment': comment, 'datetime': epoch, 'username': username}]

        blockedhistory = []

        centric = ""
        if username == owner:
            centric = 'owner'
        elif username == coowner:
            centric = 'coowner'
        elif username == reviewer:
            centric = 'reviewer'
        elif username == coreviewer:
            centric = 'coreviewer'

        estimatedcosthistory = []
        if estimatedcost:
            estimatedcosthistory_entry = {'datetime': epoch, 'estimatedcost': float(estimatedcost),
                                          'username': username, 'centric': centric}
            if estimatedcostexplanation:
                estimatedcosthistory_entry['explanation'] = estimatedcostexplanation

            estimatedcosthistory = [estimatedcosthistory_entry]

        actualcosthistory = []
        if actualcost:
            actualcosthistory_entry = {'datetime': epoch, 'actualcost': float(actualcost),
                                       'username': username, 'centric': centric}
            if actualcostexplanation:
                actualcosthistory_entry['explanation'] = actualcostexplanation

            actualcosthistory = [actualcosthistory_entry]

        estimatedtimehistory = []
        if estimatedtime:
            estimatedtimehistory_entry = {'datetime': epoch, 'estimatedtime': float(estimatedtime),
                                          'username': username, 'centric': centric}
            if estimatedtimeexplanation:
                estimatedtimehistory_entry['explanation'] = estimatedtimeexplanation

            estimatedtimehistory = [estimatedtimehistory_entry]

        actualtimehistory = []
        if actualtime:
            actualtimehistory_entry = {'datetime': epoch, 'actualtime': float(actualtime),
                                       'username': username, 'centric': centric}
            if actualtimeexplanation:
                actualtimehistory_entry['explanation'] = actualtimeexplanation

            actualtimehistory = [actualtimehistory_entry]

        hashtags = self.extract_hashtags(description)

        statehistory = []
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
        for potential_missed_out_state in uncondensed_column_states:
            if potential_missed_out_state == state:
                statehistory.append({'datetime': epoch, 'state': state, 'username': username})
                break
            else:
                statehistory.append({'datetime': epoch, 'state': potential_missed_out_state, 'username': username})

        document = {}
        if doc_id:
            document['_id'] = ObjectId(doc_id)

        if actualcost:
            document['actualcost'] = float(actualcost)

        if actualcosthistory:
            document['actualcosthistory'] = actualcosthistory

        if actualtime:
            document['actualtime'] = float(actualtime)

        if actualtimehistory:
            document['actualtimehistory'] = actualtimehistory

        if affectsversion:
            document['affectsversion'] = affectsversion

        if after:
            document['after'] = after

        if artifacts:
            document['artifacts'] = artifacts

        if before:
            document['before'] = before

        if blocked:
            document['blocked'] = blocked

        if blockedhistory:
            document['blockedhistory'] = blockedhistory

        if blockeduntil:
            document['blockeduntil'] = blockeduntil

        if blocksparent:
            document['blocksparent'] = True

        if broadcast:
            document['broadcast'] = broadcast

        if bypassreview:
            document['bypassreview'] = True

        if category:
            document['category'] = category

        if classofservice:
            document['classofservice'] = classofservice

        if coowner:
            document['coowner'] = coowner

        if comments:
            document['comments'] = comments

        if coreviewer:
            document['coreviewer'] = coreviewer

        if creator:
            document['creator'] = creator

        if crmcase:
            document['crmcase'] = crmcase

        if customer:
            document['customer'] = customer

        if deadline:
            document['deadline'] = deadline

        if deferred:
            document['deferred'] = deferred

        if deferreduntil:
            document['deferreduntil'] = deferreduntil

        if dependsupon:
            document['dependsupon'] = dependsupon

        if description:
            document['description'] = description

        if difficulty:
            document['difficulty'] = difficulty

        if emotion:
            document['emotion'] = emotion

        if escalation:
            document['escalation'] = escalation

        if estimatedcost:
            document['estimatedcost'] = float(estimatedcost)

        if estimatedcosthistory:
            document['estimatedcosthistory'] = estimatedcosthistory

        if estimatedtime:
            document['estimatedtime'] = float(estimatedtime)

        if estimatedtimehistory:
            document['estimatedtimehistory'] = estimatedtimehistory

        if expedite:
            document['expedite'] = True

        if externalhyperlink:
            document['externalhyperlink'] = externalhyperlink

        if externalreference:
            document['externalreference'] = externalreference

        if fixversion:
            document['fixversion'] = fixversion

        if focusby:
            document['focusby'] = focusby

        if focushistory:
            document['focushistory'] = focushistory

        if focusstart:
            document['focusstart'] = focusstart

        if hashtags:
            document['hashtags'] = hashtags

        if hiddenuntil:
            document['hiddenuntil'] = hiddenuntil

        if hierarchy:
            document['hierarchy'] = hierarchy

        if id:
            document['id'] = id

        if iteration:
            document['iteration'] = iteration

        if lastchanged:
            document['lastchanged'] = lastchanged

        if lastchangedby:
            document['lastchangedby'] = lastchangedby

        if lasttouched:
            document['lasttouched'] = lasttouched

        if lasttouchedby:
            document['lasttouchedby'] = lasttouchedby

        if nextaction:
            document['nextaction'] = nextaction

        if notes:
            document['notes'] = notes

        if owner:
            document['owner'] = owner

        if parent:
            document['parent'] = parent

        document['position'] = self.get_random_position()

        if priority:
            document['priority'] = priority

        if project:
            document['project'] = project

        if question:
            document['question'] = question

        if reassigncoowner:
            document['reassigncoowner'] = reassigncoowner

        if reassigncoreviewer:
            document['reassigncoreviewer'] = reassigncoreviewer

        if reassignowner:
            document['reassignowner'] = reassignowner

        if reassignreviewer:
            document['reassignreviewer'] = reassignreviewer

        if recurring:
            document['recurring'] = recurring

        if release:
            document['release'] = release

        if resolution:
            document['resolution'] = resolution

        if reviewer:
            document['reviewer'] = reviewer

        if rootcauseanalysis:
            document['rootcauseanalysis'] = rootcauseanalysis

        if rules:
            document['rules'] = rules

        if severity:
            document['severity'] = severity

        if startby:
            document['startby'] = startby

        if state:
            document['state'] = state

        if statehistory:
            document['statehistory'] = statehistory

        if status:
            document['status'] = status

        if stuck:
            document['stuck'] = stuck

        if subteam:
            document['subteam'] = subteam

        if tags:
            document['tags'] = tags

        if testcases:
            document['testcases'] = testcases

        if title:
            document['title'] = title

        if type:
            document['type'] = type

        if votes:
            document['votes'] = votes
            
        if 'customattributes' in project_document and project_document['customattributes']:
            for custom_attribute, _ in project_document['customattributes'].items():
                if custom_attribute in kwargs:
                    document[custom_attribute] = kwargs[custom_attribute]
                else:
                    document[custom_attribute] = ""

        document = self.adjust_card_state(document)
        if 'id' in document and document['id']:
            document['mode'] = 'dynamic'
            document['history'] = history

            if parent:
                parent_hierarchy = self.assemble_card_hierarchy(parent)
                hierarchy = parent_hierarchy+' '+id
            else:
                hierarchy = id

            document['hierarchy'] = hierarchy

            doc_id = self.cards_collection.insert(document)
            if doc_id:
                self.add_recent_activity_entry((datetime.datetime.utcnow(), username,
                                                doc_id, 'added'))

        self.save_card_as_json(document)
        self.update_recent_cards(session_document, id)
        raise cherrypy.HTTPRedirect("/kanban/"+destination, 302)

    def get_random_position(self):
        position = randint(1000, 98999)
        return position

    def reassemble_card_hierarchies(self, old_parent, parent):
        for card_document in self.cards_collection.find():
            hierarchy = card_document.get('hierarchy', '')
            if old_parent in hierarchy or parent in hierarchy:
                card_document['hierarchy'] = self.assemble_card_hierarchy(card_document['id'])
                self.cards_collection.save(card_document)
                self.save_card_as_json(card_document)
                
    @cherrypy.expose
    def routine_card_manager(self):
        """Allows routine cards to be managed"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        project_document = self.projects_collection.find_one({'project': project})
        if project_document:
            workflow_index = project_document.get('workflow_index', {})
        else:
            workflow_index = {}        

        content = []
        content.append(Kanbanara.header(self, 'routine_card_manager', "Routine Card Manager"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'routine_card_manager',
                                                              "Routine Card Manager"))
        attributes_and_headings = [('title', 'Title'),
                                   ('state', 'Start State'),
                                   ('priority', 'Priority'),
                                   ('severity', 'Severity'),
                                   ('interval', 'Interval'),
                                   ('last_action', 'Last Action'),
                                   ('next_action', 'Next Action')]
        content.append('<table class="sortable"><thead><tr>')
        for (attribute, heading) in attributes_and_headings:
            content.append('<th><span>'+heading+'</span></th>')
        
        content.append('<td></td></tr></thead><tbody>')
        if 'routine_cards' in member_document:
            for routine_card in member_document['routine_cards']:
                if routine_card['project'] == project:
                    content.append('<tr>')
                    for (attribute, heading) in attributes_and_headings:
                        if attribute in routine_card:
                            if attribute in ['last_action', 'next_action']:
                                if attribute == 'last_action' and not routine_card[attribute]:
                                    content.append('<td>Never</td>')
                                elif attribute == 'next_action' and not routine_card[attribute]:
                                    content.append('<td>Failed to Calculate!</td>')
                                else:
                                    displayable_date = self.convert_time_to_display_format(routine_card[attribute])
                                    content.append(f'<td>{displayable_date}</td>')

                            else:
                                content.append('<td>'+routine_card[attribute]+'</td>')

                        else:
                            content.append('<td></td>')           
                
                    content.append('<td><form action="/cards/delete_routine_card" method="post">')
                    content.append(f'<input type="hidden" name="project" value="{project}">')
                    content.append(f'<input type="hidden" name="title" value="{routine_card["title"]}">')
                    content.append('<input type="submit" value="Delete"></form></td></tr>')
            
        content.append(f'<form action="/cards/add_routine_card" method="post"><input type="hidden" name="project" value="{project}"><tr><td><input type="text" name="title"></td><td><select name="state">')
        uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
        closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
        for uncondensed_column_state in uncondensed_column_states:
            if uncondensed_column_state and uncondensed_column_state not in closed_states:
                content.append(f'<option value="{uncondensed_column_state}">{uncondensed_column_state}</option>')
        
        content.append('</select></td><td>')
        content.append(self.create_html_select_block('priority', self.priorities,
                                                     current_value='medium',
                                                     specials=['capitalise']))
        content.append('</td><td>')
        content.append(self.create_html_select_block('severity', self.severities,
                                                     current_value='medium',
                                                     specials=['capitalise']))
        content.append('</td><td><select name="interval">')
        for potential_interval in ['Daily',                     # 365
                                   'Weekdays Only',             # 260
                                   'Weekend Days Only',         # 104
                                   'Twice Weekly',              # 104
                                   'Weekly',                    # 52
                                   'Every Monday',              # 52
                                   'Every Tuesday',             # 52
                                   'Every Wednesday',           # 52
                                   'Every Thursday',            # 52
                                   'Every Friday',              # 52
                                   'Every Saturday',            # 52
                                   'Every Sunday',              # 52
                                   'Twice Monthly',             # 24
                                   'Monthly',                   # 12
                                   '1st Day of Month',          # 12
                                   '2nd Day of Month',          # 12
                                   '3rd Day of Month',          # 12
                                   '4th Day of Month',          # 12
                                   '5th Day of Month',          # 12
                                   '6th Day of Month',          # 12
                                   '7th Day of Month',          # 12
                                   '8th Day of Month',          # 12
                                   '9th Day of Month',          # 12
                                   '10th Day of Month',         # 12
                                   '11th Day of Month',         # 12
                                   '12th Day of Month',         # 12
                                   '13th Day of Month',         # 12
                                   '14th Day of Month',         # 12
                                   '15th Day of Month',         # 12
                                   '16th Day of Month',         # 12
                                   '17th Day of Month',         # 12
                                   '18th Day of Month',         # 12
                                   '19th Day of Month',         # 12
                                   '20th Day of Month',         # 12
                                   '21st Day of Month',         # 12
                                   '22nd Day of Month',         # 12
                                   '23rd Day of Month',         # 12
                                   '24th Day of Month',         # 12
                                   '25th Day of Month',         # 12
                                   '26th Day of Month',         # 12
                                   '27th Day of Month',         # 12
                                   '28th Day of Month',         # 12
                                   'First Monday in Month',     # 12
                                   'First Tuesday in Month',    # 12
                                   'First Wednesday in Month',  # 12
                                   'First Thursday in Month',   # 12
                                   'First Friday in Month',     # 12
                                   'First Saturday in Month',   # 12
                                   'First Sunday in Month',     # 12                               
                                   'Second Monday in Month',    # 12
                                   'Second Tuesday in Month',   # 12
                                   'Second Wednesday in Month', # 12
                                   'Second Thursday in Month',  # 12
                                   'Second Friday in Month',    # 12
                                   'Second Saturday in Month',  # 12
                                   'Second Sunday in Month',    # 12
                                   'Third Monday in Month',     # 12
                                   'Third Tuesday in Month',    # 12
                                   'Third Wednesday in Month',  # 12
                                   'Third Thursday in Month',   # 12
                                   'Third Friday in Month',     # 12
                                   'Third Saturday in Month',   # 12
                                   'Third Sunday in Month',     # 12 
                                   'Fourth Monday in Month',    # 12
                                   'Fourth Tuesday in Month',   # 12
                                   'Fourth Wednesday in Month', # 12
                                   'Fourth Thursday in Month',  # 12
                                   'Fourth Friday in Month',    # 12
                                   'Fourth Saturday in Month',  # 12
                                   'Fourth Sunday in Month',    # 12                                   
                                   'Quarterly',                 # 4
                                   'Half Yearly',               # 2
                                   'Yearly'                     # 1
                                  ]:
            content.append(f'<option value="{potential_interval}">{potential_interval}</option>')

        content.append('</select></td><td>-</td><td>-</td><td><input type="submit" value="Add"></td></tr></form>')
        content.append('</tbody></table>')
        content.append('<script type="text/javascript" src="/scripts/listview.js"></script>')
        content.append('</div>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    @cherrypy.expose
    def submit_update_card(self, doc_id="", actualcost=0, actualcostexplanation="", actualtime=0,
                           actualtimeexplanation="", affectsversion="", after="", artifacts="",
                           before="", blocked="", blockeduntil="", blocksparent=False, broadcast="",
                           bypassreview=False, category="", classofservice="", comment="",
                           coowner="", coreviewer="", creator="", crmcase="", customer="",
                           deadline="", deferred="", deferreduntil="", dependsupon="",
                           description="", destination="", difficulty="", emotion="", escalation="",
                           estimatedcost=0, estimatedcostexplanation="", estimatedtime=0,
                           estimatedtimeexplanation="", externalhyperlink="", externalreference="",
                           fixversion="", help=False, hiddenuntil="", id="", iteration="",
                           kanbanboard="", mode="", nextaction="", notes="", owner="", parent="",
                           priority="", project="", question="", reassigncoowner="",
                           reassigncoreviewer="", reassignowner="", reassignreviewer="",
                           recurring="", release="", resolution="", reviewer="",
                           rootcauseanalysis="", rule0="", ruleusage0="", rule1="", ruleusage1="",
                           rule2="", ruleusage2="", rule3="", ruleusage3="", rule4="",
                           ruleusage4="", rule5="", ruleusage5="", rule6="", ruleusage6="",
                           rule7="", ruleusage7="", rule8="", ruleusage8="", rule9="",
                           ruleusage9="", severity="", startby="", state="", states="", status="",
                           stuck="", subteam="", tags="", testcasetitle0="",
                           testcasedescription0="", testcasestate0="", testcasetitle1="",
                           testcasedescription1="", testcasestate1="", testcasetitle2="",
                           testcasedescription2="", testcasestate2="", testcasetitle3="",
                           testcasedescription3="", testcasestate3="", testcasetitle4="",
                           testcasedescription4="", testcasestate4="", testcasetitle5="",
                           testcasedescription5="", testcasestate5="", testcasetitle6="",
                           testcasedescription6="", testcasestate6="", testcasetitle7="",
                           testcasedescription7="", testcasestate7="", testcasetitle8="",
                           testcasedescription8="", testcasestate8="", testcasetitle9="",
                           testcasedescription9="", testcasestate9="", title="", type="", user="", **kwargs):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        project_document = self.projects_collection.find_one({'project': project})
        metastate = self.get_corresponding_metastate(project_document, state)
        epoch = datetime.datetime.utcnow()
        document = self.cards_collection.find_one({'_id': ObjectId(doc_id)})
        history = document.get('history', [])
        for (attribute, value) in [('affectsversion', affectsversion), ('after', after),
                                   ('before', before), ('blocked', blocked),
                                   ('blockeduntil', blockeduntil), ('blocksparent', blocksparent),
                                   ('bypassreview', bypassreview), ('category', category),
                                   ('coowner', coowner), ('coreviewer', coreviewer),
                                   ('crmcase', crmcase), ('customer', customer),
                                   ('deadline', deadline), ('deferred', deferred),
                                   ('deferreduntil', deferreduntil), ('dependsupon', dependsupon),
                                   ('description', description), ('difficulty', difficulty),
                                   ('emotion', emotion), ('escalation', escalation),
                                   ('externalhyperlink', externalhyperlink),
                                   ('externalreference', externalreference),
                                   ('fixversion', fixversion), ('hiddenuntil', hiddenuntil),
                                   ('iteration', iteration), ('nextaction', nextaction),
                                   ('notes', notes), ('owner', owner), ('parent', parent),
                                   ('priority', priority), ('question', question),
                                   ('reassigncoowner', reassigncoowner),
                                   ('reassigncoreviewer', reassigncoreviewer),
                                   ('reassignowner', reassignowner),
                                   ('reassignreviewer', reassignreviewer), ('recurring', recurring),
                                   ('release', release), ('resolution', resolution),
                                   ('reviewer', reviewer), ('rootcauseanalysis', rootcauseanalysis),
                                   ('severity', severity), ('classofservice', classofservice),
                                   ('startby', startby), ('state', state), ('status', status),
                                   ('stuck', stuck), ('title', title), ('subteam', subteam)]:
            history_document = self.update_card_history(username, document, attribute, value)
            if history_document:
                history.append(history_document)

        rules = []
        for (rule, usage) in [(rule0, ruleusage0),
                              (rule1, ruleusage1),
                              (rule2, ruleusage2),
                              (rule3, ruleusage3),
                              (rule4, ruleusage4),
                              (rule5, ruleusage5),
                              (rule6, ruleusage6),
                              (rule7, ruleusage7),
                              (rule8, ruleusage8),
                              (rule9, ruleusage9)]:
            if rule:
                components, rule_status = self.parse_rule(project, rule)
                rules.append({'rule': rule, 'components': components,
                              'usage': usage, 'status': rule_status})

        document['rules'] = rules

        testcases = []
        for (tc_title, tc_description, tc_state) in [(testcasetitle0, testcasedescription0, testcasestate0),
                                                     (testcasetitle1, testcasedescription1, testcasestate1),
                                                     (testcasetitle2, testcasedescription2, testcasestate2),
                                                     (testcasetitle3, testcasedescription3, testcasestate3),
                                                     (testcasetitle4, testcasedescription4, testcasestate4),
                                                     (testcasetitle5, testcasedescription5, testcasestate5),
                                                     (testcasetitle6, testcasedescription6, testcasestate6),
                                                     (testcasetitle7, testcasedescription7, testcasestate7),
                                                     (testcasetitle8, testcasedescription8, testcasestate8),
                                                     (testcasetitle9, testcasedescription9, testcasestate9)]:
            if all([tc_title, tc_description, tc_state]):
                testcases.append({'title': tc_title, 'description': tc_description,
                                  'state': tc_state})
            elif tc_title and tc_state:
                testcases.append({'title': tc_title, 'state': tc_state})

        document['testcases'] = testcases
        statehistory = document.get('statehistory', [])
        blockedhistory = document.get('blockedhistory', [])

        #Artifacts
        document['artifacts'] = []
        if artifacts:
            artifacts = artifacts.replace('\r\n', ':::')
            artifacts = artifacts.replace('\r',   ':::')
            artifacts = artifacts.replace('\n',   ':::')
            document['artifacts'] = artifacts.split(':::')

        # Comments
        comments = document.get('comments', [])
        if comment:
            comments.append({'comment': comment, 'datetime': epoch, 'username': username})

        document['comments'] = comments

        # Update Blocked and BlockedHistory
        if blocked:
            document['blocked'] = blocked
            if blockedhistory:
                latestblockedhistory_document = blockedhistory[-1]
                if latestblockedhistory_document['action'] != 'blocked':
                    blockedhistory.append({'action': 'blocked', 'reason': blocked,
                                           'datetime': datetime.datetime.utcnow(),
                                           'username': username})

            else:
                blockedhistory = [{'action': 'blocked', 'reason': blocked,
                                   'datetime': datetime.datetime.utcnow(), 'username': username}]

        else:
            if blockedhistory:
                latestblockedhistory_document = blockedhistory[-1]
                if latestblockedhistory_document['action'] == 'blocked':
                    blockedhistory.append({'action': 'unblocked',
                                           'datetime': datetime.datetime.utcnow(),
                                           'username': username})

            document['blocked'] = ""

        document['blockedhistory'] = blockedhistory

        # Update String-Based Key/Value Pairs
        for (key, value) in [('after', after), ('before', before), ('category', category),
                             ('coowner', coowner), ('coreviewer', coreviewer), ('crmcase', crmcase),
                             ('customer', customer), ('deferred', deferred),
                             ('dependsupon', dependsupon), ('description', description),
                             ('affectsversion', affectsversion), ('fixversion', fixversion),
                             ('externalhyperlink', externalhyperlink),
                             ('externalreference', externalreference), ('escalation', escalation),
                             ('iteration', iteration), ('notes', notes), ('owner', owner),
                             ('severity', severity), ('priority', priority), ('parent', parent),
                             ('project', project), ('question', question), ('recurring', recurring),
                             ('release', release), ('resolution', resolution),
                             ('reviewer', reviewer), ('status', status), ('title', title),
                             ('difficulty', difficulty), ('emotion', emotion), ('stuck', stuck),
                             ('rootcauseanalysis', rootcauseanalysis), ('broadcast', broadcast),
                             ('tags', tags), ('reassigncoowner', reassigncoowner),
                             ('reassigncoreviewer', reassigncoreviewer),
                             ('reassignowner', reassignowner),
                             ('reassignreviewer', reassignreviewer),
                             ('classofservice', classofservice), ('subteam', subteam)]:
            if value:
                document[key] = value
            elif key in document:
                document[key] = ""

        # Update Float-Based Key/Value Pairs
        for (key, value) in [('actualcost', actualcost), ('actualtime', actualtime),
                             ('estimatedcost', estimatedcost), ('estimatedtime', estimatedtime)]:
            if value:
                document[key] = float(value)
            elif key in document:
                document[key] = 0

        # Update Boolean-Based Key/Value Pairs
        for (key, value) in [('blocksparent', blocksparent), ('bypassreview', bypassreview)]:
            if value:
                document[key] = True
            elif key in document:
                document[key] = False

        # Update Date-Related Key/Value Pairs
        for (key, value) in [('startby', startby), ('deadline', deadline),
                             ('nextaction', nextaction), ('blockeduntil', blockeduntil),
                             ('deferreduntil', deferreduntil), ('hiddenuntil', hiddenuntil)]:
            if value:
                document[key] = self.dashed_date_to_datetime_object(value)
            elif key in document:
                document[key] = 0

        if state:
            original_state = document.get('state', state)
            _, _, centric, _, _ = self.get_associated_state_information(original_state)
            if centric == 'Owner':
                if owner and coowner:
                    if username == owner:
                        document['ownerstate'] = state
                        state = original_state
                    elif username == coowner:
                        document['coownerstate'] = state
                        state = original_state

            elif centric == 'Reviewer':
                if reviewer and coreviewer:
                    if username == reviewer:
                        document['reviewerstate'] = state
                        state = original_state
                    elif username == coreviewer:
                        document['coreviewerstate'] = state
                        state = original_state

            if not owner and 'ownerstate' in document and document['ownerstate']:
                del document['ownerstate']

            if not coowner and 'coownerstate' in document and document['coownerstate']:
                del document['coownerstate']

            if not reviewer and 'reviewerstate' in document and document['reviewerstate']:
                del document['reviewerstate']

            if not coreviewer and 'coreviewerstate' in document and document['coreviewerstate']:
                del document['coreviewerstate']

            if bypassreview:
                if metastate == 'unittesting':
                    state = 'unittestingaccepted'
                elif metastate == 'integrationtesting':
                    state = 'integrationtestingaccepted'
                elif metastate == 'systemtesting':
                    state = 'systemtestingaccepted'
                elif metastate == 'acceptancetesting':
                    state = 'acceptancetestingaccepted'

            if 'state' in document and document['state'] != state:
                if metastate in ['unittestingaccepted', 'integrationtestingaccepted',
                                 'systemtestingaccepted', 'acceptancetestingaccepted', 'completed',
                                 'closed'] and not self.all_testcases_accepted(testcases):
                    _, _, _, preceding_state, _ = self.get_associated_state_information(state)
                    state = preceding_state

                project_document = self.projects_collection.find_one({'project': project})
                workflow_index = project_document.get('workflow_index', {})
                uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
                for potential_missed_out_state in uncondensed_column_states:
                    found = False
                    if potential_missed_out_state == state:
                        break

                    for statehistory_document in statehistory:
                        if statehistory_document['state'] == potential_missed_out_state:
                            found = True

                    if not found:
                        statehistory.append({'datetime': epoch,
                                             'state': potential_missed_out_state,
                                             'username': username})

                statehistory.append({'datetime': epoch, 'state': state, 'username': username})
                document['statehistory'] = statehistory

            document['state'] = state
        else:
            if 'state' not in document:
                for metastate in ['untriaged', 'triaged', 'backlog', 'defined', 'analysis',
                                  'design', 'development']:
                    custom_states = self.get_custom_states_mapped_onto_metastates([metastate])
                    if custom_states:
                        document['state'] = custom_states[0]
                        statehistory.append({'datetime': epoch,
                                             'state': custom_states[0],
                                             'username': username})
                        document['statehistory'] = statehistory
                        break

        document = self.adjust_card_state(document)

        centric = ""
        if username == owner:
            centric = 'owner'
        if username == coowner:
            centric = 'coowner'
        if username == reviewer:
            centric = 'reviewer'
        if username == coreviewer:
            centric = 'coreviewer'

        actualcosthistory = document.get('actualcosthistory', [])
        if actualcost:
            if actualcosthistory:
                latest_actualcosthistory_document = actualcosthistory[-1]
                if latest_actualcosthistory_document['actualcost'] != float(actualcost) or actualcostexplanation:
                    actualcosthistory_entry = {'actualcost': float(actualcost), 'centric': centric,
                                               'datetime': epoch, 'username': username}
                    if actualcostexplanation:
                        actualcosthistory_entry['explanation'] = actualcostexplanation

                    document['actualcosthistory'] = actualcosthistory.append(actualcosthistory_entry)

            else:
                actualcosthistory_entry = {'actualcost': float(actualcost), 'centric': centric,
                                           'datetime': epoch, 'username': username}
                if actualcostexplanation:
                    actualcosthistory_entry['explanation'] = actualcostexplanation

                document['actualcosthistory'] = actualcosthistory.append(actualcosthistory_entry)

        actualtimehistory = document.get('actualtimehistory', [])
        if actualtime:
            if actualtimehistory:
                latest_actualtimehistory_document = actualtimehistory[-1]
                if latest_actualtimehistory_document['actualtime'] != float(actualtime) or actualtimeexplanation:
                    actualtimehistory_entry = {'actualtime': float(actualtime), 'centric': centric,
                                           'datetime': epoch, 'username': username}
                    if actualtimeexplanation:
                        actualtimehistory_entry['explanation'] = actualtimeexplanation

                    document['actualtimehistory'] = actualtimehistory.append(actualtimehistory_entry)

            else:
                actualtimehistory_entry = {'actualtime': float(actualtime), 'centric': centric,
                                           'datetime': epoch, 'username': username}
                if actualtimeexplanation:
                    actualtimehistory_entry['explanation'] = actualtimeexplanation

                document['actualtimehistory'] = actualtimehistory.append(actualtimehistory_entry)

        estimatedcosthistory = document.get('estimatedcosthistory', [])
        if estimatedcost:
            if estimatedcosthistory:
                latest_estimatedcosthistory_document = estimatedcosthistory[-1]
                if latest_estimatedcosthistory_document['estimatedcost'] != float(estimatedcost) or estimatedcostexplanation:
                    estimatedcosthistory_entry = {'centric': centric, 'estimatedcost': float(estimatedcost),
                                                  'datetime': epoch, 'username': username}
                    if estimatedcostexplanation:
                        estimatedcosthistory_entry['explanation'] = estimatedcostexplanation

                    document['estimatedcosthistory'] = estimatedcosthistory.append(estimatedcosthistory_entry)

            else:
                estimatedcosthistory_entry = {'centric': centric, 'estimatedcost': float(estimatedcost),
                                              'datetime': epoch, 'username': username}
                if estimatedcostexplanation:
                    estimatedcosthistory_entry['explanation'] = estimatedcostexplanation

                document['estimatedcosthistory'] = estimatedcosthistory.append(estimatedcosthistory_entry)

        estimatedtimehistory = document.get('estimatedtimehistory', [])
        if estimatedtime:
            if estimatedtimehistory:
                latest_estimatedtimehistory_document = estimatedtimehistory[-1]
                if latest_estimatedtimehistory_document['estimatedtime'] != float(estimatedtime) or estimatedtimeexplanation:
                    estimatedtimehistory_entry = {'centric': centric, 'estimatedtime': float(estimatedtime),
                                                  'datetime': epoch, 'username': username}
                    if estimatedtimeexplanation:
                        estimatedtimehistory_entry['explanation'] = estimatedtimeexplanation

                    document['estimatedtimehistory'] = estimatedtimehistory.append(estimatedtimehistory_entry)

            else:
                estimatedtimehistory_entry = {'centric': centric, 'estimatedtime': float(estimatedtime),
                                              'datetime': epoch, 'username': username}
                if estimatedtimeexplanation:
                    estimatedtimehistory_entry['explanation'] = estimatedtimeexplanation

                document['estimatedtimehistory'] = estimatedtimehistory.append(estimatedtimehistory_entry)

        hashtags = self.extract_hashtags(description)
        if hashtags:
            document['hashtags'] = hashtags
        elif 'hashtags' in document:
            del document['hashtags']

        document['lastchanged']   = datetime.datetime.utcnow()
        document['lastchangedby'] = username
        document['lasttouched']   = datetime.datetime.utcnow()
        document['lasttouchedby'] = username
        closed_states = self.get_custom_states_mapped_onto_metastates(['closed'])
        if state in closed_states:
            if 'nextaction' in document and document['nextaction']:
                del document['nextaction']

            if 'reopened' in document:
                del document['reopened']

            if 'expedite' in document:
                del document['expedite']

            if 'blocked' in document:
                del document['blocked']
                if 'blockeduntil' in document:
                    del document['blockeduntil']

            if 'deferred' in document:
                del document['deferred']
                if 'deferreduntil' in document:
                    del document['deferreduntil']

            if 'hiddenuntil' in document:
                del document['hiddenuntil']

        if metastate in ['acceptancetestingaccepted', 'completed', 'closed'] and 'blocksparent' in document:
            del document['blocksparent']

        document['history'] = history

        old_parent = document.get('parent', '')
        if old_parent != parent:
            self.reassemble_card_hierarchies(old_parent, parent)

        if parent:
            parent_hierarchy = self.assemble_card_hierarchy(parent)
            hierarchy = parent_hierarchy+' '+id
        else:
            hierarchy = id

        document['hierarchy'] = hierarchy

        if 'position' not in document:
            document['position'] = self.get_random_position()
            
        if 'customattributes' in project_document and project_document['customattributes']:
            for custom_attribute, _ in project_document['customattributes'].items():
                if custom_attribute in kwargs:
                    document[custom_attribute] = kwargs[custom_attribute]
                else:
                    document[custom_attribute] = ""

        self.cards_collection.save(document)
        self.add_recent_activity_entry((datetime.datetime.utcnow(), username, doc_id, 'updated'))
        self.save_card_as_json(document)
        self.update_recent_cards(session_document, id)
        raise cherrypy.HTTPRedirect("/kanban/"+destination, 302)

    @staticmethod
    def assemble_url_parameters(parameters):
        # TODO - ISN'T THERE A PYTHON FUNCTION TO DO THIS?
        urlparameters = ""
        if parameters:
            urlparameters = '?'
            for i, (parameter, value) in enumerate(parameters):
                urlparameters += parameter + '=' + value
                if i < len(parameters)-1:
                    urlparameters += '&'

        return urlparameters

    @cherrypy.expose
    def add_card(self, type, doc_id='', id='', project='', release='', iteration='', crmcase="",
                 escalation="", externalreference="", externalhyperlink="", nextaction="",
                 deadline="", startby="", classofservice="", subteam="", deferred="",
                 deferreduntil=""):
        """ Presents a form to allow a new card to be added """
        # TODO - project attribute as passed in must always be populated
        epoch = datetime.datetime.utcnow()
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        content = []
        parent = ""
        parent_document = None
        page_title = ""
        if type == 'substory':
            page_title = "Add SubStory"
            type = 'story'
        elif type == 'subtask':
            page_title = "Add SubTask"
            type = 'task'
        else:
            page_title = "Add "+type.capitalize()

        urlparameters = self.assemble_url_parameters([('type', type)])
        content.append(Kanbanara.header(self, 'add_card'+urlparameters, page_title))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<table width="100%" border="0"><tr><td valign="top">')

        if id:
            content.append('<h2 class="page_title">Parent</h2>')
            content.append('<table width="50%"><tr><td>')
            content.append(self.assemble_kanban_card(['owner', 'coowner'], ['display'],
                                                     -1, doc_id, False, 0))
            content.append('</td></tr></table>')
            parent = id
            parent_document = self.cards_collection.find_one({'id': parent})
            content.append('</td><td valign="top">')

        content.append(self.insert_page_title_and_online_help(session_document, 'add_card',
                                                              page_title))

        content.append('<div class="ui-state-error">')
        content.append('<ul></ul>')
        content.append('</div>')

        content.append('<form id="addrecordform" action="/cards/submit_add_card" method="post">')
        content.append('<input type="hidden" name="type" value="'+type+'">')
        if parent:
            content.append('<input type="hidden" name="parent" value="'+parent+'">')

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

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

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

        content.append('<table class="update'+type+'">')

        content.append('<tr><td colspan="4" align="center"><span class="controlgroup"><input class="save" type="submit" value="Save"><input class="save" type="reset"></form><input class="save" type="button" value="Cancel" onclick="window.location=\'/kanban\'" /></span></td></tr>')

        id_distincts = self.cards_collection.find({"id": {"$exists": True, "$ne": ""}}).distinct('id')

        # TODO - This needs to default to the project set in the filter
        project_distincts = self.get_member_projects(member_document)

        # Title and Description
        title_distincts_search_criteria = {'title': {"$exists": True, "$ne": ""},
                                           'state': {"$ne": "closed"}}
        if project:
            title_distincts_search_criteria['project'] = project
        else:
            title_distincts_search_criteria['project'] = {'$in': project_distincts}

        if type:
            title_distincts_search_criteria['type'] = type

        title_distincts = self.cards_collection.find(title_distincts_search_criteria).distinct('title')
        content.append('<tr><th title="Mandatory field">Title*</th><td><input type="text" id="title" name="title" size="40" list="titlelist"><datalist id="titlelist">')
        for titleDistinct in title_distincts:
            content.append('<option value="'+titleDistinct+'">'+titleDistinct+'</option>')

        content.append('</datalist></td>')
        description_distincts = self.cards_collection.find({"description": {"$exists": True,
                                                                            "$ne":""
                                                                           }
                                                           }).distinct('description')
        content.append('<th>Description</th><td><input type="text" name="description" size="80" list="descriptionlist"><datalist id="descriptionlist">')
        for description_distinct in description_distincts:
            content.append('<option value="'+description_distinct+'">'+description_distinct+'</option>')

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

        # Status
        content.append('<tr><th>Status</th><td colspan="3"><textarea name="status" rows="5" cols="80"></textarea></td></tr>')

        # Notes
        content.append('<tr><th>Notes</th><td colspan="3"><textarea name="notes" rows="5" cols="80"></textarea></td></tr>')

        # Get Creator/Owner Distincts
        fullnames_and_usernames = self.get_project_members(project_distincts)
        busy_order_user_distincts = self.get_busy_order_user_distincts(fullnames_and_usernames,
                                                                       project, release, iteration)

        # Creator and Class Of Service
        content.append('<tr><th>Creator</th><td>')
        content.append('<input type="hidden" name="creator" value="'+username+'">')
        fullname = member_document.get('fullname', '')
        if fullname:
            content.append('<i>'+fullname+'</i>')
        else:
            content.append('<i>'+username+'</i>')

        content.append('</td><th>Class Of Service</th><td><select name="classofservice">')
        content.append('<option class="warning" value="">Please select...</option>')
        for class_of_service_distinct in self.classes_of_service:
            if class_of_service_distinct == classofservice:
                content.append('<option value="'+class_of_service_distinct+'" selected>'+class_of_service_distinct+'</option>')
            else:
                content.append('<option value="'+class_of_service_distinct+'">'+class_of_service_distinct+'</option>')

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

        # Owner and Co-owner
        content.append('<tr><th>Owner</th><td>')
        content.append('<select name="owner">')
        content.append('<option class="warning" value="">Please select...</option>')
        suggested_owner = ""
        if parent and 'owner' in parent_document and parent_document['owner']:
            suggested_owner = parent_document['owner']
        else:
            suggested_owner = username

        for (user_count, epicCount, featureCount, storyCount, enhancementCount, defectCount, taskCount, test_count,
             bugCount, transientCount, fullname_distinct, usernameDistinct) in busy_order_user_distincts:
            if usernameDistinct == suggested_owner:
                content.append(f'<option value="{usernameDistinct}" selected>{fullname_distinct} [{user_count}]</option>')
            else:
                content.append(f'<option value="{usernameDistinct}">{fullname_distinct} [{user_count}]</option>')

        content.append('</select></td>')
        content.append('<th>Co-Owner</th><td>')
        content.append('<select name="coowner">')
        content.append('<option class="warning" value="">Please select...</option>')
        for (user_count, epicCount, featureCount, storyCount, enhancementCount, defectCount, taskCount, test_count,
             bugCount, transientCount, fullname_distinct, usernameDistinct) in busy_order_user_distincts:
            content.append(f'<option value="{usernameDistinct}">{fullname_distinct} [{user_count}]</option>')

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

        # Reviewer and Co-reviewer
        content.append('<tr><th>Reviewer</th><td>')
        content.append('<select name="reviewer">')
        content.append('<option class="warning" value="">Please select...</option>')
        suggested_reviewer = ""
        if parent and 'reviewer' in parent_document and parent_document['reviewer']:
            suggested_reviewer = parent_document['reviewer']

        for (user_count, epicCount, featureCount, storyCount, enhancementCount, defectCount, taskCount, test_count,
             bugCount, transientCount, fullname_distinct, usernameDistinct) in busy_order_user_distincts:
            if usernameDistinct == suggested_reviewer:
                content.append(f'<option value="{usernameDistinct}" selected>{fullname_distinct} [{user_count}]</option>')
            else:
                content.append(f'<option value="{usernameDistinct}">{fullname_distinct} [{user_count}]</option>')

        content.append('</select></td>')
        content.append('<th>Co-Reviewer</th><td>')
        content.append('<select name="coreviewer">')
        content.append('<option class="warning" value="">Please select...</option>')
        for (user_count, epicCount, featureCount, storyCount, enhancementCount, defectCount, taskCount, test_count,
             bugCount, transientCount, fullname_distinct, usernameDistinct) in busy_order_user_distincts:
            content.append(f'<option value="{usernameDistinct}">{fullname_distinct} [{user_count}]</option>')

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

        # Project
        content.append('<tr><th title="Mandatory field">Project*</th>')
        if project:
            content.append('<td><i>'+project+'</i></td>')
        else:
            if member_document:
                suggested_project = member_document.get('project', '')
            else:
                suggested_project = ""

            content.append('<td><select id="project" name="project">')
            content.append('<option class="warning" value="">Please select...</option>')
            for project_distinct in project_distincts:
                if project_distinct == suggested_project:
                    content.append('<option value="'+project_distinct+'" selected>'+project_distinct+'</option>')
                else:
                    content.append('<option value="'+project_distinct+'">'+project_distinct+'</option>')

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

            content.append('<th>Subteam</th><td><select name="subteam">')
            content.append('<option class="warning" value="">Please select...</option>')
            subteam_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('subteams.subteam')
            for subteam_distinct in subteam_distincts:
                if subteam_distinct == subteam:
                    content.append('<option value="'+subteam_distinct+'" selected>'+subteam_distinct+'</option>')
                else:
                    content.append('<option value="'+subteam_distinct+'">'+subteam_distinct+'</option>')

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

        # Release and Iteration
        content.append('<tr><th>Release</th>')
        release_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('releases.release')
        if release:
            content.append('<td><i>'+release+'</i></td>')
        else:
            if member_document:
                suggested_release = member_document.get('release', '')

            content.append('<td><select name="release">')
            content.append('<option class="warning" value="">Please select...</option>')
            for release_distinct in release_distincts:
                if release_distinct == suggested_release:
                    content.append('<option value="'+release_distinct+'" selected>'+release_distinct+'</option>')
                else:
                    content.append('<option value="'+release_distinct+'">'+release_distinct+'</option>')

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

        content.append('<th>Iteration</th>')
        if iteration:
            content.append('<td><i>'+iteration+'</i></td>')
        else:
            iteration_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('releases.iterations.iteration')
            if member_document and 'iteration' in member_document and member_document['iteration']:
                suggested_iteration = member_document['iteration']
            else:
                suggested_iteration = ""

            content.append('<td><select name="iteration">')
            content.append('<option class="warning" value="">Please select...</option>')
            for iteration_distinct in iteration_distincts:
                if iteration_distinct == suggested_iteration:
                    content.append('<option value="'+iteration_distinct+'" selected>'+iteration_distinct+'</option>')
                else:
                    content.append('<option value="'+iteration_distinct+'">'+iteration_distinct+'</option>')

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

        content.append('</tr>')

        # Category and Tags
        content.append('<tr><th>Category</th><td><select name="category">')
        content.append('<option class="warning" value="">Please select...</option>')
        selectable_categories = []
        selectablecategory_documents = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('categories')
        for selectablecategory_document in selectablecategory_documents:
            if 'category' in selectablecategory_document:
                if 'colour' in selectablecategory_document:
                    selectable_categories.append((selectablecategory_document['category'],selectablecategory_document['colour']))
                else:
                    selectable_categories.append((selectablecategory_document['category'],''))

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

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

        content.append('</select></td>')
        content.append('<th>Tags</th>')
        content.append('<td><input type="text" name="tags" size="40"></td>')
        content.append('</tr>')

        # State
        content.append('<tr><th>State</th><td><select name="state">')
        project_document = self.projects_collection.find_one({'project': project})
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        for state_distinct in condensed_column_states:
            metastate_distinct = self.get_corresponding_metastate(project_document, state_distinct)
            if metastate_distinct not in ['acceptancetestingaccepted', 'completed', 'closed']:
                if metastate_distinct == 'backlog':
                    if parent:
                        backlog_custom_states = self.get_custom_states_mapped_onto_metastates(['backlog'])
                        if parent_document['state'] not in backlog_custom_states:
                            content.append('<option value="'+state_distinct+'">'+state_distinct.capitalize()+'</option>')
                        else:
                            content.append('<option value="'+state_distinct+'" selected>'+state_distinct.capitalize()+'</option>')

                    else:
                        content.append('<option value="'+state_distinct+'" selected>'+state_distinct.capitalize()+'</option>')

                elif metastate_distinct == 'analysis' and parent and parent_document['state'] != 'backlog':
                    content.append('<option value="'+state_distinct+'" selected>'+state_distinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+state_distinct+'">'+state_distinct.capitalize()+'</option>')

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

        # Resolution and Customer
        content.append('<tr><th>Resolution</th><td>')
        content.append(self.create_html_select_block('resolution', self.resolutions,
                                                     default='Please select...',
                                                     specials=['capitalise']))
        content.append('</td><th>Customer</th><td><input type="text" name="customer" size="40" list="customerlist">')
        content.append('<datalist id="customerlist">')
        customer_distincts = self.cards_collection.find({'project': {'$in': project_distincts}}).distinct('customer')
        for customer_distinct in customer_distincts:
            content.append('<option value="'+customer_distinct+'">'+customer_distinct+'</option>')

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

        if parent:
            content.append('<tr><th>Parent</th>')
            if 'title' in parent_document and parent_document['title']:
                content.append(f'<td><i>{parent} {parent_document["title"]}</i></td>')
            else:
                content.append(f'<td><i>{parent}</i></td>')

            content.append('<th>Blocks Parent</th><td><input type="checkbox" name="blocksparent"></td>')
            content.append('</tr>')

        # AffectsVersion and FixVersion
        content.append('<tr><th>Affects Version</th><td><select name="affectsversion">')
        content.append('<option class="warning" value="">Please select...</option>')
        release_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('releases.release')
        for release_distinct in release_distincts:
            content.append('<option value="'+release_distinct+'">'+release_distinct+'</option>')

        content.append('</select></td><th>Fix Version</th><td><select name="fixversion">')
        content.append('<option class="warning" value="">Please select...</option>')
        release_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('releases.release')
        for release_distinct in release_distincts:
            content.append('<option value="'+release_distinct+'">'+release_distinct+'</option>')

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

        # EstimatedTime and ActualTime
        unit = "day"
        unit_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('unit')
        if len(unit_distincts) == 1:
            unit = unit_distincts[0]

        content.append('<tr><th>Estimated Time</th><td><input type="number" name="estimatedtime" min="0.0" step="0.25" value="0"> '+unit+'s <input type="text" name="estimatedtimeexplanation" size="40" placeholder="Explanation"></td>')
        content.append('<th>Actual Time</th><td><input type="number" name="actualtime" min="0.0" step="0.25" value="0"> '+unit+'s <input type="text" name="actualtimeexplanation" size="40" placeholder="Explanation"></td></tr>')

        # Cost
        currency = ""
        currency_distincts = self.projects_collection.find({'project': {'$in': project_distincts}}).distinct('currency')
        if len(currency_distincts) == 1:
            currency = currency_distincts[0]

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

        content.append('<tr><th>Estimated Cost</th><td>')
        content.append(currency_symbol)
        content.append('<input type="number" name="estimatedcost" min="0.0" value="0">  <input type="text" name="estimatedcostexplanation" size="40" placeholder="Explanation"></td><th>Actual Cost</th><td colspan="3">')
        content.append(currency_symbol)
        content.append('<input type="number" name="actualcost" min="0.0" value="0"> <input type="text" name="actualcostexplanation" size="40" placeholder="Explanation"></td></tr>')

        # Severity and Priority
        content.append('<tr><th>Severity</th><td><select name="severity">')
        for severityDistinct in self.severities:
            if parent:
                if severityDistinct == parent_document['severity']:
                    content.append('<option value="'+severityDistinct+'" selected>'+severityDistinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+severityDistinct+'">'+severityDistinct.capitalize()+'</option>')

            else:
                if severityDistinct == 'medium':
                    content.append('<option value="'+severityDistinct+'" selected>'+severityDistinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+severityDistinct+'">'+severityDistinct.capitalize()+'</option>')

        content.append('</select></td><th>Priority</th><td><select name="priority">')
        for priority_distinct in self.priorities:
            if parent:
                if priority_distinct == parent_document['priority']:
                    content.append('<option value="'+priority_distinct+'" selected>'+priority_distinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+priority_distinct+'">'+priority_distinct.capitalize()+'</option>')

            else:
                if priority_distinct == 'medium':
                    content.append('<option value="'+priority_distinct+'" selected>'+priority_distinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+priority_distinct+'">'+priority_distinct.capitalize()+'</option>')

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

        # Before and After
        content.append('<tr><th>Before</th><td><input type="text" name="before" size="40" list="beforelist"><datalist id="beforelist">')
        id_distincts = self.cards_collection.find({"id": {"$exists": True, "$ne": ""},
                                                  "state": {"$nin": ["closed"]}}).distinct('id')
        for idDistinct in id_distincts:
            document = self.cards_collection.find_one({'id': idDistinct})
            if document and document.get('id'):
                if 'title' in document and document['title']:
                    content.append(f'<option value="{document["id"]}">{document["type"].capitalize()} {document["id"]} {document["title"]}</option>')
                else:
                    content.append(f'<option value="{document["id"]}">{document["type"].capitalize()} {document["id"]}</option>')

        content.append('</datalist></td><th>After</th><td><input type="text" name="after" size="40" list="afterlist"><datalist id="afterlist">')
        id_distincts = self.cards_collection.find({"id": {"$exists": True, "$ne": ""},
                                                         "state": {"$nin": ["closed"]}}).distinct('id')
        for idDistinct in id_distincts:
            document = self.cards_collection.find_one({'id': idDistinct})
            if document and document.get('id'):
                if 'title' in document and document['title']:
                    content.append(f'<option value="{document["id"]}">{document["type"].capitalize()} {document["id"]} {document["title"]}</option>')
                else:
                    content.append(f'<option value="{document["id"]}">{document["type"].capitalize()} {document["id"]}</option>')

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

        # Depends Upon and Difficulty
        content.append('<tr><th>Depends Upon</th><td><input type="text" name="dependsupon" size="40" list="dependsuponlist"><datalist id="dependsuponlist">')
        id_distincts = self.cards_collection.find({"id": {"$exists": True, "$ne": ""},
                                                  "state": {"$nin": ["closed"]}}).distinct('id')
        for idDistinct in id_distincts:
            document = self.cards_collection.find_one({'id': idDistinct})
            if document and document.get('id'):
                if 'title' in document and document['title']:
                    content.append(f'<option value="{document["id"]}">{document["type"].capitalize()} {document["id"]} {document["title"]}</option>')
                else:
                    content.append(f'<option value="{document["id"]}">{document["type"].capitalize()} {document["id"]}</option>')

        content.append('</datalist></td><th>Difficulty</th><td>')
        content.append(self.create_html_select_block('difficulty', ['easy', 'normal', 'hard'],
                                                     default=None, specials=['capitalise']))
        content.append('</td></tr>')

        # Start By and Next Action
        content.append('<tr><th><img src="/images/Calendar-icon-16.png"> Start By</th><td>')
        if startby:
            date_format = self.convert_time_to_display_format(startby)
            content.append('<input class="startby" type="text" name="startby" value="'+date_format+'">')
        else:
            content.append('<input class="startby" type="text" name="startby">')

        content.append('</td><th><img src="/images/Calendar-icon-16.png"> Next Action</th><td>')
        if nextaction:
            date_format = self.convert_time_to_display_format(nextaction)
            content.append('<input class="nextaction" type="text" name="nextaction" value="'+date_format+'">')
        else:
            content.append('<input class="nextaction" type="text" name="nextaction">')

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

        # Deadline
        content.append('<tr><th><img src="/images/Calendar-icon-16.png"> Deadline</th><td colspan="3">')
        if deadline:
            date_format = self.convert_time_to_display_format(deadline)
            content.append('<input class="deadline" type="text" name="deadline" value="'+date_format+'">')
        else:
            content.append('<input class="deadline" type="text" name="deadline">')

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

        # Artifacts
        content.append('<tr><th>Artifacts</th><td colspan="3"><textarea name="artifacts" rows="5" cols="80"></textarea></td></tr>')

        # Rules
        content.append('<tr><th>Rules</th><td colspan="3">')

        content.append('<table><tr><th>Rule</th><th>Usage</th></tr>')
        content.append('<tr><td><input type="text" name="rule0" size="120"></td><td>')
        content.append(self.create_html_select_block('ruleusage0', ['Perpetual', 'Transient'], default=None))
        content.append('</td></tr>')
        content.append('</table>')

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

        # Test Cases
        content.append('<tr><th>Test Cases</th><td colspan="3">')

        content.append('<table><tr><th>Title</th><th>Description</th><th>State</th></tr>')
        content.append('<tr><td><input type="text" name="testcasetitle0" size="40"></td><td><input type="text" name="testcasedescription0" size="80"></td><td>')
        content.append(self.create_html_select_block('testcasestate0',
                                                     ['defined', 'progressing', 'accepted'],
                                                     default=None, specials=['capitalise']))
        content.append('</td></tr>')
        content.append('</table>')

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

        # Root-Cause Analysis
        content.append('<tr><th>Root-Cause Analysis</th><td colspan="3"><textarea name="rootcauseanalysis" rows="5" cols="80"></textarea></td></tr>')

        # CRM Case and Escalation
        content.append('<tr><th>CRM Case</th><td>')
        if crmcase:
            content.append('<input type="text" name="crmcase" size="40" value="'+crmcase+'">')
        else:
            content.append('<input type="text" name="crmcase" size="40">')

        content.append('</td><th><img src="/images/Link-icon-16.png"> Escalation</th><td>')
        if escalation:
            content.append('<input type="text" name="escalation" size="40" value="'+escalation+'">')
        else:
            content.append('<input type="text" name="escalation" size="40">')

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

        # External Reference
        content.append('<tr><th><img src="/images/Link-icon-16.png"> External Reference</th><td>')
        if externalreference:
            content.append('<input type="text" name="externalreference" size="40" value="'+externalreference+'">')
        else:
            content.append('<input type="text" name="externalreference" size="40">')

        content.append('</td><th><img src="/images/Link-icon-16.png"> External Hyperlink</th><td>')
        if externalhyperlink:
            content.append('<input type="text" name="externalhyperlink" size="40" value="'+externalhyperlink+'">')
        else:
            content.append('<input type="text" name="externalhyperlink" size="40">')

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

        # Deferred and DeferredUntil
        content.append('<tr><th>Deferred</th><td>')
        if deferred:
            content.append('<input type="text" name="deferred" size="40" value="'+deferred+'">')
        else:
            content.append('<input type="text" name="deferred" size="40" placeholder="Reason">')

        content.append('</td><th><img src="/images/Calendar-icon-16.png"> Deferred Until</th><td>')
        if deferreduntil:
            date_format = self.convert_time_to_display_format(deferreduntil)
            content.append('<input class="deferreduntil" type="text" name="deferreduntil" value="'+date_format+'">')
        else:
            content.append('<input class="deferreduntil" type="text" name="deferreduntil">')

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

        # Emotion
        content.append('<tr><th>Emotion</th><td>')
        content.append(self.create_html_select_block('emotion', self.emotions.keys(),
                                                     default='Please select...',
                                                     specials=['capitalise']))
        content.append('</td></tr>')

        # Broadcast and Question
        content.append('<tr><th>Broadcast</th><td><input type="text" name="broadcast" size="40"></td><th>Question</th><td><input type="text" name="question" size="40"></td></tr>')

        # BypassReview and Recurring
        content.append('<tr><th>Bypass Review</th><td><input type="checkbox" name="bypassreview"></td>')
        content.append('<th>Recurring</th><td><input type="checkbox" name="recurring"></td>')
        content.append('</tr>')

        # Comment
        content.append('<tr><th>Comment</th><td colspan="3"><textarea name="comment" rows="5" cols="80"></textarea></td></tr>')
        
        if 'customattributes' in project_document and project_document['customattributes']:
            content.append('<tr><td colspan="4"><h3>Custom Attributes</h3></td></tr>')
            for custom_attribute, value in project_document['customattributes'].items():
                content.append(f'<tr><th>{custom_attribute}</th><td><input type="text" name="{custom_attribute}" size="40"></td><td colspan="2"></td></tr>')

        content.append('<tr><td colspan="4" align="center"><span class="controlgroup"><input class="save" type="submit" value="Save"><input  class="edit" type="reset"></form><input class="save" type="button" value="Cancel" onclick="window.location=\'/kanban\'" /></span></td></tr>')

        content.append('</table>')
        content.append('</td></tr></table>')
        content.append('<script type="text/javascript" src="/scripts/kanban.js"></script>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    def count_descendent_test_cards(self, id, test_count):
        #TODO - THIS FUNCTION DOES NOT APPEAR TO BE CALLED ANYMORE
        for card_document in self.cards_collection.find({'parent': id}):
            if card_document['type'] == 'test':
                test_count += 1

            test_count += self.count_descendent_test_cards(card_document['id'], test_count)

        return test_count

    @cherrypy.expose
    def split_story(self, doc_id, titleoffirst="", titleofsecond=""):
        """Allows a card of type 'story' to be split into two with any descendents remaining with the original card"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        content.append(Kanbanara.header(self, "split_story", "Split Story"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'split_story', "Split Story"))
        existingcard_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        if titleoffirst and titleofsecond and titleoffirst != titleofsecond:
            existingcard_document['title'] = titleoffirst
            existingcard_document['lastchanged'] = datetime.datetime.utcnow()
            self.cards_collection.save(existingcard_document)
            self.add_recent_activity_entry((datetime.datetime.utcnow(), username, doc_id, 'split'))
            new_card_document = {}
            for key in existingcard_document.keys():
                if key not in ['_id', 'id', 'lastchanged', 'title']:
                    new_card_document[key] = existingcard_document[key]

            new_card_document['id'] = self.get_project_next_card_number(existingcard_document['project'], existingcard_document['type'])
            new_card_document['title'] = titleofsecond
            new_card_document['lastchanged'] = datetime.datetime.utcnow()
            doc_id = self.cards_collection.insert(new_card_document)
            self.add_recent_activity_entry((datetime.datetime.utcnow(), username, doc_id, 'split'))
            raise cherrypy.HTTPRedirect('/kanban', 302)
        else:
            content.append('<div class="ui-state-error"><ul></ul></div>')
            existing_title = existingcard_document.get('title', '')
            if len(existing_title) > 80:
                size = len(existing_title)
            else:
                size = 80

            content.append('<form id="splitstoryform" action="/cards/split_story" method="post">')
            content.append('<input type="hidden" name="doc_id" value="'+doc_id+'">')
            content.append('<table class="form">')
            content.append(f'<tr><th>Title of First Story</th><td><input type="text" id="titleoffirst" name="titleoffirst" value="{existing_title}" size="{size}"></td><td>')
            if self.cards_collection.find({"parent": existingcard_document['id']}).count():
                content.append('<p class="warning">Note: Any Child cards will remain with this Story</p>')

            content.append('</td></tr><tr><th>Title of Second Story</th><td>')
            content.append(f'<input type="text" id="titleofsecond" name="titleofsecond" value="{existing_title}" size="{size}">')
            content.append('</td><td></td></tr>')
            content.append('<tr><td align="center" colspan="3"><input type="submit" value="Split Story"></td></tr>')
            content.append('</table><form>')

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

    def count_descendents(self, doc_id, count):
        """comment"""
        parent_card_document = self.cards_collection.find_one({'_id': ObjectId(doc_id)})
        if parent_card_document:
            for child_card_document in self.cards_collection.find({'parent': parent_card_document['id']}):
                count += 1
                count = self.count_descendents(child_card_document['_id'], count)

            return count

    @cherrypy.expose
    def add_resolution(self, doc_id, new_resolution=""):
        """Allows a resolution to be added to a card"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        id, title = self.get_card_attribute_values(card_document, ['id', 'title'])
        if new_resolution:
            card_document['resolution'] = new_resolution
            self.cards_collection.save(card_document)
            self.save_card_as_json(card_document)
            raise cherrypy.HTTPRedirect("/kanban/index", 302)
        else:
            content = []
            content.append(Kanbanara.header(self, 'add_resolution', "Add Resolution"))
            content.append(Kanbanara.filter_bar(self, 'index'))
            content.append(Kanbanara.menubar(self))
            content.append('<div align="center">')
            content.append(self.insert_page_title_and_online_help(session_document, 'add_resolution',
                                                                  f'Add Resolution to {id} `{title}`'))
            content.append('<div align="center">')
            content.append('<form action="/cards/add_resolution" method="post">')
            content.append(f'<input type="hidden" name="doc_id" value="{doc_id}">')
            content.append('<table><tr><th>Resolution</th><td>')
            content.append(self.create_html_select_block('new_resolution', self.resolutions,
                                                         default='Please select...',
                                                         specials=['capitalise']))
            content.append('</td></tr><tr><td colspan="2"><input type="submit" value="Add Resolution">')
            content.append('</td></tr></table></form></div>')
            content.append(Kanbanara.footer(self))
            return "".join(content)

    @cherrypy.expose
    def upload_attachment(self, doc_id, uploaded_file=""):
        """Allows a file to be uploaded and attached to a card"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        content.append(Kanbanara.header(self, 'upload_attachment', "Upload Attachment"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        doc_id, id, project, title = self.get_card_attribute_values(card_document, ['_id', 'id', 'project', 'title'])
        content.append(self.insert_page_title_and_online_help(session_document, 'upload_attachment',
                                                              'Upload Attachment to '+id+' `'+title+'`'))
        if uploaded_file == '' or not uploaded_file.filename:
            content.append('<div align="center"><form action="/kanban/upload_attachment" method="post" enctype="multipart/form-data">')
            content.append(f'<input type="hidden" name="doc_id" value="{doc_id}">')
            content.append('<table border="0">')
            content.append('<tr><td align="right"><p>Attachment</p></td><td><p>')
            content.append('<input type="file" name="uploaded_file">')
            content.append('</p></td></tr><tr><td align="right" valign="top"><input type="submit" value="Upload Attachment"></form></td><td valign="top"><form><input type="button" name="cancel" value="Cancel" onClick="javascript:history.back();"></form></td></tr></table>')
            content.append('<div id="dropfile">Drop Attachment Here!</div>')
            content.append('</div>')
        else:
            filename = uploaded_file.filename
            (leafname, ext) = os.path.splitext(filename)
            if not os.path.exists(os.path.join(self.current_dir, '..', 'attachments', project)):
                os.mkdir(os.path.join(self.current_dir, '..', 'attachments', project))

            if not os.path.exists(os.path.join(self.current_dir, '..', 'attachments', project, id)):
                os.mkdir(os.path.join(self.current_dir, '..', 'attachments', project, id))

            with open(os.path.join(self.current_dir, '..', 'attachments', project, id, leafname+ext),'wb') as op:
                while True:
                    chunk = uploaded_file.file.read(4096)
                    if not chunk:
                        break
                    else:
                        op.write(chunk)

            raise cherrypy.HTTPRedirect('/kanban', 302)

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

    @cherrypy.expose
    def index(self):
        """Redirects you to the kanban board"""
        raise cherrypy.HTTPRedirect("/kanban", 302)

    @cherrypy.expose
    def hierarchy(self, doc_id):
        """comment"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        content = []
        content.append(Kanbanara.header(self, "hierarchy", "Hierarchy"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, "hierarchy", "Hierarchy"))
        content.append('<script type="text/javascript">')
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        if 'parent' in card_document and card_document['parent']:
            parent_document = self.cards_collection.find_one({'id': card_document['parent']})
            if parent_document:
                hierarchical_tree_data = self.generate_hierarchical_tree_data(str(parent_document['_id']))
            else:
                hierarchical_tree_data = self.generate_hierarchical_tree_data(doc_id)

        else:
            hierarchical_tree_data = self.generate_hierarchical_tree_data(doc_id)

        content.append(f'var treeData = {hierarchical_tree_data};')
        content.append('''
// set the dimensions and margins of the diagram
var margin = {top: 50, right: 50, bottom: 50, left: 50},
    scrollbar = 25,
    width = window.innerWidth - scrollbar,
    height = window.innerHeight;

// declares a tree layout and assigns the size
var treemap = d3.tree().size([width-margin.left-margin.right, height-margin.top-margin.bottom]);

//  assigns the data to a hierarchy using parent-child relationships
var nodes = d3.hierarchy(treeData);

// maps the node data to the tree layout
nodes = treemap(nodes);

// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height),
    g = svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// adds the links between the nodes
var link = g.selectAll(".link")
    .data( nodes.descendants().slice(1))
  .enter().append("path")
    .attr("class", "link")
    .attr("d", function(d) {
       return "M" + d.x + "," + d.y
         + "C" + d.x + "," + (d.y + d.parent.y) / 2
         + " " + d.parent.x + "," +  (d.y + d.parent.y) / 2
         + " " + d.parent.x + "," + d.parent.y;
       });

// adds each node as a group
var node = g.selectAll(".node")
    .data(nodes.descendants())
  .enter().append("g")
    .attr("class", function(d) {
      return "node" +
        (d.children ? " node--internal" : " node--leaf"); })
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")"; });

// adds the circle to the node
node.append("circle")
  .attr("r", 10)
  .attr("class", function(d) { return d.data.type; })
  .attr("title", function(d) { return d.data.title; });

// adds the text to the node
node.append("text")
  .attr("dy", ".35em")
  .attr("y", function(d) { return d.children ? -30 : 20; })
  .style("text-anchor", "middle")
  .text(function(d) { return d.data.type; });

// adds the text to the node
node.append("a")
  .attr("xlink:href", function(d) { return "/kanban/hierarchy?doc_id="+d.data._id; })
  .append("text")
  .attr("vector-effect", "non-scaling-stroke")
  .attr("dy", "1.35em")
  .attr("y", function(d) { return d.children ? -30 : 20; })
  .style("text-anchor", "middle")
  .text(function(d) { return d.data.id; });

</script>
  ''')
        content.append('</div>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    def generate_hierarchical_tree_data(self, doc_id):
        """Generate the hierarchical tree for a given card ID"""
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        if card_document:
            hierarchical_tree_data = {'_id':      str(card_document['_id']),
                                      'id':       card_document['id'],
                                      'title':    card_document['title'],
                                      'type':     card_document['type'],
                                      'children': []
                                     }
            for child_card_document in self.cards_collection.find({'parent': card_document['id']}):
                child_tree_data = self.generate_hierarchical_tree_data(child_card_document['_id'])
                hierarchical_tree_data['children'].append(child_tree_data)

        return hierarchical_tree_data

    @cherrypy.expose
    def jsonview(self):
        """comment"""
        Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        content.append(Kanbanara.header(self, "jsonview","JSON View"))
        content.append(Kanbanara.filter_bar(self, 'jsonview'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "jsonview", "JSON View"))
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        required_columns, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        count = self.cards_collection.find(owner_reviewer_search_criteria).count()
        content.append('<p class="json">[')
        for no, document in enumerate(self.cards_collection.find(owner_reviewer_search_criteria)):
            content.append(self.dictionary_as_json('html', document, 0))
            if no < count-1:
                content.append(',')

        content.append(']</p>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    def get_hidden_until_values(self):
        epoch = datetime.datetime.utcnow()
        day_count = 1
        hidden_until_values = []
        while day_count < 7:
            future_epoch = epoch + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_display_format(future_epoch)
            hidden_until_values.append(date_format)
            day_count += 1

        return hidden_until_values

    def parse_rule(self, project, rule):
        revised_rule = ""
        while '  ' in rule:
            rule = rule.replace('  ', ' ')

        inside_list          = False
        inside_single_quotes = False
        inside_double_quotes = False
        # TODO - Do we want to support """...""" and '''...''' quoted strings?
        for char in rule:
            if char == "'":
                revised_rule += char
                if not inside_list:
                    if not inside_single_quotes:
                        if not inside_double_quotes:
                            inside_single_quotes = True

                    else:
                        inside_single_quotes = False

                else:
                    inside_single_quotes = False

            elif char == '[':
                revised_rule += char
                if not inside_single_quotes:
                    if not inside_double_quotes:
                        inside_list = True

            elif char == ']':
                revised_rule += char
                if inside_list:
                    inside_list = False

            elif char == '"':
                revised_rule += char
                if not inside_list:
                    if not inside_double_quotes:
                        if not inside_single_quotes:
                            inside_double_quotes = True

                    else:
                        inside_double_quotes = False

                else:
                    inside_double_quotes = False

            elif char == ' ':
                if inside_list or inside_double_quotes or inside_single_quotes:
                    revised_rule += char
                else:
                    revised_rule += '--===--'

            else:
                revised_rule += char

        components = revised_rule.split('--===--')
        status = self.validate_rule(project, components)
        return components, status

    @cherrypy.expose
    def update_card(self, doc_id):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        content = []
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        doc_id = card_document["_id"]
        actualcost = Decimal(card_document.get('actualcost', 0))
        actualtime = float(card_document.get('actualtime', 0))
        affectsversion = card_document.get('affectsversion', '')
        after = card_document.get('after', '')
        artifacts = card_document.get('artifacts', [])
        before = card_document.get('before', '')
        blocked = card_document.get('blocked', '')
        blockeduntil = card_document.get('blockeduntil', '')
        blocksparent = card_document.get('blocksparent', False)
        broadcast = card_document.get('broadcast', '')
        bypassreview = card_document.get('bypassreview', False)
        category = card_document.get('category', '')
        classofservice = card_document.get('classofservice', '')
        comments = card_document.get('comments', [])
        coowner = card_document.get('coowner', '')
        coownerstate = card_document.get('coownerstate', '')
        coreviewer = card_document.get('coreviewer', '')
        coreviewerstate = card_document.get('coreviewerstate', '')
        crmcase = card_document.get('crmcase', '')
        customer = card_document.get('customer', '')
        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', '')
        difficulty = card_document.get('difficulty', '')
        emotion = card_document.get('emotion', '')
        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', '')
        hiddenuntil = card_document.get('hiddenuntil', '')
        id = card_document.get('id', '')
        iteration = card_document.get('iteration', '')
        nextaction = card_document.get('nextaction', '')
        notes = card_document.get('notes', '')
        owner = card_document.get('owner', '')
        ownerstate = card_document.get('ownerstate', '')
        parent = card_document.get('parent', '')
        priority = card_document.get('priority', '')
        project = card_document.get('project', '')
        question = card_document.get('question', '')
        reassigncoowner = card_document.get('reassigncoowner', '')
        reassigncoreviewer = card_document.get('reassigncoreviewer', '')
        reassignowner = card_document.get('reassignowner', '')
        reassignreviewer = card_document.get('reassignreviewer', '')
        recurring = card_document.get('recurring', '')
        release = card_document.get('release', '')
        resolution = card_document.get('resolution', '')
        reviewer = card_document.get('reviewer', '')
        reviewerstate = card_document.get('reviewerstate', '')
        rootcauseanalysis = card_document.get('rootcauseanalysis', '')
        rules = card_document.get('rules', [])
        severity = card_document.get('severity', '')
        startby = card_document.get('startby', '')
        state = card_document.get('state', '')
        status = card_document.get('status', '')
        stuck = card_document.get('stuck', '')
        tags = card_document.get('tags', '')
        subteam = card_document.get('subteam', '')
        testcases = card_document.get('testcases', [])
        title = card_document.get('title', '')
        type = card_document.get('type', '')
        page_title = "Update "+type.capitalize()
        content.append(Kanbanara.header(self, 'update_card',page_title))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<table width="100%" border="0"><tr><td valign="top">')
        content.append(f'<h2 class="page_title">{page_title}</h2>')
        content.append('<div class="ui-state-error"><ul></ul></div>')
        content.append('<form id="updaterecordform" action="/cards/submit_update_card" method="post">')
        content.append(f'<input type="hidden" name="doc_id" value="{doc_id}">')
        content.append(f'<input type="hidden" name="id" value="{id}">')
        content.append(f'<input type="hidden" name="type" value="{type}">')
        content.append(f'<input type="hidden" name="project" value="{project}">')

        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', {})
        state_position = -1
        if state and state in condensed_column_states_dict:
            state_position = condensed_column_states_dict[state]

        child_states = self.cards_collection.find({"parent": id, "blocksparent": True}).distinct('state')
        lowest_child_state, highest_child_state = self.calculate_child_state_range(uncondensed_column_states, condensed_column_states_dict, child_states)
        if lowest_child_state <= state_position:
            content.append('<table class="update'+type+'blocked" title="This '+type+' is blocked by one of its children">')
        else:
            content.append('<table class="update'+type+'">')

        content.append('<tr><td colspan="4" align="center"><span class="controlgroup"><input class="save" type="submit" value="Save"><input class="save" type="reset"></form><input class="save" type="button" value="Cancel" onclick="window.location=\'/kanban\'" /></span></td></tr>')

        # Title and Description
        title_distincts = self.cards_collection.find({"title": {"$exists": True,"$ne":""}}).distinct('title')
        content.append('<tr><th>Title</th><td>')
        if title:
            escaped_title = title.replace('"', '&#34;')
            content.append('<input type="text" name="title" id="title" size="40" value="'+escaped_title+'" list="titlelist">')
        else:
            content.append('<input type="text" name="title" id="title" size="40" list="titlelist">')

        content.append('<datalist id="titlelist">')
        for titleDistinct in title_distincts:
            content.append('<option value="'+titleDistinct+'">'+titleDistinct+'</option>')

        content.append('</datalist></td>')
        description_distincts = self.cards_collection.find({"description": {"$exists": True,"$ne":""}}).distinct('description')
        content.append('<th>Description</th><td>')
        if description:
            # TODO - IS THERE A BETTER WAY TO DO THIS?
            modified_description = description.replace('"', '&quot;')
            content.append('<input type="text" name="description" size="80" value="'+modified_description+'" list="descriptionlist">')
        else:
            content.append('<input type="text" name="description" size="80" list="descriptionlist">')

        content.append('<datalist id="descriptionlist">')
        for description_distinct in description_distincts:
            content.append('<option value="'+description_distinct+'">'+description_distinct+'</option>')

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

        # Status
        content.append('<tr><th>Status</th><td colspan="3"><textarea name="status" rows="5" cols="80">'+status+'</textarea></td></tr>')

        # Notes
        content.append('<tr><th>Notes</th><td colspan="3"><textarea name="notes" rows="5" cols="80">'+notes+'</textarea></td></tr>')

        # Get Creator/Owner Distincts
        fullnames_and_usernames = self.get_project_members([project])
        busy_order_user_distincts = self.get_busy_order_user_distincts(fullnames_and_usernames, project, release, iteration)

        # Creator and Class Of Service
        content.append('<tr><th>Creator</th><td>')
        content.append('<input type="hidden" name="creator" value="'+username+'">')
        fullname = member_document.get('fullname', '')
        if fullname:
            content.append('<i>'+fullname+'</i>')
        else:
            content.append('<i>'+username+'</i>')

        content.append('</td><th>Class Of Service</th><td><select name="classofservice">')
        content.append('<option class="warning" value="">Please select...</option>')
        for class_of_service_distinct in self.classes_of_service:
            if class_of_service_distinct == classofservice:
                content.append('<option value="'+class_of_service_distinct+'" selected>'+class_of_service_distinct+'</option>')
            else:
                content.append('<option value="'+class_of_service_distinct+'">'+class_of_service_distinct+'</option>')

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

        # Owner and ReassignOwner, Co-Owner and ReassignCo-Owner, Reviewer and ReassignReviewer, Co-Reviewer and ReassignCo-Reviewer
        for (thText, htmlName, userAttribute, reassignAttribute) in [('Owner', 'owner', owner, reassignowner),
                                                                     ('Co-Owner', 'coowner', coowner, reassigncoowner),
                                                                     ('Reviewer', 'reviewer', reviewer, reassignreviewer),
                                                                     ('Co-Reviewer', 'coreviewer', coreviewer, reassigncoreviewer)]:
            content.append('<tr><th>'+thText+'</th><td>')
            content.append('<select name="'+htmlName+'">')
            content.append('<option class="warning" value="">Please select...</option>')
            for (user_count, epicCount, featureCount, storyCount, enhancementCount, defectCount, taskCount, test_count, bugCount, transientCount, fullname_distinct, usernameDistinct) in busy_order_user_distincts:
                if usernameDistinct == userAttribute:
                    content.append(f'<option value="{usernameDistinct}" selected>{fullname_distinct} [{user_count}]</option>')
                else:
                    content.append(f'<option value="{usernameDistinct}">{fullname_distinct} [{user_count}]</option>')

            content.append('</select></td>')
            content.append('<th>Reassign '+thText+'</th><td>')
            if reassignAttribute:
                content.append('<input type="text" name="reassign'+htmlName+'" size="40" value="'+reassignAttribute+'">')
            else:
                content.append('<input type="text" name="reassign'+htmlName+'" size="40" placeholder="Reason">')

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

        # Project and Subteam
        content.append('<tr><th>Project</th><td><i>'+project+'</i></td>')

        content.append('<th>Subteam</th><td><select name="subteam">')
        content.append('<option class="warning" value="">Please select...</option>')
        subteam_distincts = self.projects_collection.find({'project': project}).distinct('subteams.subteam')
        for subteam_distinct in subteam_distincts:
            if subteam_distinct == subteam:
                content.append('<option value="'+subteam_distinct+'" selected>'+subteam_distinct+'</option>')
            else:
                content.append('<option value="'+subteam_distinct+'">'+subteam_distinct+'</option>')

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

        # Release and Iteration
        content.append('<tr><th>Release</th><td>')
        release_distincts = self.projects_collection.find({'project': project}).distinct('releases.release')
        content.append('<select name="release">')
        content.append('<option class="warning" value="">Please select...</option>')
        for release_distinct in release_distincts:
            if release_distinct == release:
                content.append('<option value="'+release_distinct+'" selected>'+release_distinct+'</option>')
            else:
                content.append('<option value="'+release_distinct+'">'+release_distinct+'</option>')

        content.append('</select></td>')
        content.append('<th>Iteration</th><td>')
        iteration_distincts = self.projects_collection.find({'project': project}).distinct('releases.iterations.iteration')
        content.append('<select name="iteration">')
        content.append('<option class="warning" value="">Please select...</option>')
        for iteration_distinct in iteration_distincts:
            if iteration_distinct == iteration:
                content.append('<option value="'+iteration_distinct+'" selected>'+iteration_distinct+'</option>')
            else:
                content.append('<option value="'+iteration_distinct+'">'+iteration_distinct+'</option>')

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

        # Category and Tags
        content.append('<tr><th>Category</th><td><select name="category">')
        content.append('<option class="warning" value="">Please select...</option>')
        selectable_categories = []
        selectablecategory_documents = self.projects_collection.find({'project': project}).distinct('categories')
        for selectablecategory_document in selectablecategory_documents:
            if 'category' in selectablecategory_document:
                if 'colour' in selectablecategory_document:
                    selectable_categories.append((selectablecategory_document['category'],selectablecategory_document['colour']))
                else:
                    selectable_categories.append((selectablecategory_document['category'],''))

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

                content.append(' value="'+selectableCategory+'"')
                if selectableCategory == category:
                    content.append(' selected')

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

        content.append('</select></td><th>Tags</th><td>')
        if tags:
            content.append('<input type="text" name="tags" size="40" value="'+tags+'">')
        else:
            content.append('<input type="text" name="tags" size="40">')

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

        # State
        content.append('<tr><th>State</th><td><select name="state">')
        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', [])
        if state and child_states:
            for selectable_state in condensed_column_states:
                if selectable_state == state:
                    content.append('<option value="'+selectable_state+'" selected>'+selectable_state.capitalize()+'</option>')
                elif condensed_column_states_dict[state] < condensed_column_states_dict[selectable_state] < lowest_child_state:
                    content.append('<option value="'+selectable_state+'">'+selectable_state.capitalize()+'</option>')
                elif condensed_column_states_dict[state] > condensed_column_states_dict[selectable_state] > highest_child_state:
                    content.append('<option value="'+selectable_state+'">'+selectable_state.capitalize()+'</option>')
                elif lowest_child_state <= condensed_column_states_dict[selectable_state] <= highest_child_state:
                    content.append('<option value="'+selectable_state+'">'+selectable_state.capitalize()+'</option>')

        elif state and not child_states:
            for selectable_state in condensed_column_states:
                if selectable_state == state:
                    content.append('<option value="'+selectable_state+'" selected>'+selectable_state.capitalize()+'</option>')
                else:
                    content.append('<option value="'+selectable_state+'">'+selectable_state.capitalize()+'</option>')

        elif not state and child_states:
            for selectable_state in condensed_column_states:
                if selectable_state == 'backlog':
                    content.append('<option value="'+selectable_state+'" selected>'+selectable_state.capitalize()+'</option>')
                else:
                    content.append('<option value="'+selectable_state+'">'+selectable_state.capitalize()+'</option>')

        elif not state and not child_states:
            for selectable_state in condensed_column_states:
                if selectable_state == 'backlog':
                    content.append('<option value="'+selectable_state+'" selected>'+selectable_state.capitalize()+'</option>')
                else:
                    content.append('<option value="'+selectable_state+'">'+selectable_state.capitalize()+'</option>')

        content.append('</select>')
        metastate = self.get_corresponding_metastate(project_document, state)
        if state in ['unittesting', 'integrationtesting', 'systemtesting', 'acceptancetesting'] and not self.all_testcases_accepted(testcases):
            content.append('<sup class="testcase">Testcase Unaccepted!</sup>')

        content.append('</td><td colspan="2">')
        step_no, step_role, centric, preceding_state, next_state = self.get_associated_state_information(state)
        centric_message = False
        if centric == 'Owner':
            if owner and coowner:
                if ownerstate and not coownerstate:
                    if username == owner:
                        content.append(f'<p><i>Awaiting {coowner}!</i></p>')
                    else:
                        content.append(f'<p><i>{owner} has already moved to {ownerstate}</i></p>')

                    centric_message = True

                elif coownerstate and not ownerstate:
                    if username == coowner:
                        content.append(f'<p><i>Awaiting {owner}!</i></p>')
                    else:
                        content.append(f'<p><i>{coowner} has already moved to {coownerstate}</i></p>')

                    centric_message = True

        elif centric == 'Reviewer':
            if reviewer and coreviewer:
                if reviewerstate and not coreviewerstate:
                    if username == reviewer:
                        content.append(f'<p><i>Awaiting Co-Reviewer!</i></p>')
                    else:
                        content.append(f'<p><i>{reviewer} has already moved to {reviewerstate}</i></p>')

                    centric_message = True

                elif coreviewerstate and not reviewerstate:
                    if username == coreviewer:
                        content.append(f'<p><i>Awaiting Reviewer!</i></p>')
                    else:
                        content.append(f'<p><i>{coreviewer} has already moved to {coreviewerstate}</i></p>')

                    centric_message = True

        if not centric_message:
            progressbar_value = 0
            if metastate:
                if metastate in ['untriaged', 'triaged', 'backlog']:
                    progressbar_value = 0
                elif metastate in ['defined', 'analysis']:
                    progressbar_value = 20
                elif metastate in ['analysed', 'design', 'designed', 'development']:
                    progressbar_value = 40
                elif metastate in ['developed', 'unittesting', 'unittestingaccepted', 'integrationtesting',
                                   'integrationtestingaccepted', 'systemtesting', 'systemtestingaccepted',
                                   'acceptancetesting']:
                    progressbar_value = 60
                elif metastate in ['acceptancetestingaccepted']:
                    progressbar_value = 80
                elif metastate in ['completed', 'closed']:
                    progressbar_value = 100

            content.append(f'<div id="progressbar" class="ui-progressbar ui-widget ui-widget-content ui-corner-all" data-value="{progressbar_value}">')
            content.append(f'<div id="progressbar-label">{progressbar_value}%</div>')
            content.append('</div>')

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

        # Resolution and Customer
        content.append('<tr><th>Resolution</th><td>')
        resolution_distincts = self.resolutions
        content.append(self.create_html_select_block('resolution', resolution_distincts,
                                                     default='Please select...',
                                                     current_value=resolution))
        content.append('</td><th>Customer</th><td>')
        if customer:
            content.append('<input type="text" name="customer" size="40" value="'+customer+'" list="customerlist">')
        else:
            content.append('<input type="text" name="customer" size="40" list="customerlist">')

        content.append('<datalist id="customerlist">')
        for customer_distinct in self.cards_collection.find({'project': project}).distinct('customer'):
            content.append('<option value="'+customer_distinct+'">'+customer_distinct+'</option>')

        content.append('</datalist></td></tr>')
        card_document_ids = sorted(self.cards_collection.find({'project': project}).distinct('id'))
        content.append('<tr><th>Parent</th><td><select name="parent">')
        content.append('<option value="">Please select...</option>')
        for card_document_id in card_document_ids:
            potential_parent_card_document = self.cards_collection.find_one({'id': card_document_id})
            if 'title' in potential_parent_card_document:
                potential_parent_title = potential_parent_card_document['title']
            else:
                potential_parent_title = ""

            if card_document_id == parent:
                content.append('<option value="'+card_document_id+'" title="'+potential_parent_title+'" selected>'+card_document_id+'</option>')
            else:
                content.append('<option value="'+card_document_id+'" title="'+potential_parent_title+'">'+card_document_id+'</option>')

        content.append('</select></td><th>Blocks Parent</th><td>')
        if blocksparent:
            content.append('<input type="checkbox" name="blocksparent" checked>')
        else:
            content.append('<input type="checkbox" name="blocksparent">')

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

        # AffectsVersion and FixVersion
        content.append('<tr><th>Affects Version</th><td>')
        content.append('<select name="affectsversion">')
        content.append('<option class="warning" value="">Please select...</option>')
        release_distincts = self.projects_collection.find({'project': project}).distinct('releases.release')
        for release_distinct in release_distincts:
            if release_distinct == affectsversion:
                content.append('<option value="'+release_distinct+'" selected>'+release_distinct+'</option>')
            else:
                content.append('<option value="'+release_distinct+'">'+release_distinct+'</option>')

        content.append('</select></td><th>Fix Version</th><td><select name="fixversion">')
        content.append('<option class="warning" value="">Please select...</option>')
        release_distincts = self.projects_collection.find({'project': project}).distinct('releases.release')
        for release_distinct in release_distincts:
            if release_distinct == fixversion:
                content.append('<option value="'+release_distinct+'" selected>'+release_distinct+'</option>')
            else:
                content.append('<option value="'+release_distinct+'">'+release_distinct+'</option>')

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

        # EstimatedTime and ActualTime
        project_document = self.projects_collection.find_one({'project': project})
        unit = project_document.get('unit', 'day')
        content.append('<tr><th>Estimated Time</th><td>')
        if estimatedtime:
            content.append(f'<input type="number" name="estimatedtime" min="0.0" step="0.25" value="{estimatedtime}">')
        else:
            content.append('<input type="number" name="estimatedtime" min="0.0" step="0.25">')

        content.append(' '+unit+'s <input type="text" name="estimatedtimeexplanation" placeholder="Explanation">')
        children_estimated_time = self.amalgamate_child_numerical_values(id, 'estimatedtime')
        if children_estimated_time:
            content.append(f' <b class="childrenvalues">Children: {children_estimated_time}</b>')

        content.append('</td>')
        content.append('<th>Actual Time</th><td>')
        if actualtime:
            content.append(f'<input type="number" name="actualtime" min="0.0" step="0.25" value="{actualtime}">')
        else:
            content.append('<input type="number" name="actualtime" min="0.0" step="0.25" value="0">')

        content.append(' '+unit+'s  <input type="text" name="actualtimeexplanation" placeholder="Explanation">')
        children_actual_time = self.amalgamate_child_numerical_values(id, 'actualtime')
        if children_actual_time:
            content.append(f' <b class="childrenvalues">Children: {children_actual_time}</b>')

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

        # Cost
        currency = ""
        currency_distincts = self.projects_collection.find({'project': project}).distinct('currency')
        if len(currency_distincts) == 1:
            currency = currency_distincts[0]

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

        content.append('<tr><th>Estimated Cost</th><td>')
        content.append(currency_symbol)
        if estimatedcost:
            content.append(f'<input type="number" min="0.0" name="estimatedcost" value="{estimatedcost}">')
        else:
            content.append('<input type="number" min="0.0" name="estimatedcost" value="0">')

        content.append(' <input type="text" name="estimatedcostexplanation" placeholder="Explanation">')

        children_estimated_cost = self.amalgamate_child_numerical_values(id, 'estimatedcost')
        if children_estimated_cost:
            content.append(f' <b class="childrenvalues">Children: {children_estimated_cost}</b>')

        content.append('</td><th>Actual Cost</th><td>')
        content.append(currency_symbol)
        if actualcost:
            content.append(f'<input type="number" min="0.0" name="actualcost" value="{actualcost}">')
        else:
            content.append('<input type="number" min="0.0" name="actualcost" value="0">')

        content.append(' <input type="text" name="actualcostexplanation" placeholder="Explanation">')

        children_actual_cost = self.amalgamate_child_numerical_values(id, 'actualcost')
        if children_actual_cost:
            content.append(f' <b class="childrenvalues">Children: {children_actual_cost}</b>')

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

        # Severity and Priority
        content.append('<tr><th>Severity</th><td><select name="severity">')
        for severityDistinct in self.severities:
            if severity:
                if severityDistinct == severity:
                    content.append('<option value="'+severityDistinct+'" selected>'+severityDistinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+severityDistinct+'">'+severityDistinct.capitalize()+'</option>')

            else:
                if severityDistinct == 'medium':
                    content.append('<option value="'+severityDistinct+'" selected>'+severityDistinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+severityDistinct+'">'+severityDistinct.capitalize()+'</option>')

        content.append('</select></td><th>Priority</th><td><select name="priority">')
        for priority_distinct in self.priorities:
            if priority:
                if priority_distinct == priority:
                    content.append('<option value="'+priority_distinct+'" selected>'+priority_distinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+priority_distinct+'">'+priority_distinct.capitalize()+'</option>')

            else:
                if priority_distinct == 'medium':
                    content.append('<option value="'+priority_distinct+'" selected>'+priority_distinct.capitalize()+'</option>')
                else:
                    content.append('<option value="'+priority_distinct+'">'+priority_distinct.capitalize()+'</option>')

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

        # Before and After
        content.append('<tr><th>Before</th><td>')
        if before:
            content.append('<input type="text" name="before" size="40" value="'+before+'" list="beforelist">')
        else:
            content.append('<input type="text" name="before" size="40" list="beforelist">')

        content.append('<datalist id="beforelist">')
        query = {}
        if project:
            query['project'] = project

        if release:
            query['release'] = release

        if iteration:
            query['iteration'] = iteration

        for card_document in self.cards_collection.find(query):
            if 'title' in card_document and card_document['title']:
                content.append(f'<option value="{card_document["id"]}">{card_document["id"]}: {card_document["title"]}</option>')
            else:
                content.append(f'<option value="{card_document["id"]}">{card_document["id"]}</option>')

        content.append('</datalist></td><th>After</th><td>')
        if after:
            content.append('<input type="text" name="after" size="40" value="'+after+'" list="afterlist">')
        else:
            content.append('<input type="text" name="after" size="40" list="afterlist">')

        content.append('<datalist id="afterlist">')
        query = {}
        if project:
            query['project'] = project

        if release:
            query['release'] = release

        if iteration:
            query['iteration'] = iteration

        for card_document in self.cards_collection.find(query):
            if 'title' in card_document and card_document['title']:
                content.append(f'<option value="{card_document["id"]}">{card_document["id"]}: {card_document["title"]}</option>')
            else:
                content.append(f'<option value="{card_document["id"]}">{card_document["id"]}</option>')

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

        # Depends Upon and Difficulty
        content.append('<tr><th>Depends Upon</th><td>')
        if dependsupon:
            content.append('<input type="text" name="dependsupon" size="40" value="'+dependsupon+'" list="dependsuponlist">')
        else:
            content.append('<input type="text" name="dependsupon" size="40" list="dependsuponlist">')

        content.append('<datalist id="dependsuponlist">')
        query = {}
        if project:
            query['project'] = project

        if release:
            query['release'] = release

        if iteration:
            query['iteration'] = iteration

        for card_document in self.cards_collection.find(query):
            if 'title' in card_document and card_document["title"]:
                content.append(f'<option value="{card_document["id"]}">{card_document["id"]}: {card_document["title"]}</option>')
            else:
                content.append(f'<option value="{card_document["id"]}">{card_document["id"]}</option>')

        content.append('</datalist></td><th>Difficulty</th><td>')
        content.append(self.create_html_select_block('difficulty', ['easy', 'normal', 'hard'],
                                                     default=None, current_value=difficulty,
                                                     specials=['capitalise']))
        content.append('</td></tr>')

        # Start By and Next Action
        content.append('<tr><th><img src="/images/Calendar-icon-16.png"> Start By</th><td>')
        if startby:
            date_format = self.convert_time_to_display_format(startby)
            content.append('<input class="startby" type="text" name="startby" value="'+date_format+'">')
        else:
            content.append('<input class="startby" type="text" name="startby">')

        content.append('</td><th><img src="/images/Calendar-icon-16.png"> Next Action</th><td>')
        if nextaction:
            date_format = self.convert_time_to_display_format(nextaction)
            content.append('<input class="nextaction" type="text" name="nextaction" value="'+date_format+'">')
        else:
            content.append('<input class="nextaction" type="text" name="nextaction">')

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

        # Deadline
        content.append('<tr><th><img src="/images/Calendar-icon-16.png"> Deadline</th><td colspan="3">')
        if deadline:
            date_format = self.convert_time_to_display_format(deadline)
            content.append('<input class="deadline" type="text" name="deadline" value="'+date_format+'">')
        else:
            content.append('<input class="deadline" type="text" name="deadline">')

        start_date, end_date, scope = self.get_project_release_iteration_dates(project, release, iteration)
        if end_date:
            date_format = self.convert_time_to_display_format(end_date)
            content.append(' <b class="end_dateconfirmation" title="'+scope+'">End Date: '+date_format+'</b>')

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

        # Artifacts
        content.append('<tr><th>Artifacts</th><td colspan="3"><textarea name="artifacts" rows="5" cols="80">'+"\n".join(artifacts)+'</textarea></td></tr>')

        # Rules
        content.append('<tr><th>Rules</th><td colspan="3">')

        content.append('<table><tr><th>Rule</th><th>Usage</th><th>Status</th><th>Last Triggered</th></tr>')
        for rule_no, rule_document in enumerate(rules):
            modified_rule = rule_document['rule']
            # TODO - IS THERE A BETTER WAY TO DO THIS?
            modified_rule = modified_rule.replace('"', '&quot;')
            content.append(f'<tr><td><input type="text" name="rule{rule_no}" size="120" value="{modified_rule}"></td><td><select name="ruleusage{rule_no}">')
            for usage in ['Perpetual', 'Transient']:
                if 'usage' in rule_document and rule_document['usage'] and rule_document['usage'] == usage:
                    content.append('<option value="'+usage+'" selected>'+usage+'</option>')
                else:
                    content.append('<option value="'+usage+'">'+usage+'</option>')

            content.append('</select></td><td>')
            if 'status' in rule_document:
                content.append(rule_document['status'])

            content.append('</td><td>')
            if 'lasttriggered' in rule_document and rule_document['lasttriggered']:
                date_format = self.convert_time_to_display_format(rule_document['lasttriggered'])
                content.append(date_format)
            else:
                content.append('Never')

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

        rule_no = len(rules)
        if rule_no < 10:
            content.append(f'<tr><td><input type="text" size="120" name="rule{rule_no}"></td><td><select name="ruleusage{rule_no}">')
            for usage in ['Perpetual', 'Transient']:
                content.append(f'<option value="{usage}">{usage}</option>')

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

        content.append('</table>')

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

        # Test Cases
        content.append('<tr><th>Test Cases</th><td colspan="3">')

        content.append('<table><tr><th>Title</th><th>Description</th><th>State</th></tr>')
        for testcase_no, testcase_document in enumerate(testcases):
            testcase_description = ""
            if 'description' in testcase_document:
                testcase_description = testcase_document['description']

            content.append(f'<tr><td><input type="text" name="testcasetitle{testcase_no}" size="40" value="{testcase_document["title"]}"></td><td><input type="text" name="testcasedescription{testcase_no}" size="80" value="{testcase_description}"></td><td><select name="testcasestate{testcase_no}">')

            for testcase_state_distinct in ['defined', 'progressing', 'accepted']:
                if testcase_state_distinct == testcase_document['state']:
                    content.append(f'<option value="{testcase_state_distinct}" selected>{testcase_state_distinct.capitalize()}</option>')
                else:
                    content.append(f'<option value="{testcase_state_distinct}">{testcase_state_distinct.capitalize()}</option>')

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

        testcase_no = len(testcases)
        if testcase_no < 10:
            content.append(f'<tr><td><input type="text" size="40" name="testcasetitle{testcase_no}"></td><td><input type="text" size="80" name="testcasedescription{testcase_no}"></td><td><select name="testcasestate{testcase_no}">')
            for testcase_state_distinct in ['defined', 'progressing', 'accepted']:
                content.append('<option value="'+testcase_state_distinct+'">'+testcase_state_distinct.capitalize()+'</option>')

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

        content.append('</table>')

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

        # Root-Cause Analysis
        content.append('<tr><th>Root-Cause Analysis</th><td colspan="3"><textarea name="rootcauseanalysis" rows="5" cols="80">'+rootcauseanalysis+'</textarea></td></tr>')

        # CRM Case and Escalation
        content.append('<tr><th>CRM Case</th><td>')
        if crmcase:
            content.append('<input type="text" name="crmcase" size="40" value="'+crmcase+'">')
        else:
            content.append('<input type="text" name="crmcase" size="40">')

        content.append('</td><th><img src="/images/Link-icon-16.png"> Escalation</th><td>')
        if escalation:
            content.append('<input type="text" name="escalation" size="40" value="'+escalation+'">')
        else:
            content.append('<input type="text" name="escalation" size="40">')

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

        # External Reference
        content.append('<tr><th><img src="/images/Link-icon-16.png"> External Reference</th><td>')
        if externalreference:
            content.append('<input type="text" name="externalreference" size="40" value="'+externalreference+'">')
        else:
            content.append('<input type="text" name="externalreference" size="40">')

        content.append('</td><th><img src="/images/Link-icon-16.png"> External Hyperlink</th><td>')
        if externalhyperlink:
            content.append('<input type="text" name="externalhyperlink" size="40" value="'+externalhyperlink+'">')
        else:
            content.append('<input type="text" name="externalhyperlink" size="40">')

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

        # HiddenUntil
        content.append('<tr><th>Hidden Until</th><td><select name="hiddenuntil">')
        content.append('<option value="">Please select...</option>')
        hiddenuntil_date_format = ""
        if hiddenuntil:
            hiddenuntil_date_format = self.convert_time_to_display_format(hiddenuntil)

        for hiddenuntil_distinct in self.get_hidden_until_values():
            if hiddenuntil_distinct == hiddenuntil_date_format:
                content.append('<option value="'+hiddenuntil_distinct+'" selected>'+hiddenuntil_distinct+'</option>')
            else:
                content.append('<option value="'+hiddenuntil_distinct+'">'+hiddenuntil_distinct+'</option>')

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

        # Deferred and DeferredUntil
        content.append('<tr><th>Deferred</th><td>')
        if deferred:
            content.append('<input type="text" name="deferred" size="40" value="'+deferred+'">')
        else:
            content.append('<input type="text" name="deferred" size="40" placeholder="Reason">')

        content.append('</td><th><img src="/images/Calendar-icon-16.png"> Deferred Until</th><td>')
        if deferreduntil:
            date_format = self.convert_time_to_display_format(deferreduntil)
            content.append('<input class="deferreduntil" type="text" name="deferreduntil" value="'+date_format+'">')
        else:
            content.append('<input class="deferreduntil" type="text" name="deferreduntil">')

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

        # Blocked and BlockedUntil
        content.append('<tr><th>Blocked</th><td>')
        if blocked:
            content.append('<input type="text" name="blocked" size="40" value="'+blocked+'">')
        else:
            content.append('<input type="text" name="blocked" size="40" placeholder="Reason">')

        content.append('</td><th><img src="/images/Calendar-icon-16.png"> Blocked Until</th><td>')
        if blockeduntil:
            date_format = self.convert_time_to_display_format(blockeduntil)
            content.append('<input class="blockeduntil" type="text" name="blockeduntil" value="'+date_format+'">')
        else:
            content.append('<input class="blockeduntil" type="text" name="blockeduntil">')

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

        # Emotion and I'm stuck
        content.append('<tr><th>Emotion</th><td>')
        content.append(self.create_html_select_block('emotion', self.emotions.keys(), default='Please select...', current_value=emotion, specials=['capitalised']))
        content.append('</td><th>I\'m stuck</th><td>')
        if stuck:
            content.append('<input type="text" name="stuck" size="40" value="%s">' % stuck)
        else:
            content.append('<input type="text" name="stuck" size="40" placeholder="Reason">')

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

        # Broadcast and Question
        content.append('<tr><th>Broadcast</th><td>')
        if broadcast:
            content.append('<input type="text" name="broadcast" size="40" value="'+broadcast+'">')
        else:
            content.append('<input type="text" name="broadcast" size="40">')

        content.append('</td><th>Question</th><td>')
        if question:
            content.append('<input type="text" name="question" size="40" value="'+question+'">')
        else:
            content.append('<input type="text" name="question" size="40">')

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

        # Bypass Review and Recurring
        content.append('<tr><th>Bypass Review</th><td>')
        if bypassreview:
            content.append('<input type="checkbox" name="bypassreview" checked>')
        else:
            content.append('<input type="checkbox" name="bypassreview">')

        content.append('</td><th>Recurring</th><td>')
        if recurring:
            content.append('<input type="checkbox" name="recurring" checked>')
        else:
            content.append('<input type="checkbox" name="recurring">')

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

        # Comments
        content.append('<tr><th>Comments</th><td colspan="3">')
        content.append('<table>')
        if comments:
            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('<tr><th colspan="2">Add new comment</th></tr>')
        content.append('<tr><td colspan="2"><textarea name="comment" rows="5" cols="80"></textarea></td></tr>')
        content.append('</table>')
        content.append('</td></tr>')

        if 'customattributes' in project_document and project_document['customattributes']:
            content.append('<tr><td colspan="4"><h3>Custom Attributes</h3></td></tr>')
            for custom_attribute, _ in project_document['customattributes'].items():
                value = card_document.get(custom_attribute, '')
                content.append(f'<tr><th>{custom_attribute}</th><td><input type="text" name="{custom_attribute}" size="40" value="{value}"></td><td colspan="2"></td></tr>')

        content.append('<tr><td colspan="4" align="center"><span class="controlgroup"><input class="save" type="submit" value="Save"><input class="save" type="reset"></form><input class="save" type="button" value="Cancel" onclick="window.location=\'/kanban\'" /></span></td></tr>')

        content.append('</table>')

        if id:
            content.append('</td><td valign="top">')
            content.append('<h2 class="page_title">'+type.capitalize()+' As Is</h2>')
            content.append('<table width="50%"><tr><td>')
            content.append(self.assemble_tabbed_kanban_card(['owner', 'coowner'], ['display'], "", doc_id, False, 0))
            content.append('</td></tr></table>')

        content.append('</td></tr></table>')
        content.append('<script type="text/javascript" src="/scripts/kanban.js"></script>')

        content.append(Kanbanara.footer(self))
        return "".join(content)

    @cherrypy.expose
    def view_card(self, id):
        """Allows a card to be viewed"""
        username = Kanbanara.check_authentication(f'/{self.component}')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        content = []
        content.append(Kanbanara.header(self, 'view_card', "View Card"))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        card_document = self.cards_collection.find_one({'id': id})
        self.update_recent_cards(session_document, id)
        doc_id = card_document["_id"]
        actualcost = Decimal(card_document.get('actualcost', 0))
        actualtime = float(card_document.get('actualtime', 0))
        after = card_document.get('after', '')
        artifacts = card_document.get('artifacts', [])
        before = card_document.get('before', '')
        blocked = card_document.get('blocked', '')
        blockeduntil = card_document.get('blockeduntil', '')
        blocksparent = card_document.get('blocksparent', False)
        broadcast = card_document.get('broadcast', '')
        bypassreview = card_document.get('bypassreview', False)
        category = card_document.get('category', '')
        classofservice = card_document.get('classofservice', '')
        comments = card_document.get('comments', [])
        coowner = card_document.get('coowner', '')
        coownerstate = card_document.get('coownerstate', '')
        coreviewer = card_document.get('coreviewer', '')
        coreviewerstate = card_document.get('coreviewerstate', '')
        creator = card_document.get('creator', '')
        crmcase = card_document.get('crmcase', '')
        customer = card_document.get('customer', '')
        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', '')
        difficulty = card_document.get('difficulty', '')
        emotion = card_document.get('emotion', '')
        escalation = card_document.get('escalation', '')
        estimatedcost = Decimal(card_document.get('estimatedcost', 0))
        estimatedtime = float(card_document.get('estimatedtime', 0))
        externalhyperlink = card_document.get('externalhyperlink', '')
        externalreference = card_document.get('externalreference', '')
        hiddenuntil = card_document.get('hiddenuntil', '')
        id = card_document.get('id', '')
        iteration = card_document.get('iteration', '')
        nextaction = card_document.get('nextaction', '')
        notes = card_document.get('notes', '')
        owner = card_document.get('owner', '')
        ownerstate = card_document.get('ownerstate', '')
        parent = card_document.get('parent', '')
        priority = card_document.get('priority', '')
        project = card_document.get('project', '')
        question = card_document.get('question', '')
        recurring = card_document.get('recurring', '')
        release = card_document.get('release', '')
        resolution = card_document.get('resolution', '')
        reviewer = card_document.get('reviewer', '')
        reviewerstate = card_document.get('reviewerstate', '')
        severity = card_document.get('severity', '')
        startby = card_document.get('startby', '')
        state = card_document.get('state', '')
        status = card_document.get('status', '')
        stuck = card_document.get('stuck', '')
        subteam = card_document.get('subteam', '')
        tags = card_document.get('tags', '')
        title = card_document.get('title', '')
        card_type = card_document.get('type', '')
        project_document = self.projects_collection.find_one({'project': project})
        content.append('<h2 class="page_title">View '+card_type.capitalize()+'</h2>')
        content.append('<table class="view'+card_type+'">')

        # ID
        content.append('<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(f'ID</th><td><i>{id}</i></td><th>Title</th><td><div class="edit" id="{doc_id}:::title">{title}</div></td></tr>')

        # Title and Description
        content.append(f'<tr><th>Description</th><td colspan="3"><div class="edit" id="{doc_id}:::description">{description}</div></td></tr>')

        # Status
        content.append('<tr><th>Status</th><td colspan="3">')
        modified_status = self.format_multiline(status)
        content.append(f'<div class="edit_area" id="{doc_id}:::status">{modified_status}</div>')
        content.append('</td></tr>')

        # Notes
        content.append(f'<tr><th>Notes</th><td colspan="3"><div class="edit" id="{doc_id}:::notes">{notes}</div></td></tr>')

        # Creator and Class Of Service
        content.append(f'<tr><th>Creator</th><td><i>{creator}</i></td><th>Class Of Service</th><td>{classofservice}</td></tr>')

        # Owner and Co-Owner
        content.append(f'<tr><th>Owner</th><td>{owner}</td><th>Co-Owner</th><td>{coowner}</td></tr>')

        # Reviewer and Co-Reviewer
        content.append(f'<tr><th>Reviewer</th><td>{reviewer}</td><th>Co-Reviewer</th><td>{coreviewer}</td></tr>')

        # Project and Subteam
        content.append(f'<tr><th>Project</th><td>{project}</td><th>Subteam</th><td>{subteam}</td></tr>')

        # Release and Iteration
        content.append(f'<tr><th>Release</th><td>{release}</td><th>Iteration</th><td>{iteration}</td></tr>')

        # Category and Tags
        content.append(f'<tr><th>Category</th><td>{category}</td><th>Tags</th><td><div class="edit" id="{doc_id}:::tags">{tags}</div></td></tr>')

        # State
        content.append('<tr><th>State</th><td>'+state+'</td><td colspan="2">')
        progressbar_value = 0
        if state:
            project_document = self.projects_collection.find_one({'project': project})
            workflow_index = project_document.get('workflow_index', {})
            condensed_column_states = workflow_index.get('condensed_column_states', [])
            condensed_column_states_dict = workflow_index.get('condensed_column_states_dict', {})
            state_pos = condensed_column_states_dict[state]
            progressbar_value = int((state_pos / len(condensed_column_states)) * 100)

        _, _, centric, _, _ = self.get_associated_state_information(state)
        centric_message = False
        if centric == 'Owner':
            if owner and coowner:
                if ownerstate and not coownerstate:
                    if username == owner:
                        content.append(f'<p><i>Awaiting {coowner}!</i></p>')
                    else:
                        content.append(f'<p><i>{owner} has already moved to {ownerstate}</i></p>')

                    centric_message = True

                elif coownerstate and not ownerstate:
                    if username == coowner:
                        content.append(f'<p><i>Awaiting {owner}!</i></p>')
                    else:
                        content.append(f'<p><i>{coowner} has already moved to {coownerstate}</i></p>')

                    centric_message = True

        elif centric == 'Reviewer':
            if reviewer and coreviewer:
                if reviewerstate and not coreviewerstate:
                    if username == reviewer:
                        content.append(f'<p><i>Awaiting Co-Reviewer!</i></p>')
                    else:
                        content.append(f'<p><i>{reviewer} has already moved to {reviewerstate}</i></p>')

                    centric_message = True

                elif coreviewerstate and not reviewerstate:
                    if username == coreviewer:
                        content.append(f'<p><i>Awaiting Reviewer!</i></p>')
                    else:
                        content.append(f'<p><i>{coreviewer} has already moved to {coreviewerstate}</i></p>')

                    centric_message = True

        if not centric_message:
            content.append(f'<div id="progressbar" class="ui-progressbar ui-widget ui-widget-content ui-corner-all" data-value="{progressbar_value}">')
            content.append(f'''<div id="progressbar-label">{progressbar_value}%</div>''')
            content.append('</div>')

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

        # Resolution
        content.append(f'<tr><th>Resolution</th><td>{resolution}</td><th>Customer</th><td><div class="edit" id="{doc_id}:::customer">{customer}</div></td></tr>')

        # Parent and BlocksParent
        if parent:
            for parent_document in self.cards_collection.find({'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><table>')
                content.append('<tr><th>ID</th><th>Title</th><th>State</th><th>Children</th></tr>')
                content.append('<tr><td class="'+parent_type+'" title="'+parent_type.capitalize()+'">')
                buttons = self.ascertain_card_menu_items(parent_document, member_document)
                content.append(self.assemble_card_menu(member_document, parent_document, buttons, 'index'))
                content.append('<a href="/cards/view_card?id='+parent_id+'">'+parent_id+'</a></td>')
                content.append('<td>'+parent_title+'</td>')
                content.append('<td>'+parent_state+'</td><td>')
                content.append(str(self.cards_collection.find({"parent": parent_id}).count()))
                content.append('</td></tr>')
                content.append('</table></td><th>Blocks Parent</th><td>')
                if blocksparent:
                    content.append('True')
                else:
                    content.append('False')

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

        # Children
        type_distincts = []
        if card_type == 'epic':
            type_distincts = ['feature', 'story', 'enhancement', 'defect']
        elif card_type == 'feature':
            type_distincts = ['story', 'enhancement', 'defect']
        elif card_type in ['story', 'enhancement', 'defect']:
            type_distincts = ['story', 'enhancement', 'defect', 'task', 'test', 'bug']
        elif card_type == 'task':
            type_distincts = ['task', 'test', 'bug']
        elif card_type == 'test':
            type_distincts = ['bug']

        if type_distincts or self.cards_collection.find({"parent": id}).count():
            content.append('<tr><th>Children</th><td colspan="3"><table>')
            content.append('<tr><th>ID/Type</th><th>Title</th><th>State</th><th>Children</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(f'<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(f'<a href="/cards/view_card?id={child_id}">{child_id}</a></td>')
                content.append(f'<td>{child_title}</td><td>{child_state}</td><td>')
                content.append(str(self.cards_collection.find({"parent": child_id}).count()))
                content.append('</td></tr>')

            if type_distincts:
                content.append('<form action="/cards/add_child" method="post"><input type="hidden" name="parent" value="'+id+'"><tr><td>')
                content.append(self.create_html_select_block('type', type_distincts, specials=['capitalised']))
                content.append('</td><td><input type="text" name="title"></td><td><select name="state">')
                project_document = self.projects_collection.find_one({'project': project})
                workflow_index = project_document.get('workflow_index', {})
                uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
                for state_distinct in uncondensed_column_states:
                    content.append('<option value="'+state_distinct+'">'+state_distinct.capitalize()+'</option>')

                content.append('</select></td><td><input type="submit" value="Add Child"></td></tr></form>')

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

        # EstimatedTime and ActualTime
        unit = ""
        if project_document and 'unit' in project_document and project_document['unit']:
            unit = project_document['unit']

        content.append('<tr><th>Estimated Time</th><td>')
        if estimatedtime:
            if estimatedtime <= 1:
                et_unit = unit
            else:
                et_unit = unit+'s'

            content.append(str(estimatedtime)+' '+et_unit)

        content.append('</td>')
        content.append('<th>Actual Time</th><td>')
        if actualtime:
            if actualtime <= 1:
                at_unit = unit
            else:
                at_unit = unit+'s'

            content.append(str(actualtime)+' '+at_unit)

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

        # Cost
        currency = ""
        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 = ""

        content.append(f'<tr><th>Estimated Cost</th><td>{currency_symbol}{estimatedcost}</td><th>Actual Cost</th><td>{currency_symbol}{actualcost}</td></tr>')

        # Severity and Priority
        content.append(f'<tr><th>Severity</th><td>{severity}</td><th>Priority</th><td>{priority}</td></tr>')

        # Before and After
        content.append(f'<tr><th>Before</th><td>{before}</td><th>After</th><td>{after}</td></tr>')

        # Depends Upon and Difficulty
        content.append('<tr><th>Depends Upon</th><td>'+dependsupon+'</td><th>Difficulty</th><td>'+difficulty+'</td></tr>')

        # Start By and Next Action
        content.append('<tr><th>Start By</th><td>')
        if startby:
            content.append(self.convert_time_to_display_format(startby))

        content.append('</td><th>Next Action</th><td>')
        if nextaction:
            content.append(self.convert_time_to_display_format(nextaction))

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

        # Deadline
        content.append('<tr><th>Deadline</th><td colspan="3">')
        if deadline:
            content.append(self.convert_time_to_display_format(deadline))

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

        # Artifacts
        content.append('<tr><th>Artifacts</th><td colspan="3">'+'<br>'.join(artifacts)+'</td></tr>')

        # CRM Case and Escalation
        content.append(f'<tr><th>CRM Case</th><td><div class="edit" id="{doc_id}:::crmcase">{crmcase}</div></td><th>Escalation</th><td><div class="edit" id="{doc_id}:::escalation">{escalation}</div></td></tr>')

        # External Reference
        content.append(f'<tr><th>External Reference</th><td><div class="edit" id="{doc_id}:::externalreference">{externalreference}</div></td><th>External Hyperlink</th><td><a href="{externalhyperlink}">{externalhyperlink}</a></td></tr>')

        # HiddenUntil
        content.append('<tr><th>Hidden Until</th><td colspan="3">')
        if hiddenuntil:
            hiddenuntil_date_format = self.convert_time_to_display_format(hiddenuntil)
            content.append(hiddenuntil_date_format)

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

        # Deferred and DeferredUntil
        content.append(f'<tr><th>Deferred</th><td><div class="edit" id="{doc_id}:::deferred">{deferred}</div></td><th>Deferred Until</th><td>')
        if deferreduntil:
            content.append(self.convert_time_to_display_format(deferreduntil))

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

        # Blocked and BlockedUntil
        content.append(f'<tr><th>Blocked</th><td><div class="edit" id="{doc_id}:::blocked">{blocked}</div></td><th>Blocked Until</th><td>')
        if blockeduntil:
            content.append(self.convert_time_to_display_format(blockeduntil))

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

        # Emotion and I'm stuck
        content.append(f'<tr><th>Emotion</th><td>{emotion}</td><th>I\'m stuck</th><td><div class="edit" id="{doc_id}:::stuck">{stuck}</div></td></tr>')

        # Broadcast and Question
        content.append(f'<tr><th>Broadcast</th><td><div class="edit" id="{doc_id}:::broadcast">{broadcast}</div></td><th>Question</th><td><div class="edit" id="{doc_id}:::question">{question}</div></td></tr>')

        # Bypass Review and Recurring
        content.append('<tr><th>Bypass Review</th><td>')
        if bypassreview:
            content.append('True')
        else:
            content.append('False')

        content.append('</td><th>Recurring</th><td>')
        if recurring:
            content.append('True')
        else:
            content.append('False')

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

        # Comments
        content.append('<tr><th>Comments</th><td colspan="3">')
        if comments:
            content.append('<table>')
            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><p class="'+comment_class+'">'+modified_comment+'</p></td></tr>')

            content.append('</table>')

        content.append('</td></tr>')
        
        if 'customattributes' in project_document and project_document['customattributes']:
            content.append('<tr><td colspan="4"><h3>Custom Attributes</h3></td></tr>')
            for custom_attribute, _ in project_document['customattributes'].items():
                value = card_document.get(custom_attribute, '')
                content.append(f'<tr><th>{custom_attribute}</th><td><div class="edit" id="{doc_id}:::{custom_attribute}">{value}</div></td><td colspan="2"></td></tr>')
        
        content.append('</table>')
        content.append('</div>')

        if session_document and 'debug' in session_document and session_document['debug']:
            content.append('<div class="debug">')
            content.append('<p>Debug</p>')
            content.append('<table class="debug">')
            for key, value in card_document.items():
                if key.startswith('_'):
                    content.append(f'<tr><td>{key}</td><td>{value}</td></tr>') 
                   
            content.append('</table>')
            content.append('</div>')
            content.append('<p></p>')

        content.append('<script type="text/javascript" src="/scripts/kanban.js"></script>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

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

cherrypy.tree.mount(Cards(), '/cards', config=conf)
