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

'''Kanbanara's Visuals Component'''

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

from bson import ObjectId
import cherrypy
from kanbanara import Kanbanara
from mako.template import Template
from pymongo import MongoClient


class Visuals(Kanbanara):
    '''Kanbanara's Visuals Component'''

    def assemble_listview_table(self, owner_reviewer_search_criteria, member_document,
                                selected_project, selected_user, potential_keys, documents):
        """Assembles the table used on the listview page"""
        content = []
        if documents:
            total_estimatedtime = 0
            total_actualtime = 0
            total_estimatedcost = 0
            total_actualcost = 0
            content.append('<table class="sortable">')
            keys = []
            for potential_key in potential_keys:
                values = self.cards_collection.find(owner_reviewer_search_criteria).distinct(potential_key)
                if values:
                    if potential_key == 'project' and len(values) == 1 and values[0] == selected_project:
                        continue
                    elif potential_key in ['owner', 'coowner', 'reviewer', 'coreviewer'] and len(values) == 1 and values[0] == selected_user:
                        continue
                    elif potential_key in ['estimatedtime', 'actualtime'] and len(values) == 1 and values[0] == 0:
                        continue
                    elif potential_key in ['actualtime', 'actualcost'] and len(values) == 1 and values[0] == '0':
                        continue
                    elif potential_key in ['estimatedcost'] and len(values) == 1 and values[0] == 0.0:
                        continue

                    keys.append(potential_key)

            content.append('<thead><tr><td></td>')
            for key in keys:
                content.append('<th><span>')
                if key in ['actualtime', 'estimatedtime']:
                    content.append('<a href="/kanban/times_report">'+self.displayable_key(key)+'</a>')
                elif key in ['actualcost', 'estimatedcost']:
                    content.append(f'<a href="/kanban/costs_report">{self.displayable_key(key)}</a>')
                elif key in ['affectsversion', 'deadline', 'dependsupon', 'externalhyperlink',
                             'externalreference', 'fixversion', 'iteration', 'nextaction', 'owner',
                             'priority', 'project', 'release', 'reviewer', 'rootcauseanalysis',
                             'classofservice', 'stuck']:
                    content.append(self.displayable_key(key))
                else:
                    content.append(key.capitalize())

                content.append('</span></th>')

            content.append('</tr></thead><tbody>')
            for document in documents:
                doc_id = document.get('_id', '')
                card_id = document.get('id', '')
                content.append('<tr><td>')
                buttons = self.ascertain_card_menu_items(document, member_document)
                content.append(self.assemble_card_menu(member_document, document, buttons, 'listview'))
                content.append('</td>')
                for key in keys:
                    if key in document:
                        if key == 'id':
                            content.append('<td><a name="'+card_id+'"></a>')
                            content.append(f'<a href="/{self.get_page_component("view_card")}/view_card?id={document["id"]}">{document[key]}</a></td>')
                        elif key in ['dependsupon', 'parent']:
                            content.append(f'<td><a href="#{document[key]}">{document[key]}</a></td>')
                        elif key in ['deadline', 'nextaction']:
                            if document[key]:
                                date_format = self.convert_time_to_display_format(document[key])
                                content.append('<td>'+date_format+'</td>')
                            else:
                                content.append('<td></td>')

                        elif key == 'externalhyperlink':
                            content.append(f'<td><a href="{document[key]}" title="{document[key]}">[Link]</a></td>')
                        elif key in ['blocked', 'iteration', 'owner', 'priority', 'project',
                                     'release', 'reviewer', 'status', 'stuck']:
                            content.append(f'<td>{document[key]}</td>')
                        elif key == 'type':
                            content.append(f'<td class="{document[key]}">{document[key]}</td>')
                        elif key == "title":
                            content.append(f'<td><div class="edit" id="{doc_id}:::title">{document[key]}</div></td>')
                        elif key == 'estimatedtime':
                            total_estimatedtime += float(document[key])
                            content.append(f'<td align="right">{document[key]}</td>')
                        elif key == 'actualtime':
                            total_actualtime += float(document[key])
                            content.append(f'<td align="right">{document[key]}</td>')
                        elif key == 'estimatedcost':
                            total_estimatedcost += float(document[key])
                            content.append(f'<td align="right">{document[key]}</td>')
                        elif key == 'actualcost':
                            total_actualcost += float(document[key])
                            content.append(f'<td align="right">{document[key]}</td>')
                        else:
                            content.append(f'<td>{document[key]}</td>')

                    else:
                        content.append('<td></td>')

                content.append('</tr>')

            content.append('</tbody><tfoot><tr><td></td>')
            for key in keys:
                if key == 'estimatedtime':
                    content.append(f'<td align="right" title="Total for {self.displayable_key(key)}">{total_estimatedtime}</td>')
                elif key == 'actualtime':
                    content.append(f'<td align="right" title="Total for {self.displayable_key(key)}">{total_actualtime}</td>')
                elif key == 'estimatedcost':
                    content.append(f'<td align="right" title="Total for {self.displayable_key(key)}">{total_estimatedcost}</td>')
                elif key == 'actualcost':
                    content.append(f'<td align="right" title="Total for {self.displayable_key(key)}">{total_actualcost}</td>')
                else:
                    content.append('<td></td>')

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

        return "".join(content)

    @cherrypy.expose
    def listview(self):
        """Presents all card types as a list view"""
        Kanbanara.check_authentication(f'/{self.component}/listview')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        content.append(Kanbanara.header(self, "listview", "List View"))
        content.append(Kanbanara.filter_bar(self, 'listview'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, "listview", "List View"))
        member_document = Kanbanara.get_member_document(self, session_document)
        selected_user = member_document.get('teammember', '')
        selected_project = member_document.get('project', '')
        selected_attributes = member_document.get('listview', ['hierarchy', 'id', 'type', 'title', 'state', 'resolution', 'description',
                              'affectsversion', 'fixversion', 'notes', 'status', 'customer', 'deferred',
                              'deferreduntil', 'blocked', 'blockeduntil', 'stuck', 'project', 'release', 'iteration',
                              'creator', 'owner', 'coowner', 'reviewer', 'coreviewer', 'estimatedtime', 'actualtime',
                              'estimatedcost', 'actualcost', 'severity', 'priority', 'nextaction', 'deadline',
                              'dependsupon', 'escalation', 'crmcase', 'externalreference', 'externalhyperlink', 'emotion',
                              'rootcauseanalysis', 'broadcast', 'classofservice'])
        _, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        if self.cards_collection.find(owner_reviewer_search_criteria).count():
            documents = self.cards_collection.find(owner_reviewer_search_criteria)
            content.append(self.assemble_listview_table(owner_reviewer_search_criteria,
                                                        member_document, selected_project,
                                                        selected_user, selected_attributes,
                                                        documents))
        else:
            content.append('<h3>Currently, there are no cards which match your filter settings!</h3>')

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

    @cherrypy.expose
    def listview_settings(self):
        """Allows the attributes shown on listview to be user defined"""
        Kanbanara.check_authentication(f'/{self.component}/listview_settings')
        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)
        listview_attributes = member_document.get('listview', [])
        content = []
        content.append(Kanbanara.header(self, 'listview_settings', 'Listview Settings'))
        content.append(Kanbanara.filter_bar(self, 'listview_settings'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, 'listview_settings', 'Listview Settings'))
        content.append('<div align="center">')
        content.append(f'<form action="/{self.component}/update_listview_settings" method="post"><table class="form">')
        selectable_attributes = ['actualcost', 'actualtime', 'affectsversion', 'after', 'artifacts',
                                 'before', 'blocked', 'blockeduntil', 'blocksparent', 'broadcast',
                                 'bypassreview', 'category', 'classofservice', 'comments',
                                 'coowner', 'coreviewer', 'creator', 'crmcase', 'customer',
                                 'deadline', 'deferred', 'deferreduntil', 'dependsupon',
                                 'description', 'difficulty', 'emotion', 'escalation',
                                 'estimatedcost', 'estimatedtime', 'expedite', 'externalhyperlink',
                                 'externalreference', 'fixversion', 'focusby', 'focusstart',
                                 'hashtags', 'hiddenuntil', 'id', 'iteration', 'lastchanged',
                                 'lastchangedby', 'lasttouched', 'lasttouchedby', 'nextaction',
                                 'notes', 'owner', 'parent', 'priority', 'project', 'question',
                                 'reassigncoowner', 'reassigncoreviewer', 'reassignowner',
                                 'reassignreviewer', 'recurring', 'release', 'resolution',
                                 'reviewer', 'rootcauseanalysis', 'rules', 'severity', 'startby',
                                 'state', 'status', 'stuck', 'subteam', 'tags', 'testcases',
                                 'title', 'type', 'votes']
        content.append('<tr>')
        row_count = 0
        for attribute in selectable_attributes:
            content.append(f'<td><input name="{attribute}" type="checkbox"> {self.displayable_key(attribute)}</td>')
            row_count += 1
            if row_count == 5:
                content.append('</tr><tr>')
                row_count = 0

        content.append('</tr><tr><td colspan="5" align="center"><input type="submit" value="Update"></td></tr>')
        content.append('</table></form></div>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    @cherrypy.expose
    def update_listview_settings(self, actualcost="", actualtime="", affectsversion="", after="",
                                 artifacts="", before="", blocked="", blockeduntil="",
                                 blocksparent="", broadcast="", bypassreview="", category="",
                                 classofservice="", comments="", coowner="", coreviewer="",
                                 creator="", crmcase="", customer="", deadline="", deferred="",
                                 deferreduntil="", dependsupon="", description="", difficulty="",
                                 emotion="", escalation="", estimatedcost="", estimatedtime="",
                                 expedite="", externalhyperlink="", externalreference="",
                                 fixversion="", focusby="", focusstart="", hashtags="",
                                 hiddenuntil="", id="", iteration="", lastchanged="",
                                 lastchangedby="", lasttouched="", lasttouchedby="", nextaction="",
                                 notes="", owner="", parent="", priority="", project="",
                                 question="", reassigncoowner="", reassigncoreviewer="",
                                 reassignowner="", reassignreviewer="", recurring="", release="",
                                 resolution="", reviewer="", rootcauseanalysis="", rules="",
                                 severity="", startby="", state="", status="", stuck="", subteam="",
                                 tags="", testcases="", title="", type="", votes=""):
        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)
        listview_attributes = []
        for (attribute, variable) in [('parent', parent),
                                      ('id', id),
                                      ('title', title),
                                      ('description', description),
                                      ('type', type),
                                      ('classofservice', classofservice),
                                      ('customer', customer),
                                      ('crmcase', crmcase),
                                      ('escalation', escalation),
                                      ('creator', creator),
                                      ('owner', owner),
                                      ('coowner', coowner),
                                      ('reviewer', reviewer),
                                      ('coreviewer', coreviewer),
                                      ('project', project),
                                      ('release', release),
                                      ('iteration', iteration),
                                      ('priority', priority),
                                      ('severity', severity),
                                      ('nextaction', nextaction),
                                      ('deadline', deadline),
                                      ('estimatedcost', estimatedcost),
                                      ('actualcost', actualcost),
                                      ('estimatedtime', estimatedtime),
                                      ('actualtime', actualtime),
                                      ('notes', notes),
                                      ('comments', comments),
                                      ('blocked', blocked),
                                      ('blocksparent', blocksparent),
                                      ('blockeduntil', blockeduntil),
                                      ('deferred', deferred),
                                      ('deferreduntil', deferreduntil),
                                      ('hiddenuntil', hiddenuntil),
                                      ('question', question),
                                      ('stuck', stuck),
                                      ('lasttouched', lasttouched),
                                      ('lasttouchedby', lasttouchedby),
                                      ('focusby', focusby),
                                      ('focusstart', focusstart),
                                      ('lastchanged', lastchanged),
                                      ('lastchangedby', lastchangedby),
                                      ('state', state),
                                      ('resolution', resolution),
                                      ('recurring', recurring),
                                      ('rootcauseanalysis', rootcauseanalysis),
                                      ('affectsversion', affectsversion),
                                      ('after', after),
                                      ('artifacts', artifacts),
                                      ('before', before),
                                      ('broadcast', broadcast),
                                      ('bypassreview', bypassreview),
                                      ('category', category),
                                      ('dependsupon', dependsupon),
                                      ('difficulty', difficulty),
                                      ('emotion', emotion),
                                      ('expedite', expedite),
                                      ('externalhyperlink', externalhyperlink),
                                      ('externalreference', externalreference),
                                      ('fixversion', fixversion),
                                      ('hashtags', hashtags),
                                      ('reassigncoowner', reassigncoowner),
                                      ('reassigncoreviewer', reassigncoreviewer),
                                      ('reassignowner', reassignowner),
                                      ('reassignreviewer', reassignreviewer),
                                      ('rules', rules),
                                      ('startby', startby),
                                      ('status', status),
                                      ('subteam', subteam),
                                      ('tags', tags),
                                      ('testcases', testcases),
                                      ('votes', votes),
                                     ]:
            if variable:
                listview_attributes.append(attribute)

        member_document['listview'] = listview_attributes
        self.members_collection.save(member_document)
        raise cherrypy.HTTPRedirect(f'/{self.component}/listview', 302)

    @cherrypy.expose
    def timeline(self):
        """Displays a timeline for the past 14 days"""
        Kanbanara.check_authentication(f'/{self.component}/timeline')
        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, "timeline", "Timeline"))
        content.append(Kanbanara.filter_bar(self, 'timeline'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "timeline",
                                                              "Timeline"))
        epoch = datetime.datetime.utcnow()
        two_weeks_ago_epoch = epoch - (self.timedelta_week * 2)
        day_count = 0
        day_labels = []
        day_epochs = []
        while day_count < 14:
            day_count += 1
            past_epoch = two_weeks_ago_epoch + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_display_format(past_epoch)
            day_of_week = self.day_of_week[past_epoch.weekday()]
            day_labels.append(date_format+'<br>'+day_of_week)
            day_epochs.append(past_epoch)

        content.append('<div align="center"><table class="admin"><tr>')
        for day_label in day_labels:
            content.append('<th>'+day_label+'</th>')

        content.append('</tr><tr>')
        project_document = self.projects_collection.find_one({'project': member_document['project']})
        workflow_index = project_document.get('workflow_index', {})
        uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document,
                                                                                 uncondensed_column_states)
        for day_epoch in day_epochs:
            content.append('<td valign="top">')
            min_day_epoch, max_day_epoch = self.min_max_day_epoch(day_epoch)
            owner_reviewer_search_criteria['$and'] = [{'history.epoch': {'$gte': min_day_epoch}},
                                                      {'history.epoch': {'$lte': max_day_epoch}}]
            for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                content.append('<table class="'+card_document['type']+'"><tr><th>')
                content.append(f'<a href="/{self.get_page_component("view_card")}/view_card?id={card_document["id"]}">{card_document["id"]}</a>')
                if card_document.get('title', ''):
                    content.append(' : ' + card_document['title'])

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

            content.append('</td>')

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

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

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

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

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

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

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

        # Connect to 'cards' collection
        self.cards_collection = db['cards']

    @cherrypy.expose
    def dashboard(self):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}/dashboard')
        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, 'dashboard', "Dashboard"))
        content.append(Kanbanara.filter_bar(self, 'dashboard'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'dashboard',
                                                              'Dashboard'))

        for cell in ['topleft', 'topcentre', 'topright',
                     'middleleft', 'middlecentre', 'middleright',
                     'bottomleft', 'bottomcentre', 'bottomright']:
            content.append('<div class="dashboardcomponent">')
            content.append(Kanbanara.component_recidivism_rate(self))
            content.append('</div>')

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

    @cherrypy.expose
    def dashboard_settings(self):
        """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 = []
        content.append(Kanbanara.header(self, 'dashboard_settings', "Dashboard Settings"))
        content.append(Kanbanara.filter_bar(self, 'dashboard_settings'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document,
                                                              'dashboard_settings',
                                                              'Dashboard Settings'))
        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 releasekickoff(self, page="1"):
        """ Comment """
        username = Kanbanara.check_authentication(f'/{self.component}/releasekickoff')
        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, "releasekickoff","Release Kick Off"))
        content.append(Kanbanara.filter_bar(self, 'releasekickoff'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "releasekickoff",
                                                              "Release Kick Off"))
        if member_document.get('project', ''):
            project_document = self.projects_collection.find_one({'project': member_document['project']})
            workflow_index = project_document.get('workflow_index', {})
            uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])

        if member_document.get('release', ''):
            page = int(page)
            nextpage = page + 1
            content.append('<div align="center">')
            if page == 1:
                # Title Page
                epoch = datetime.datetime.utcnow()
                release = member_document['release']
                date_format = self.convert_time_to_display_format(epoch)
                content.append('<h1>'+release+' Release Kick Off<br>Date: '+date_format+'<br>Presenter: '+member_document['fullname']+'</h1>')
            elif page == 2:
                # Start and End Dates
                if 'releases' in project_document:
                    for release_document in project_document['releases']:
                        if release_document['release'] == member_document['release']:
                            if 'start_date' in release_document:
                                start_date = release_document['start_date']
                                date_format = self.convert_time_to_display_format(start_date)
                                content.append('<h1>Start Date: '+date_format+'</h1>')

                            if 'end_date' in release_document:
                                end_date = release_document['end_date']
                                date_format = self.convert_time_to_display_format(end_date)
                                content.append('<h1>End Date: '+date_format+'</h1>')

                            break

            elif page == 3:
                # Proposed Epics
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
                owner_reviewer_search_criteria['type'] = 'epic'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Proposed Epics</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/releasekickoff?page="+str(nextpage), 302)

            elif page == 4:
                # Proposed Features
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
                owner_reviewer_search_criteria['type'] = 'feature'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Proposed Features</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/releasekickoff?page="+str(nextpage), 302)

            elif page == 5:
                # Proposed Stories
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
                owner_reviewer_search_criteria['type'] = 'story'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Proposed Stories</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/releasekickoff?page="+str(nextpage), 302)

            elif page == 6:
                # Proposed Enhancements
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
                owner_reviewer_search_criteria['type'] = 'enhancement'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Proposed Enhancements</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/releasekickoff?page="+str(nextpage), 302)

            elif page == 7:
                # Proposed Defects
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
                owner_reviewer_search_criteria['type'] = 'defect'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Proposed Defects</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/releasekickoff?page="+str(nextpage), 302)

            elif page == 8:
                content.append('<h1>Any Questions?</h1>')

            if nextpage < 8:
                content.append(f'<form id="kickoff" action="/kanban/releasekickoff" method="post"><input type="hidden" name="page" value="{nextpage}"><input type="submit" value="Next Page"></form>')

            content.append('</div>')

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

    @cherrypy.expose
    def retrospective(self, page="1"):
        """ Comment """
        username = Kanbanara.check_authentication(f'/{self.component}/retrospective')
        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, "retrospective", "Retrospective"))
        content.append(Kanbanara.filter_bar(self, 'retrospective'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "retrospective",
                                                              "Retrospective"))
        if member_document.get('project', ''):
            project_document = self.projects_collection.find_one({'project': member_document['project']})

        if member_document.get('release', ''):
            page = int(page)
            nextpage = page + 1
            content.append('<div align="center">')
            if page == 1:
                # Title Page
                epoch = datetime.datetime.utcnow()
                release = member_document['release']
                date_format = self.convert_time_to_display_format(epoch)
                content.append('<h1>'+release+' Retrospective<br>Date: '+date_format+'<br>Presenter: '+member_document['fullname']+'</h1>')
            elif page == 2:
                # Start and End Dates
                if 'releases' in project_document:
                    for release_document in project_document['releases']:
                        if release_document['release'] == member_document['release']:
                            if 'start_date' in release_document:
                                start_date = release_document['start_date']
                                date_format = self.convert_time_to_display_format(start_date)
                                content.append('<h1>Start Date: '+date_format+'</h1>')

                            if 'end_date' in release_document:
                                end_date = release_document['end_date']
                                date_format = self.convert_time_to_display_format(end_date)
                                content.append('<h1>End Date: '+date_format+'</h1>')

                            break

            elif page == 3:
                # Completed Epics
                states = self.get_custom_states_mapped_onto_metastates(['closed'])
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
                owner_reviewer_search_criteria['resolution'] = 'Released'
                owner_reviewer_search_criteria['type'] = 'epic'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Completed Epics</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/retrospective?page="+str(nextpage), 302)

            elif page == 4:
                # Completed Features
                states = self.get_custom_states_mapped_onto_metastates(['closed'])
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
                owner_reviewer_search_criteria['resolution'] = 'Released'
                owner_reviewer_search_criteria['type'] = 'feature'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Completed Features</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/retrospective?page="+str(nextpage), 302)

            elif page == 5:
                # Completed Stories
                states = self.get_custom_states_mapped_onto_metastates(['closed'])
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
                owner_reviewer_search_criteria['resolution'] = 'Released'
                owner_reviewer_search_criteria['type'] = 'story'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Completed Stories</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/retrospective?page="+str(nextpage), 302)

            elif page == 6:
                # Completed Enhancements
                states = self.get_custom_states_mapped_onto_metastates(['closed'])
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
                owner_reviewer_search_criteria['resolution'] = 'Released'
                owner_reviewer_search_criteria['type'] = 'enhancement'
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Enhancements</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/retrospective?page="+str(nextpage), 302)

            elif page == 7:
                # Completed Defects
                states = self.get_custom_states_mapped_onto_metastates(['closed'])
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
                owner_reviewer_search_criteria['resolution'] = 'Released'
                owner_reviewer_search_criteria['type'] in {'$in': ['bug', 'defect']}
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append('<h1>Completed Defects</h1>')
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/retrospective?page="+str(nextpage), 302)

            elif page == 8:
                # Abandoned/Won't Fix Cards
                states = self.get_custom_states_mapped_onto_metastates(['closed'])
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
                owner_reviewer_search_criteria['resolution'] = {'$in': ['Abandoned', "Won't fix"]}
                if self.cards_collection.find(owner_reviewer_search_criteria).count():
                    content.append("<h1>Abandoned / Won't Fix Cards</h1>")
                    content.append('<table class="sortable"><tr><th>Title</th><th>Owner</th></tr>')
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if 'title' in card_document:
                            content.append('<tr><td>'+card_document['title']+'</td>')
                            if 'owner' in card_document and 'coowner' in card_document:
                                content.append('<td>'+card_document['owner']+'/'+card_document['coowner']+'</td>')
                            elif 'owner' in card_document:
                                content.append('<td>'+card_document['owner']+'</td>')

                            content.append('</tr>')

                    content.append('</table>')
                else:
                    raise cherrypy.HTTPRedirect("/kanban/retrospective?page="+str(nextpage), 302)

            elif page == 9:
                content.append('<h1>Any Questions?</h1>')

            if nextpage < 9:
                content.append(f'<form id="kickoff" action="/kanban/retrospective" method="post"><input type="hidden" name="page" value="{nextpage}"><input type="submit" value="Next Page"></form>')

            content.append('</div>')

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

    @cherrypy.expose
    def activity_stream(self):
        """ Comment """
        username = Kanbanara.check_authentication(f'/{self.component}/activity_stream')
        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 = []
        epoch = datetime.datetime.utcnow()
        content.append(Kanbanara.header(self, "activity_stream", "Activity Stream"))
        content.append(Kanbanara.filter_bar(self, 'activity_stream'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "activity_stream",
                                                              "Activity Stream"))
        content.append('<div align="center">')
        required_columns, required_states = self.get_displayable_columns()
        owner_search_criteria, reviewer_search_criteria, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        owner_reviewer_search_criteria['history.epoch'] = {'$gte': epoch-self.timedelta_week}
        change_logs = []
        for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
            history = card_document.get('history')
            for history_document in reversed(history):
                if history_document['datetime'] > epoch-self.timedelta_week:
                    doc_id = card_document.get('_id')
                    epoch = history_document['datetime']
                    mode = history_document.get('mode', '')
                    attribute = history_document.get('attribute', '')
                    value = history_document.get('value', '')
                    username = history_document.get('username', '')
                    change_logs.append((epoch, doc_id, mode, attribute, value, username))

        if change_logs:
            change_logs.sort(reverse=True)
            content.append('<table  class="sortable"><thead><tr><th></th><th><span>Epoch</span></th><th><span>ID</span></th><th><span>Title</span></th><th><span>Mode</span></th><th><span>Attribute</span></th><th><span>Value</span></th><th><span>Username</span></th></tr></thead><tbody>')
            for (epoch, doc_id, mode, attribute, value, username) in change_logs:
                for card_document in self.cards_collection.find({'_id': ObjectId(doc_id)}):
                    card_id = card_document.get('id', '')
                    title = card_document.get('title', '')
                    content.append('<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><td>')
                    date_format = self.convert_time_to_display_format(epoch)
                    content.append(date_format+'</td><td>'+card_id+'</td><td>'+title+'</td><td>'+mode+'</td><td>'+attribute+'</td><td>'+value+'</td><td>'+username+'</td></tr>')
                    break

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

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

    @cherrypy.expose
    def diary(self):
        """Shows what cards will require attention in the coming 28 days"""
        Kanbanara.check_authentication(f'/{self.component}/diary')
        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, "diary","Diary"))
        content.append(Kanbanara.filter_bar(self, 'diary'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "diary", "Diary"))
        epoch = datetime.datetime.utcnow()
        day_count = 0
        day_labels = []
        day_epochs = []
        while day_count < 28:
            future_epoch = epoch + (self.timedelta_day * day_count)
            date_format = self.convert_time_to_display_format(future_epoch)
            day_of_week = self.day_of_week[future_epoch.weekday()]
            day_labels.append(date_format+'<br>'+day_of_week)
            day_epochs.append(future_epoch)
            day_count += 1

        project_document = self.projects_collection.find_one({'project': member_document['project']})
        workflow_index = project_document.get('workflow_index', {})
        uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
        content.append('<div align="center"><table class="diary">')
        already_displayed = []
        for week_start in [0, 7, 14, 21]:
            content.append('<tr>')
            for dl in range(week_start, week_start+7):
                content.append('<th>'+day_labels[dl]+'</th>')

            content.append('</tr>')
            for de in range(week_start, week_start+7):
                content.append('<td valign="top">')
                min_day_epoch, max_day_epoch = self.min_max_day_epoch(day_epochs[de])
                for action_attribute in ['deadline', 'nextaction', 'startby', 'hiddenuntil']:
                    owner_reviewer_search_criteria['$and'] = [{action_attribute: {'$gte': min_day_epoch}},
                                                              {action_attribute: {'$lte': max_day_epoch}}]
                    for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                        if card_document['id'] not in already_displayed:
                            content.append('<table class="'+card_document['type']+'"><tr><th>')
                            content.append('<a href="/kanban/view_card?id='+card_document['id']+'">'+card_document['id']+'</a>')
                            if 'title' in card_document and card_document['title']:
                                content.append(' : ' + card_document['title'])

                            content.append('</th></tr></table>')
                            already_displayed.append(card_document['id'])

                content.append('</td>')

            content.append('</tr>')

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

    @cherrypy.expose
    def timesheet(self):
        """ Comment """
        username = Kanbanara.check_authentication(f'/{self.component}/timesheet')
        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, "timesheet", "Time Sheet"))
        content.append(Kanbanara.filter_bar(self, 'timesheet'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, "timesheet", "Time Sheet"))
        epoch = datetime.datetime.utcnow()
        content.append('<div class="tabs" class="ui-tabs ui-widget ui-widget-content ui-corner-all">')
        content.append('<ul class="tabs" class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all">')
        content.append('<li class="ui-state-default ui-corner-top ui-tabs-selected ui-state-active"><a href="#4weekagotab">4 Weeks Ago</a></li>')
        content.append('<li class="ui-state-default ui-corner-top"><a href="#3weekagotab">3 Weeks Ago</a></li>')
        content.append('<li class="ui-state-default ui-corner-top"><a href="#2weekagotab">2 Weeks Ago</a></li>')
        content.append('<li class="ui-state-default ui-corner-top"><a href="#1weekagotab">1 Week Ago</a></li>')
        content.append('</ul>')

        for weekNumber in [4, 3, 2, 1]:
            content.append(f'<div class="tab_content" id="{weekNumber}weekagotab" class="ui-tabs-panel ui-widget-content ui-corner-bottom">')
            start_of_week_epoch = epoch - (self.timedelta_week * weekNumber)
            day_count = 0
            day_labels = []
            day_epochs = []
            while day_count < 7:
                day_count += 1
                past_epoch = start_of_week_epoch + (self.timedelta_day * day_count)
                date_format = self.convert_time_to_display_format(past_epoch)
                day_of_week = self.day_of_week[past_epoch.weekday()]
                day_labels.append(date_format+'<br>'+day_of_week)
                day_epochs.append(past_epoch)

            content.append('<div align="center"><table class="admin"><tr>')
            for day_label in day_labels:
                content.append('<th>'+day_label+'</th>')

            content.append('</tr><tr>')
            project_document = self.projects_collection.find_one({'project': member_document['project']})
            workflow_index = project_document.get('workflow_index', {})
            uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, uncondensed_column_states)
            for day_epoch in day_epochs:
                content.append('<td valign="top">')
                min_day_epoch, max_day_epoch = self.min_max_day_epoch(day_epoch)
                owner_reviewer_search_criteria['$and'] = [{'actualtimehistory.epoch': {'$gte':min_day_epoch}},{'actualtimehistory.epoch': {'$lte':max_day_epoch}}]
                for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
                    latest_actualtime_epoch = card_document['actualtimehistory'][-1]['datetime']
                    if min_day_epoch <= latest_actualtime_epoch <= max_day_epoch:
                        previous_actualtime = 0
                        explanation = ""
                        latest_actualtime = card_document['actualtimehistory'][-1]['actualtime']
                        if 'explanation' in card_document['actualtimehistory'][-1]:
                            explanation = card_document['actualtimehistory'][-1]['explanation']

                        if len(card_document['actualtimehistory']) > 1:
                            previous_actualtime = card_document['actualtimehistory'][-2]['actualtime']
                            latest_actualtime -= previous_actualtime

                        if latest_actualtime:
                            content.append('<table class="'+card_document['type']+'"><tr><th>')
                            content.append(f'<a href="/{self.get_page_component("view_card")}/view_card?id={card_document["id"]}">{card_document["id"]}</a>')
                            if 'title' in card_document and card_document['title']:
                                content.append(' : ' + card_document['title'])

                            content.append('<sup class="time"')
                            if explanation:
                                content.append(f' title="{explanation}"')

                            content.append(f'>{latest_actualtime}</sup>')
                            content.append('</th></tr></table>')

                content.append('</td>')

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

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

    @cherrypy.expose
    def roadmap(self):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}/roadmap')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        content.append(Kanbanara.header(self, 'roadmap', "Roadmap"))
        content.append(Kanbanara.filter_bar(self, 'roadmap'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'roadmap', 'Roadmap'))
        member_document = Kanbanara.get_member_document(self, session_document)
        project = member_document.get('project', '')
        selectable_releases = []
        for project_document in self.projects_collection.find({'project': project}):
            if 'releases' in project_document:
                for release_document in project_document['releases']:
                    release = release_document['release']
                    release_start_date = datetime.timedelta()
                    release_end_date = datetime.timedelta()
                    if 'start_date' in release_document and release_document['start_date']:
                        release_start_date = release_document['start_date']

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

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

        selectable_releases.sort()
        content.append('<table class="admin"><tr>')
        for release_no, (release_start_date, release_end_date, release) in enumerate(selectable_releases):
            if release_no < 10:
                content.append('<th>'+release+'</th>')

        content.append('</tr>')

        content.append('<tr>')
        for release_no, (release_start_date, release_end_date, release) in enumerate(selectable_releases):
            if release_no < 10:
                content.append(f'<td valign="top"><div id="roadmap{release_no}"><!-- This section is automatically populated by JQuery and Ajax --><p>Please enable Javascript!</p></div></td>')

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

    @cherrypy.expose
    def dropped_on_roadmap_release(self, release_no, doc_id):
        new_release = self.map_release_number_to_release_name(release_no)
        card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
        card_document['release'] = new_release
        self.cards_collection.save(card_document)
        return self.roadmap_release(release_no)

    def map_release_number_to_release_name(self, release_no):
        release = ""
        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', '')
        selectable_releases = []
        for project_document in self.projects_collection.find({'project': project}):
            if 'releases' in project_document:
                for release_document in project_document['releases']:
                    release_name = release_document['release']
                    release_start_date = datetime.timedelta()
                    release_end_date = datetime.timedelta()
                    if 'start_date' in release_document and release_document['start_date']:
                        release_start_date = release_document['start_date']

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

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

        selectable_releases.sort()
        try:
            (_, _, release) = selectable_releases[int(release_no)]
        except:
            True
            
        return release

    @staticmethod
    def min_max_day_epoch(day_epoch):
        """Returns two datetime objects giving the minimum and maximum time values for a given date"""
        year = day_epoch.year
        month = day_epoch.month
        day = day_epoch.day
        min_day_epoch = datetime.datetime(year, month, day,  0,  0,  0)
        max_day_epoch = datetime.datetime(year, month, day, 23, 59, 59)
        return min_day_epoch, max_day_epoch

    @cherrypy.expose
    def roadmap_release(self, release_no):
        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', '')
        content = []
        selectable_releases = []
        for project_document in self.projects_collection.find({'project': project}):
            if 'releases' in project_document:
                for release_document in project_document['releases']:
                    release = release_document['release']
                    release_start_date = datetime.timedelta()
                    release_end_date = datetime.timedelta()
                    if 'start_date' in release_document and release_document['start_date']:
                        release_start_date = release_document['start_date']

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

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

        selectable_releases.sort()
        try:
            (_, _, release) = selectable_releases[int(release_no)]
            if self.cards_collection.find({'project': project, 'release': release}).count():
                for card_document in self.cards_collection.find({'project': project,
                                                                'release': release}):
                    content.append(self.assemble_kanban_card(['owner', 'coowner', 'reviewer', 'coreviewer'],
                                                             ['updatable'], -1, card_document['_id'],
                                                             False, 0))

            else:
                content.append('<span class="ui-icon ui-icon-info" title="There are no cards in this release" />')
                
        except:
            content.append('<span class="ui-icon ui-icon-info" title="Sorry, an error has occured!" />')

        for js_file in ['kanban.js', 'dragdrop.js']:
            content.append(f'<script type="text/javascript" src="/scripts/{js_file}"></script>')

        return "".join(content)

    def assemble_pair_programming_graph_data(self):
        graph_data = {}
        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', '')
        graph_data['nodes'] = []
        graph_data['links'] = []
        fullnames_and_usernames = self.get_project_members([project])
        pairs = {}
        for (fullname, username) in fullnames_and_usernames:
            graph_data['nodes'].append({'id': username, 'group': 1, 'name': fullname})
            for (_, alternative_username) in fullnames_and_usernames:
                if username != alternative_username:
                    if not username+alternative_username in pairs and not alternative_username+username in pairs:
                        pairs[username+alternative_username] = True
                        count1 = self.cards_collection.find({'project': project, 'owner': username,
                                                            'coowner': alternative_username}).count()
                        count2 = self.cards_collection.find({'project': project, 'owner': alternative_username,
                                                            'coowner': username}).count()
                        if count1+count2:
                            graph_data['links'].append({'source': username, 'target': alternative_username, 'value': count1+count2})

        return graph_data

    @cherrypy.expose
    def pair_programming(self):
        """comment"""
        username = Kanbanara.check_authentication(f'/{self.component}/pair_programming')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        content.append(Kanbanara.header(self, 'pair_programming', "Pair Programming"))
        content.append(Kanbanara.filter_bar(self, 'pair_programming'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'pair_programming',
                                                              'Pair Programming'))
        content.append('<svg></svg>')
        content.append('<script>')
        graph_data = self.assemble_pair_programming_graph_data()
        content.append(f'var graph = {graph_data};')
        content.append('''// set the dimensions and margins of the diagram
var width = window.innerWidth,
    height = window.innerHeight,
    scrollbar = 25;

var svg = d3.select("svg")
    .attr("width", width - scrollbar)
    .attr("height", height),
    width = +svg.attr("width"),
    height = +svg.attr("height");

var color = d3.scaleOrdinal(d3.schemeCategory20);

var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

var link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
    .attr("stroke-width", function(d) { return Math.sqrt(d.value); });

var node = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
      .attr("r", 5)
      .attr("fill", function(d) { return color(d.group); })
      .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended));

  //node.append("title")
  //    .text(function(d) { return d.id; });

  node.append("title")
      .text(function(d) { return d.name; });

  simulation
      .nodes(graph.nodes)
      .on("tick", ticked);

  simulation.force("link")
      .links(graph.links);

  function ticked() {
    link
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
  }

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

</script>''')

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

    @cherrypy.expose
    def standup(self, teammember="", doc_id=""):
        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)
        theme = member_document.get('theme', 'kanbanara')
        if member_document.get('project', ''):
            projects = [member_document['project']]
        else:
            projects = self.get_member_projects(member_document)

        project_document = self.projects_collection.find_one({'project': projects[0], 'workflow': {'$exists': True}})
        content = Template(filename=os.path.join(self.current_dir, '..', 'templates', 'standup.tpl')).render(theme=theme)
        required_columns, required_states = self.get_displayable_columns()
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        card_document_ids = self.cards_collection.find(owner_reviewer_search_criteria).distinct('id')

        content += '<table id="searchresults">'

        selectable_users = {username}

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

        selectable_users = list(selectable_users)
        selectable_users.sort()

        content += '<tr><th valign="top">Team Members</th><td colspan="9">'

        for selectable_user in selectable_users:
            content += f'<form action="/{self.component}/standup" method="post"><input type="hidden" name="teammember" value="{selectable_user}">'
            if doc_id:
                content += f'<input type="hidden" name="doc_id" value="{doc_id}">'

            content += '<input class="'
            if teammember == selectable_user:
                content += 'standupselected'
            else:
                content += 'story'

            content += '" type="submit" value="'+selectable_user+'"></form>'

        content += '</td></tr>'

        workflow = project_document['workflow']
        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        uncondensed_column_states = workflow_index.get('uncondensed_column_states', [])
        content += '<tr><td></td>'
        for step_document in workflow:
            step_name = step_document['step']
            if step_name:
                number_of_columns = 0
                metastate = ""
                for column in ['maincolumn', 'counterpartcolumn', 'buffercolumn']:
                    if column in step_document:
                        number_of_columns += 1
                        if not metastate:
                            variable_column = step_document[column]
                            state = variable_column['state']
                            if state in self.metastates_list:
                                metastate = state
                            else:
                                custom_states = project_document.get('customstates', {})
                                if state in custom_states:
                                    metastate = custom_states[state]

                if number_of_columns:
                    if number_of_columns > 1:
                        content += f'<th id="{metastate}step" colspan="{number_of_columns}">{step_name}</th>'
                    else:
                        content += f'<th id="{metastate}step">{step_name}</th>'

        content += '</tr><tr><td></td>'
        for step_document in workflow:
            step_name = step_document['step']
            if step_name:
                for column in ['maincolumn', 'counterpartcolumn', 'buffercolumn']:
                    if column in step_document:
                        variable_column = step_document[column]
                        column_name = variable_column['name']
                        state = variable_column['state']
                        if state in self.metastates_list:
                            metastate = state
                        else:
                            custom_states = project_document.get('customstates', {})
                            metastate = custom_states[state]

                        content += f'<th id="{metastate}columnheader">{column_name}</th>'

        content += '</tr>'

        valid_types = {}
        card_details = []
        for card_document_id in sorted(card_document_ids):
            card_document = self.cards_collection.find_one({"id": card_document_id})
            card_id = card_document.get('id', '')
            card_doc_id = card_document.get('_id', '')
            card_state = card_document.get('state', '')
            card_title = card_document.get('title', '')
            card_type = card_document.get('type', '')
            card_owner = card_document.get('owner', '')
            card_coowner = card_document.get('coowner', '')
            card_reviewer = card_document.get('reviewer', '')
            card_coreviewer = card_document.get('coreviewer', '')
            card_parent = card_document.get('parent', '')
            card_blocked = card_document.get('blocked', '')
            card_stuck = card_document.get('stuck', '')
            card_details.append((card_id, card_doc_id, card_state, card_title, card_type,
                                 card_owner, card_coowner, card_reviewer, card_coreviewer,
                                 card_parent, card_blocked, card_stuck))
            if not teammember or teammember in [card_owner, card_coowner, card_reviewer, card_coreviewer]:
                valid_types[card_type] = True

        doc_id_valid_for_user = False
        for selectedType in ['epic', 'feature', 'story', 'enhancement', 'defect', 'task', 'test', 'bug']:
            if selectedType in valid_types:
                content += '<tr><th valign="top">'+selectedType.capitalize()+'</th>'
                for selected_state in condensed_column_states:
                    content += '<td valign="top">'
                    for (card_id, card_doc_id, card_state, card_title, card_type, card_owner, card_coowner,
                         card_reviewer, card_coreviewer, card_parent, card_blocked, card_stuck) in card_details:
                        if card_type == selectedType and card_state == selected_state:
                            if not teammember or teammember in [card_owner, card_coowner, card_reviewer, card_coreviewer]:
                                content += f'<form action="/{self.component}/standup" method="post">'
                                if teammember:
                                    content += '<input type="hidden" name="teammember" value="'+teammember+'">'

                                content += '<input type="hidden" name="doc_id" value="'+str(card_doc_id)+'"><input class="'
                                metastate = self.get_corresponding_metastate(project_document, card_state)
                                if doc_id == str(card_doc_id):
                                    content += 'standupselected'
                                    doc_id_valid_for_user = True
                                elif card_blocked or card_stuck:
                                    content += 'cardcompromised'
                                elif not card_owner and not card_coowner and metastate in ['defined', 'analysis', 'analysed', 'design', 'designed', 'development']:
                                    content += 'cardcompromised'
                                elif not card_reviewer and not card_coreviewer and metastate in ['unittesting', 'integrationtesting', 'systemtesting', 'acceptancetesting']:
                                    content += 'cardcompromised'
                                elif card_parent:
                                    content += card_type+'child'
                                else:
                                    content += card_type

                                value = card_id
                                if card_blocked:
                                    value += ' [B]'

                                if not card_owner and not card_coowner and metastate in ['defined', 'analysis', 'analysed', 'development']:
                                    value += ' [O]'

                                if not card_reviewer and not card_coreviewer and metastate in ['unittesting', 'integrationtesting', 'systemtesting', 'acceptancetesting']:
                                    value += ' [R]'

                                if card_stuck:
                                    value += ' [S]'

                                content += '" type="submit" value="'+value+'" title="'+card_title+'"></form>'

                    content += '</td>'

                content  += '</tr>'

        if doc_id_valid_for_user:
            if doc_id:
                card_document = self.cards_collection.find_one({"_id": ObjectId(doc_id)})
                card_documents = [card_document]
                for childcard_document in self.cards_collection.find({"parent": card_document['id']}):
                    card_documents.append(childcard_document)

            else:
                card_documents = []

            content += '<tr><td></td><td colspan="9"><hr></td></tr>'

            content += '<tr><td></td>'
            for state in uncondensed_column_states:
                content += '<td valign="top">'
                for card_document in card_documents:
                    if 'state' in card_document and card_document['state'] == state:
                        content += self.assemble_kanban_card(['owner', 'coowner', 'reviewer', 'coreviewer'], ['display'],
                                                             -1, card_document['_id'], False, 0)

                content += '</td>'

        content += '</tr></table>'

        if doc_id_valid_for_user:
            selected_user = member_document.get('teammember', '')
            selected_project = member_document.get('project', '')
            potential_keys = ['hierarchy', 'id', 'title', 'status', 'blocked', 'blockeduntil',
                              'deferred', 'deferreduntil', 'stuck', 'owner', 'coowner', 'reviewer',
                              'coreviewer', 'estimatedtime', 'actualtime', 'estimatedcost',
                              'actualcost', 'severity', 'priority', 'nextaction', 'deadline',
                              'dependsupon']
            content += self.assemble_listview_table(owner_reviewer_search_criteria, member_document,
                                                    selected_project, selected_user, potential_keys,
                                                    card_documents)

        content += '<p><br></p>'

        for script_file in ['kanban.js', 'listview.js']:
            content += '<script type="text/javascript" src="/scripts/'+script_file+'"></script>'

        content += Kanbanara.footer(self)
        content += '</body></html>'
        return content

    @cherrypy.expose
    def wallboard(self):
        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)
        theme = member_document.get('theme', 'kanbanara')
        project, release, iteration = self.get_member_project_release_iteration(member_document)
        return Template(filename=os.path.join(self.current_dir, '..', 'templates', 'wallboard.tpl')).render(theme=theme, project=project,
                                                                                                   release=release, iteration=iteration, footer=Kanbanara.footer(self))

    @cherrypy.expose
    def wallboardleft(self):
        """Randomly populates the left hand side of the wallboard"""
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        content = []
        states = []
        rand_no = randint(1, 5)
        if rand_no == 1:
            content.append('<h2 class="page_title">In Untriaged, Triaged or Backlog</h2>')
            states = self.get_custom_states_mapped_onto_metastates(['untriaged', 'triaged', 'backlog'])
        elif rand_no == 2:
            content.append('<h2 class="page_title">In Analysis and Design</h2>')
            states = self.get_custom_states_mapped_onto_metastates(['analysis', 'analysed', 'design'])
        elif rand_no == 3:
            content.append('<h2 class="page_title">In Development</h2>')
            states = self.get_custom_states_mapped_onto_metastates(['development'])
        elif rand_no == 4:
            content.append('<h2 class="page_title">In Testing</h2>')
            states = self.get_custom_states_mapped_onto_metastates(['unittesting', 'integrationtesting',
                                                                    'systemtesting', 'acceptancetesting'])
        elif rand_no == 5:
            content.append('<h2 class="page_title">In Accepted</h2>')
            states = self.get_custom_states_mapped_onto_metastates(['acceptancetestingaccepted', 'completed'])

        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, states)
        content.append('<table class="sortable"><tr><th>ID</th><th>Title</th></tr>')
        for card_document in self.cards_collection.find(owner_reviewer_search_criteria):
            if 'id' in card_document and 'title' in card_document:
                content.append(f'<tr><td>{card_document["id"]}</td><td>{card_document["title"]}</td></tr>')

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

    @cherrypy.expose
    def wallboardright(self):
        """Randomly populates the right hand side of the wallboard"""
        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, release, iteration = self.get_member_project_release_iteration(member_document)
        content = []
        rand_no = randint(1, 3)
        if rand_no == 1:
            content.append('<h2 class="page_title">Recent Activity</h2>')
            valid_recent_activities = []
            for (past_time, any_username, doc_id, mode) in self.recent_activities:
                if past_time+self.timedelta_day >= datetime.datetime.utcnow():
                    if self.cards_collection.find({"_id": ObjectId(doc_id), 'project': project}).count():
                        recent_activity = self.assemble_recent_activity_entry(past_time, any_username, doc_id, mode)
                        if recent_activity:
                            content.append('<p>'+recent_activity+'</p>')

                    valid_recent_activities.append((past_time, any_username, doc_id, mode))

            self.recent_activities = valid_recent_activities
        elif rand_no == 2:
            content.append(self.cumulative_flow_diagram_chart())
        elif rand_no == 3:
            content.append(self.abandoned_chart())

        return "".join(content)

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

cherrypy.tree.mount(Visuals(), '/visuals', config=conf)
