easydoneit-cli

Easydoneit cli is a simple task manager for terminals.
git clone https://noulin.net/git/easydoneit-cli.git
Log | Files | Refs | README | LICENSE

edi_core.py (85124B)


      1 #! /usr/bin/env python
      2 # -*- coding: latin-1 -*-
      3 ## @package edi_core
      4 #  Core module
      5 #
      6 #
      7 # Copyright (C) 2014 Spartatek AB
      8 #
      9 # contact@spartatek.se
     10 # http://spartatek.se
     11 #
     12 # EASYDONEIT CLI is free software: you can redistribute it and/or modify
     13 # it under the terms of the GNU Genereric Public License as published by
     14 # the Free Software Foundation, either version 3 of the License, or
     15 # (at your option) any later version.
     16 #
     17 # EASYDONIT CLI is distributed in the hope that it will be usesul,
     18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     20 # GNU Genereral Public License for more details.
     21 #
     22 # You should have received a copy of the GNU General Public License
     23 # along with this program. If not, see
     24 # GNU licences http://www.gnu.org/licenses
     25 
     26 import sys
     27 import re
     28 import time
     29 import shutil
     30 import os
     31 import ConfigParser
     32 from string import ascii_lowercase
     33 from string import ascii_uppercase
     34 from string import digits
     35 import random
     36 import stat
     37 import getpass
     38 
     39 from edi_common import *
     40 
     41 ## user_interface is initialized in start() and changes the behavior of some functions to allow a functional user interface design. The default is not used, start() is always called before using edi_core.
     42 # Values: 'cli', 'web'
     43 user_interface       = 'cli'
     44 
     45 data_location        = ''
     46 data_location_tasks  = ''
     47 data_location_groups = ''
     48 data_location_tree   = ''
     49 
     50 ## Available databases
     51 databases            = []
     52 
     53 ## list selected databases
     54 selected             = []
     55 selected_path        = []
     56 ## default database where tasks are created
     57 default_add_in       = ''
     58 
     59 ## add new tasks in 'bottom' of group or 'top' of group
     60 add_top_or_bottom    = 'bottom'
     61 
     62 ## Autlink groups (array of tids)
     63 autolink             = ''
     64 
     65 ## list of groups for edi ls -L, -La and -Lx (array of tids)
     66 list_of_groups       = ''
     67 
     68 ## characters for task ids
     69 ID_BASE              = sorted(list(digits)+list(ascii_lowercase)+list(ascii_uppercase)+['_',','])
     70 ID_BASE_STRING       = ''.join(ID_BASE)
     71 ## Length of task id
     72 ID_LENGTH            = 16
     73 ## Length of order id in groups
     74 ORDER_ID_LENGTH      = 8
     75 BASE                 = len(ID_BASE)
     76 
     77 TASK_STATUS          = ['  Active',
     78                         '    Done',
     79                         ' Ongoing',
     80 			' Pending',
     81                         'Inactive',
     82                         '    Void']
     83 TASK_STATUS_ACTIVE   = 0
     84 TASK_STATUS_DONE     = 1
     85 TASK_STATUS_ONGOING  = 2
     86 TASK_STATUS_PENDING  = 3
     87 TASK_STATUS_INACTIVE = 4
     88 TASK_STATUS_VOID     = 5
     89 
     90 ## Sort order for sort_task_attributes function, ongoing, active, pending, done, inactive, void
     91 SORT_TASK_ORDER      = [2,0,3,1,4,5]
     92 
     93 LIST_OPTIONS         = ['tids',
     94 			'positions',
     95 			'html']
     96 list_option          = 'tids'
     97 
     98 STATUS_FILTER_STATES = ['enable','disable']
     99 
    100 ## enables all status filters
    101 status_filters       = [STATUS_FILTER_STATES[0] for i in range(len(TASK_STATUS))]
    102 ## status filter dictionary, initialized after loading ini file
    103 status_filters_d     = {}
    104 
    105 ## colors
    106 no_color             = (-1,-1,-1,255)
    107 #status_fgColors      = [(0,0,0,255) for i in range(len(TASK_STATUS))]
    108 status_fgColors      = [(0,0,0,255),(0,255,0,255),(255,128,0,255),(160,32,240,255),(192,192,192,255),(0,0,0,255)]
    109 status_fgColors_d    = {}
    110 ## no background color by default
    111 status_bgColors      = [no_color for i in range(len(TASK_STATUS))]
    112 status_fgColors_d    = {}
    113 
    114 ## text editor in terminal - easydoneit.ini [settings] EDITOR
    115 editor               = 'vi'
    116 
    117 ## Agenda generated by edi.in_cli
    118 agenda               = []
    119 
    120 ## user name
    121 user                 = ''
    122 
    123 ## user email
    124 email                = ''
    125 
    126 ## stats for statistics function
    127 stats                = {}
    128 
    129 ## total number of tasks for statistics function
    130 stats_total          = 0
    131 
    132 ## creation dates of tasks in statistics, used to find out oldest task
    133 stats_creation_dates = []
    134 
    135 ## timely state changes and creation dates
    136 # Type: dict of dict of integers
    137 # keys are dates
    138 # each dates is a dictionary with STATS_OVERTIME_KEYS keys
    139 # each key is the amount for states and creation
    140 stats_overtime       = {}
    141 
    142 ## timely state changes and creation dates
    143 STATS_OVERTIME_KEYS  = [TASK_STATUS[i] for i in range(len(TASK_STATUS))] + ['Creation']
    144 
    145 ## Group directory, cache result to speed up color functions
    146 group_directory_file_list = []
    147 
    148 ## initializes path to data.
    149 # creates directories<br>
    150 # creates root group and task<br>
    151 # @ingroup EDI_CORE
    152 def init():
    153 	# Set global variables
    154 	global data_location_tasks
    155 	global data_location_groups
    156 	global data_location_tree
    157 	global status_filters
    158 	global status_filters_d
    159 	global status_fgColors
    160 	global status_fgColors_d
    161 	global status_bgColors
    162 	global status_bgColors_d
    163 
    164 	status_filters_d     = dict(zip(TASK_STATUS,status_filters))
    165 	status_fgColors_d    = dict(zip(TASK_STATUS,status_fgColors))
    166 	status_bgColors_d    = dict(zip(TASK_STATUS,status_bgColors))
    167 
    168 	data_location_tasks  = '%s/tasks'%data_location
    169 	data_location_groups = '%s/groups'%data_location
    170 	data_location_tree   = '%s/tree'%data_location
    171 
    172 	if not os.path.exists(data_location_tasks):
    173 		if not os.path.exists(data_location):
    174 			os.mkdir(data_location)
    175 		os.mkdir(data_location_tasks)
    176 		os.mkdir(data_location_groups)
    177 		os.mkdir(data_location_tree)
    178 		os.mkdir('%s/root'%data_location_groups)
    179 
    180 		# Create root description in tasks
    181 		task_path = '%s/root'%data_location_tasks
    182 		os.mkdir(task_path)
    183 		# copy root description from script directory
    184 		shutil.copy('%s/root.txt'%os.path.abspath(os.path.dirname(sys.argv[0])),'%s/description.txt'%task_path)
    185 
    186 		# Create status
    187 		f         = open('%s/status'%task_path,'w')
    188 		f.write(TASK_STATUS[TASK_STATUS_VOID])
    189 		f.close()
    190 
    191 ## log actions in database - data_location/log.txt
    192 # @param[in] s string without user name and timestamp
    193 # @ingroup EDI_CORE
    194 def edi_log(s):
    195 	global user
    196 	global email
    197 	f = open('%s/log.txt'%data_location,'a')
    198 	f.write('%s - %s - %s\n'%(time.strftime("%Y-%m-%d %H:%M"), '%s <%s>'%(user,email),s.strip()))
    199 	f.close()
    200 
    201 
    202 ## select location database
    203 # @param[in] location database name
    204 # @ingroup EDI_CORE
    205 def select_database(location):
    206 	global data_location
    207 	global data_location_tasks
    208 	global data_location_groups
    209 	global data_location_tree
    210 
    211 	z = dict(zip(selected, selected_path))
    212 	data_location        = os.path.expanduser(z[location])
    213 	data_location_tasks  = '%s/tasks'%data_location
    214 	data_location_groups = '%s/groups'%data_location
    215 	data_location_tree   = '%s/tree'%data_location
    216 	if not os.path.exists(data_location):
    217 		return '%s is unreachable.'%data_location
    218 	else:
    219 		return ''
    220 
    221 ## Mix foreground colors from task, groups and default colors.
    222 # @return color array
    223 # @param[in] tid task id
    224 # @ingroup EDI_CORE
    225 # Collect all defined colors in link groups.
    226 # For each group, the group gets the first color defined in the tree<br>
    227 # compute average color
    228 def mix_fgcolors(tid):
    229 	# mix colors
    230 	# collect all defined colors in groups to root
    231 	colors = []
    232 	# root has no parent group, dont search parent group color
    233 	if tid != 'root':
    234 		# find all groups - link and parent group
    235 		if is_linked(tid):
    236 			groups = os.listdir('%s/groups/' % generate_task_path(tid))
    237 		else:
    238 			groups = [find_group_containing_task(tid)]
    239 
    240 		# walk parent groups until a color or root is found
    241 		while groups:
    242 			status_color = 'search'
    243 			if os.path.isfile('%s/fgColor'%generate_task_path(groups[0])):
    244 				f       = open('%s/fgColor'%generate_task_path(groups[0]))
    245 				c       = tuple([int(i) for i in f.readline().split(',')])
    246 				f.close()
    247 				# -1 means no_color, mix colors
    248 				if c[0] != -1:
    249 					colors.append(c)
    250 					status_color = 'found color'
    251 			if status_color == 'search':
    252 				parent_group = find_group_containing_task(groups[0])
    253 				if parent_group != 'root':
    254 					groups.append(parent_group)
    255 			del groups[0]
    256 
    257 		# compute average color
    258 		if colors:
    259 			color = [0,0,0,0]
    260 			for c in colors:
    261 				for i in range(len(c)):
    262 					color[i] += c[i]
    263 			colorCount = len(colors)
    264 			color      = tuple([i/colorCount for i in color])
    265 	if not colors:
    266 		# no defined colors, use default color for status
    267 		task_status = get_status(tid)
    268 		color       = status_fgColors_d[task_status]
    269 	return color
    270 
    271 ## get color for task tid
    272 # @return color array
    273 # @param[in] tid task id
    274 # @ingroup EDI_CORE
    275 # when no color is set, mix colors from groups or use defaults
    276 def get_forground_color(tid):
    277 	color = no_color
    278 
    279 	if os.path.isfile('%s/fgColor'%generate_task_path(tid)):
    280 		f       = open('%s/fgColor'%generate_task_path(tid))
    281 		color   = tuple([int(i) for i in f.readline().split(',')])
    282 		f.close()
    283 		# -1 means no_color, mix colors
    284 		if color[0] == -1:
    285 			color = mix_fgcolors(tid)
    286 	else:
    287 		color   = mix_fgcolors(tid)
    288 	return color
    289 
    290 ## set color for task tid
    291 # @return color array
    292 # @param[in] tid task id
    293 # @param[in] color_s color array
    294 # @ingroup EDI_CORE
    295 def set_forground_color(tid,color_s):
    296 	f       = open('%s/fgColor'%generate_task_path(tid),'w')
    297 	f.write(color_s)
    298 	f.close()
    299 	edi_log('set foreground color in %s to %s'%(tid,color_s))
    300 
    301 ## Mix background colors from task, groups and default colors.
    302 # @return color array
    303 # @param[in] tid task id
    304 # @ingroup EDI_CORE
    305 # Collect all defined colors in link groups.
    306 # For each group, the group gets the first color defined in the tree<br>
    307 # compute average color
    308 def mix_bgcolors(tid):
    309 	# mix colors
    310 	# collect all defined colors in groups to root
    311 	colors = []
    312 	# root has no parent group, dont search parent group color
    313 	if tid != 'root':
    314 		# find all groups - link and parent group
    315 		if is_linked(tid):
    316 			groups = os.listdir('%s/groups/' % generate_task_path(tid))
    317 		else:
    318 			groups = [find_group_containing_task(tid)]
    319 
    320 		# walk parent groups until a color or root is found
    321 		while groups:
    322 			status_color = 'search'
    323 			if os.path.isfile('%s/bgColor'%generate_task_path(groups[0])):
    324 				f       = open('%s/bgColor'%generate_task_path(groups[0]))
    325 				c       = tuple([int(i) for i in f.readline().split(',')])
    326 				f.close()
    327 				# -1 means no_color, mix colors
    328 				if c[0] != -1:
    329 					colors.append(c)
    330 					status_color = 'found color'
    331 			if status_color == 'search':
    332 				parent_group = find_group_containing_task(groups[0])
    333 				if parent_group != 'root':
    334 					groups.append(parent_group)
    335 			del groups[0]
    336 
    337 		# compute average color
    338 		if colors:
    339 			color = [0,0,0,0]
    340 			for c in colors:
    341 				for i in range(len(c)):
    342 					color[i] += c[i]
    343 			colorCount = len(colors)
    344 			color      = tuple([i/colorCount for i in color])
    345 	if not colors:
    346 		# no defined colors, use default color for status
    347 		task_status = get_status(tid)
    348 		color       = status_bgColors_d[task_status]
    349 	return color
    350 
    351 ## get color for task tid
    352 # @return color array
    353 # @param[in] tid task id
    354 # @ingroup EDI_CORE
    355 # when no color is set, mix colors from groups or use defaults
    356 def get_background_color(tid):
    357 	color = no_color
    358 
    359 	if os.path.isfile('%s/bgColor'%generate_task_path(tid)):
    360 		f       = open('%s/bgColor'%generate_task_path(tid))
    361 		color   = tuple([int(i) for i in f.readline().split(',')])
    362 		f.close()
    363 		# -1 means no_color, mix colors
    364 		if color[0] == -1:
    365 			color = mix_bgcolors(tid)
    366 	else:
    367 		color   = mix_bgcolors(tid)
    368 	return color
    369 
    370 ## set color for task tid
    371 # @return color array
    372 # @param[in] tid task id
    373 # @param[in] color_s color array
    374 # @ingroup EDI_CORE
    375 def set_background_color(tid,color_s):
    376 	f       = open('%s/bgColor'%generate_task_path(tid),'w')
    377 	f.write(color_s)
    378 	f.close()
    379 	edi_log('set background color in %s to %s'%(tid,color_s))
    380 
    381 ## converts color to hex format
    382 # @return color hex string
    383 # @param[in] color color array
    384 # @ingroup EDI_CORE
    385 def color_to_hex(color):
    386 	c = []
    387 	for n in color:
    388 		if n == -1:
    389 			# -1 is no color, white
    390 			c.append(255)
    391 		else:
    392 			c.append(n)
    393 	color = tuple(c)
    394 	return '%02x%02x%02x' % (color[0],color[1],color[2])
    395 
    396 ## filesystem path for task in database tasks
    397 # @return path
    398 # @param[in] tid task id
    399 # @ingroup EDI_CORE
    400 def generate_task_path(tid):
    401 	return '%s/%s'%(data_location_tasks,tid)
    402 
    403 ## filesystem path for group in database groups
    404 # @return path
    405 # @param[in] tid task id
    406 # @ingroup EDI_CORE
    407 def generate_group_path(tid):
    408 	return '%s/%s/'%(data_location_groups,tid)
    409 
    410 ## get status for tid
    411 # @return status string
    412 # @param[in] tid task id
    413 # @ingroup EDI_CORE
    414 def get_status(tid):
    415 	# open status file
    416 	try:
    417 		f      = open('%s/status'%generate_task_path(tid))
    418 		status = f.readline()
    419 		f.close()
    420 	except:
    421 		print '%s is an invalid task.'%generate_task_path(tid)
    422 		status = TASK_STATUS[TASK_STATUS_VOID]
    423 	return status
    424 
    425 ## creates a string with task id, status and string parameter
    426 # @return list of strings
    427 # @param[in] tid task id
    428 # @param[in] s task title string
    429 # @ingroup EDI_CORE
    430 # Displays a task depending on edi_core.list_option
    431 def generate_task_string_with_tid(tid,s):
    432 	status = get_status(tid)
    433 	if list_option == 'tids':
    434 		# %*s to dynamically adjust id length
    435 		r             = '%*s - %s -   %s'%(ID_LENGTH,tid,status,s)
    436 	if list_option == 'positions':
    437 		if is_this_task_a_group(tid):
    438 			# print tid for groups in the list
    439 			r             = '%*s - %s -   %s'%(ID_LENGTH,tid,status,s)
    440 		else:
    441 			tid           = ' '
    442 			r             = '%*s   %s -   %s'%(ID_LENGTH,tid,status,s)
    443 	if list_option == 'md':
    444 		# print title and description
    445 		f             = open('%s/description.txt'%generate_task_path(tid))
    446 		# remove title line
    447 		description   = ''.join(f.readlines()[1:])
    448 		f.close()
    449 
    450 		tid           = ' '
    451 		# add <br> for long titles
    452 		r             = '\n---\n#%s<br>\n%s'%(s,description)
    453 	if list_option == 'rst':
    454 		# print title and description
    455 		f             = open('%s/description.txt'%generate_task_path(tid))
    456 		# remove title line
    457 		description_l = f.readlines()[1:]
    458 		f.close()
    459 
    460 		# convert urls to link
    461 		NOBRACKET = r'[^\]\[]*'
    462 		BRK = ( r'\[('
    463 			+ (NOBRACKET + r'(\[')*6
    464 			+ (NOBRACKET+ r'\])*')*6
    465 			+ NOBRACKET + r')\]' )
    466 		NOIMG = r'(?<!\!)'
    467 		LINK_RE = NOIMG + BRK + \
    468 		r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)'''
    469 		# [text](url) or [text](<url>) or [text](url "title")
    470 
    471 		compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE)
    472 
    473 		for line_index,l in enumerate(description_l):
    474 			if 'http' in l:
    475 				try:
    476 					match      = compiled_re.match(l)
    477 					url        = match.group(9)
    478 					text       = match.group(2)
    479 					before_url = match.group(1)
    480 					#title      = match.group(13)
    481 					after_url  = match.group(14)
    482 
    483 					l = '%s`%s<%s>`_%s'% (before_url, text, url, after_url)
    484 				except:
    485 					# not used, information
    486 					link_status = 'WARNING: the link is not in format [title](url)'
    487 			description_l[line_index] = l
    488 
    489 		description   = ''.join(description_l)
    490 
    491 		tid           = ' '
    492 		# remove position and type (GROUP, LINK) from title
    493 		title         = s[11:].lstrip()
    494 		r             = '%s\n'%title + '='*len(title) + '\n%s\n\n'%description
    495 	if list_option == 'html':
    496 		#<tr bgcolor="#FEFFCC">
    497 		#  <td class="subject">          <font color="#000000">t1</font></td>
    498 		#  <td class="description">          <font color="#000000">&nbsp;</font></td>
    499 		#</tr>
    500 #:define read_description
    501 		# convert < to &lt; and > &gt; to ignore html tags in title line
    502 		s             = s.replace('<','&lt;').replace('>','&gt;')
    503 		f             = open('%s/description.txt'%generate_task_path(tid))
    504 		# remove title line, convert < to &lt; and > &gt; to ignore html tags
    505 		description_l = ['%s<br>'%i.rstrip().replace('<','&lt;').replace('>','&gt;') for i in f.readlines()[1:]]
    506 		f.close()
    507 
    508 		# convert urls to link
    509 		NOBRACKET = r'[^\]\[]*'
    510 		BRK = ( r'\[('
    511 			+ (NOBRACKET + r'(\[')*6
    512 			+ (NOBRACKET+ r'\])*')*6
    513 			+ NOBRACKET + r')\]' )
    514 		NOIMG = r'(?<!\!)'
    515 		LINK_RE = NOIMG + BRK + \
    516 		r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)'''
    517 		# [text](url) or [text](<url>) or [text](url "title")
    518 
    519 		compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE)
    520 
    521 		for line_index,l in enumerate(description_l):
    522 			if 'http' in l:
    523 				try:
    524 					match      = compiled_re.match(l)
    525 					url        = match.group(9)
    526 					text       = match.group(2)
    527 					before_url = match.group(1)
    528 					#title      = match.group(13)
    529 					after_url  = match.group(14)
    530 
    531 					l = '%s<a href="%s">%s</a>%s'% (before_url, url, text, after_url)
    532 				except:
    533 					# not used, information
    534 					link_status = 'WARNING: the link is not in format [title](url)'
    535 			description_l[line_index] = l
    536 
    537 		description   = '\n'.join(description_l)
    538 #:end
    539 
    540 		if is_this_task_a_group(tid) and user_interface == 'web':
    541 			subject       = '<a href="edi_web.py?tid=%s">%s -   %s</a>'%(tid,status,s)
    542 		else:
    543 			subject       = '%s -   %s'%(status,s)
    544 		fg            = color_to_hex(get_forground_color(tid))
    545 		bg            = color_to_hex(get_background_color(tid))
    546 		r='        <tr bgcolor="#%s">\n          <td class="subject">          <font color="#%s">%s</font></td>\n          <td class="description">          <font color="#%s">%s</font></td>\n        </tr>' % (bg, fg, subject, fg, description)
    547 	return r
    548 
    549 ## creates a string with task id, status and string parameter
    550 # @return list of strings
    551 # @param[in] tid task id
    552 # @param[in] s task title string
    553 # @ingroup EDI_CORE
    554 # Displays a group depending on edi_core.list_option
    555 def generate_group_string_with_tid(tid,s):
    556 	global agenda
    557 
    558 	status = get_status(tid)
    559 	if (list_option == 'tids') or (list_option == 'positions'):
    560 		# %*s to dynamically adjust id length
    561 		r           = '%*s - %s - %s'%(ID_LENGTH,tid,status,s)
    562 	if list_option == 'md':
    563 		tid         = ' '
    564 		# print group title only
    565 		r           = '#%s'%s.replace('  0 - GROUP','')
    566 		if agenda:
    567 			r += '\n---\n# Agenda\n%s' % '\n\n'.join(agenda)
    568 	if list_option == 'rst':
    569 		tid         = ' '
    570 		# print group title only
    571 		title       = s.replace('  0 - GROUP','')
    572 		r           = '='*len(title) + '\n%s\n'%title + '='*len(title) + '\n\n'
    573 		if agenda:
    574 			r += 'Agenda\n======\n%s\n\n' % '\n'.join(agenda)
    575 		else:
    576 			r += '\n.. contents::\n\n'
    577 	if list_option == 'html':
    578 		#<tr bgcolor="#FEFFCC">
    579 		#  <td class="subject">          <font color="#000000">t1</font></td>
    580 		#  <td class="description">          <font color="#000000">&nbsp;</font></td>
    581 		#</tr>
    582 		# display description in html
    583 #:read_description
    584 		# convert < to &lt; and > &gt; to ignore html tags in title line
    585 		s             = s.replace('<','&lt;').replace('>','&gt;')
    586 		f             = open('%s/description.txt'%generate_task_path(tid))
    587 		# remove title line, convert < to &lt; and > &gt; to ignore html tags
    588 		description_l = ['%s<br>'%i.rstrip().replace('<','&lt;').replace('>','&gt;') for i in f.readlines()[1:]]
    589 		f.close()
    590 
    591 		# convert urls to link
    592 		NOBRACKET = r'[^\]\[]*'
    593 		BRK = ( r'\[('
    594 			+ (NOBRACKET + r'(\[')*6
    595 			+ (NOBRACKET+ r'\])*')*6
    596 			+ NOBRACKET + r')\]' )
    597 		NOIMG = r'(?<!\!)'
    598 		LINK_RE = NOIMG + BRK + \
    599 		r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)'''
    600 		# [text](url) or [text](<url>) or [text](url "title")
    601 
    602 		compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE)
    603 
    604 		for line_index,l in enumerate(description_l):
    605 			if 'http' in l:
    606 				try:
    607 					match      = compiled_re.match(l)
    608 					url        = match.group(9)
    609 					text       = match.group(2)
    610 					before_url = match.group(1)
    611 					#title      = match.group(13)
    612 					after_url  = match.group(14)
    613 
    614 					l = '%s<a href="%s">%s</a>%s'% (before_url, url, text, after_url)
    615 				except:
    616 					# not used, information
    617 					link_status = 'WARNING: the link is not in format [title](url)'
    618 			description_l[line_index] = l
    619 
    620 		description   = '\n'.join(description_l)
    621 
    622 		subject     = '%s - %s'%(status, s)
    623 		fg          = color_to_hex(get_forground_color(tid))
    624 		bg          = color_to_hex(get_background_color(tid))
    625 		r='        <tr bgcolor="#%s">\n          <td class="subject">          <font color="#%s">%s</font></td>\n          <td class="description">          <font color="#%s">%s</font></td>\n        </tr>' % (bg, fg, subject, fg, description)
    626 	return r
    627 
    628 ## determines if task is a group
    629 # @return empty string or 'this task is a group'
    630 # @param[in] tid task id
    631 # @ingroup EDI_CORE
    632 def is_this_task_a_group(tid):
    633 	# Identify if task is a group
    634 	is_a_group       = ''
    635 	# list groups
    636 	groups           = os.listdir(data_location_groups)
    637 	if tid in groups:
    638 		is_a_group = 'this task is a group'
    639 	return is_a_group
    640 
    641 ## get task title
    642 # @return first line of task description
    643 # @param[in] tid task id
    644 # @ingroup EDI_CORE
    645 def get_task_title(tid):
    646 	task_path = generate_task_path(tid)
    647 	f         = open('%s/description.txt'%task_path)
    648 	r         = f.readline().strip()
    649 	f.close()
    650 	return r
    651 
    652 ## get creation date
    653 # @return array of date string and unix time
    654 # @param[in] tid task id
    655 # @ingroup EDI_CORE
    656 def get_creation_date(tid):
    657 	r = []
    658 	# Figure out creation time and modification times for description and status
    659 	task_path         = generate_task_path(tid)
    660 	if not os.path.exists('%s/ctime.txt'%task_path):
    661 		task_ctime        = 'Not available'
    662 		r.append(task_ctime)
    663 		r.append(0)
    664 	else:
    665 		f = open('%s/ctime.txt'%task_path)
    666 		task_ctime        = f.readline()
    667 		f.close()
    668 		r.append(task_ctime)
    669 		r.append(time.mktime(time.strptime(task_ctime)))
    670 
    671 	return r
    672 
    673 ## get media
    674 # @return media file name
    675 # @param[in] tid task id
    676 # @ingroup EDI_CORE
    677 def get_media(tid):
    678 	r = ['None']
    679 	task_path = generate_task_path(tid)
    680 	if os.path.exists('%s/media'%task_path):
    681 		files = os.listdir('%s/media'%task_path)
    682 		r     = [files[0].split('.')[0], '%s/media/%s'%(task_path,files[0])]
    683 	return r
    684 
    685 ## set media, one media file per task
    686 # @return media file name
    687 # @param[in] tid task id
    688 # @param[in] filename media file to copy to task
    689 # @ingroup EDI_CORE
    690 def set_media(tid,filename):
    691 	r = '%s is not a supported media file.' % filename
    692 
    693 	task_path = generate_task_path(tid)
    694 	if filename.strip()[-3:] == 'wav':
    695 #:define create_media_folder
    696 		if not os.path.exists('%s/media'%task_path):
    697 			# create media folder
    698 			os.mkdir('%s/media'%task_path)
    699 #:end
    700 		else:
    701 			# media exists, check type
    702 			files = os.listdir('%s/media'%task_path)
    703 			if not files[0][-3:] == 'wav':
    704 				r = '%s is not a sound.'%tid
    705 				return r
    706 		# sound file
    707 		shutil.copy(filename,'%s/media/sound.wav'%task_path)
    708 		r = 'sound.wav'
    709 	if filename.strip()[-3:] == 'jpg':
    710 #:create_media_folder
    711 		if not os.path.exists('%s/media'%task_path):
    712 			# create media folder
    713 			os.mkdir('%s/media'%task_path)
    714 		else:
    715 			# media exists, check type
    716 			files = os.listdir('%s/media'%task_path)
    717 			if not files[0][-3:] == 'jpg':
    718 				r = '%s is not an image.'%tid
    719 				return r
    720 		# image file
    721 		shutil.copy(filename,'%s/media/image.jpg'%task_path)
    722 		r = 'image.jpg'
    723 	edi_log('added media %s in task %s'%(filename,tid))
    724 	return r
    725 
    726 ## get attachments
    727 # @return attachment file names
    728 # @param[in] tid task id
    729 # @ingroup EDI_CORE
    730 def get_attachments(tid):
    731 	r = ['None']
    732 	task_path = generate_task_path(tid)
    733 	if os.path.exists('%s/attachments'%task_path):
    734 		files = os.listdir('%s/attachments'%task_path)
    735 		r     = ['%s/attachments/%s'%(task_path,i) for i in files]
    736 	return r
    737 
    738 ## set attachments
    739 # @return attachment file names
    740 # @param[in] tid task id
    741 # @param[in] array of filenames
    742 # @ingroup EDI_CORE
    743 # With *, set_attachments copies multiple files at once
    744 def set_attachments(tid,filenames):
    745 	task_path = generate_task_path(tid)
    746 	if not os.path.exists('%s/attachments'%task_path):
    747 		# create attachment folder
    748 		os.mkdir('%s/attachments'%task_path)
    749 
    750 	r         = []
    751 	for fn in filenames:
    752 		try:
    753 			shutil.copy(fn,'%s/attachments/'%task_path)
    754 			edi_log('added attachment %s in task %s'%(fn,tid))
    755 			# Remove path from fn, keep only filename
    756 			r.append('%s/attachments/%s'%(task_path,fn.split(os.sep)[-1]))
    757 		except:
    758 			r.append('Failed to copy %s'%fn)
    759 	return r
    760 
    761 ## sort task attributes (ls in edi cli)
    762 # @return sorted task list by state
    763 # @param[in] result from list_group
    764 # @ingroup EDI_CORE
    765 def sort_task_attributes(task_attributes):
    766 	r = []
    767 	# Keep head group on top
    768 	if task_attributes[0]['head'] == 'head group':
    769 		r.append(task_attributes[0])
    770 		del task_attributes[0]
    771 	for s in SORT_TASK_ORDER:
    772 		for t in task_attributes:
    773 			if t['status'] == TASK_STATUS[s]:
    774 				r.append(t)
    775 	return r
    776 
    777 ## sort function for sorting an array of tasks by date from newest to oldest
    778 # @return compare result
    779 # @param[in] result from list_group
    780 # @ingroup EDI_CORE
    781 def sortdatefunc(x,y):
    782 	return cmp(y['ctime'],x['ctime'])
    783 
    784 ## sort task attributes (ls in edi cli)
    785 # @return sorted task list by date
    786 # @param[in] result from list_group
    787 # @ingroup EDI_CORE
    788 def sort_task_attributes_by_date(task_attributes):
    789 	r = []
    790 	# Keep head group on top
    791 	if task_attributes[0]['head'] == 'head group':
    792 		r.append(task_attributes[0])
    793 		del task_attributes[0]
    794 
    795 	# -1 to exclude the empty line
    796 	a = task_attributes[:-1]
    797 	a.sort(sortdatefunc)
    798 	#print task_attributes
    799 	r += a
    800 	r.append(task_attributes[-1])
    801 	return r
    802 
    803 ## list id - status - group - first line from description
    804 # @return list of tasks and groups title
    805 # @param[in] tid task id
    806 # @ingroup EDI_CORE
    807 # items in groups have the format 'ORDER_ID''TASK_ID'<br>
    808 # task 0 is group tittle<br>
    809 #<br>
    810 # result array elements:<br>
    811 # {head :string, tid :string, position :value, group :string, title :string, status :string}<br>
    812 # head tells if the element is a group title.<br>
    813 #<br>
    814 # example:<br>
    815 #fTTB1KRWfDpoSR1_ -   Active -   GROUP Motivational Interviewing<br>
    816 #62ZvFA_q0pCZFr0Y -   Active -         news<br>
    817 def list_group(tid):
    818 	result      = []
    819 	# list visible groups only
    820 	task_status = get_status(tid)
    821 	if status_filters_d[task_status] == STATUS_FILTER_STATES[0]:
    822 		# List task titles in group
    823 		group_path     = generate_group_path(tid)
    824 
    825 		# List groups to indicate when a task is a group
    826 		# Identify if task is a group
    827 		groups         = os.listdir(data_location_groups)
    828 		# End Identify if task is a group
    829 
    830 		# print items in tid in order
    831 		for n,fn in enumerate(sorted(os.listdir(group_path))):
    832 			# print position in list
    833 			current_postion = baseconvert_to_dec(fn[:ORDER_ID_LENGTH])
    834 
    835 			# Identify if task is a group
    836 			# Remove order_id, keep task id only
    837 			group = '     '
    838 			if fn[ORDER_ID_LENGTH:] in groups:
    839 				group = 'GROUP'
    840 			if is_linked(fn[ORDER_ID_LENGTH:]):
    841 				group = ' LINK'
    842 			# End Identify if task is a group
    843 			# Get task title
    844 			# Remove order_id, keep task id only
    845 			task_path = generate_task_path(fn[ORDER_ID_LENGTH:])
    846 			f         = open('%s/description.txt'%task_path)
    847 			# Remove order_id, keep task id only
    848 			if (not n) and (not tid == 'root'):
    849 				# First task is group title
    850 				# filter status, keep task if status filter is enabled
    851 				task_status = get_status(fn[ORDER_ID_LENGTH:])
    852 				if status_filters_d[task_status] == STATUS_FILTER_STATES[0]:
    853 					task_d             = {}
    854 					task_d['head']     = 'head group'
    855 					task_d['tid']      = fn[ORDER_ID_LENGTH:]
    856 					task_d['position'] = current_postion
    857 					task_d['group']    = group
    858 					task_d['title']    = f.readline().strip()
    859 					task_d['status']   = get_status(task_d['tid'])
    860 					task_d['ctime']    = get_creation_date(task_d['tid'])[1]
    861 					result.append(task_d)
    862 			else:
    863 				# filter status, keep task if status filter is enabled
    864 				task_status = get_status(fn[ORDER_ID_LENGTH:])
    865 				if status_filters_d[task_status] == STATUS_FILTER_STATES[0]:
    866 					task_d             = {}
    867 					task_d['head']     = 'element'
    868 					task_d['tid']      = fn[ORDER_ID_LENGTH:]
    869 					task_d['position'] = current_postion
    870 					task_d['group']    = group
    871 					task_d['title']    = f.readline().strip()
    872 					task_d['status']   = get_status(task_d['tid'])
    873 					task_d['ctime']    = get_creation_date(task_d['tid'])[1]
    874 					result.append(task_d)
    875 			f.close()
    876 		task_d             = {}
    877 		task_d['head']     = 'empty line'
    878 		task_d['tid']      = ''
    879 		task_d['position'] = 0
    880 		task_d['group']    = ''
    881 		task_d['title']    = ''
    882 		task_d['status']   = ''
    883 		task_d['ctime']    = 0
    884 		result.append(task_d)
    885 	return result
    886 
    887 ## lists all items in the tid group
    888 # @return list of tasks and groups title
    889 # @param[in] tid task id
    890 # @ingroup EDI_CORE
    891 def list_tree(tid):
    892 	result     = []
    893 	# walk_group is the list of groups to visit. FIFO
    894 	walk_group = [tid]
    895 	# the while loop goes through all the group that are found
    896 	while walk_group:
    897 		# list visible groups only
    898 		task_status = get_status(walk_group[0])
    899 		if status_filters_d[task_status] == STATUS_FILTER_STATES[0]:
    900 			# list items in first group
    901 			tasks      = sorted(os.listdir(generate_group_path(walk_group[0])))
    902 			result    += list_group(walk_group[0])
    903 
    904 			# add group found in first group
    905 			for t in tasks:
    906 				# Check tasks that are not title task in a group
    907 				if (t[ORDER_ID_LENGTH:] != walk_group[0]) and is_this_task_a_group(t[ORDER_ID_LENGTH:]):
    908 					walk_group.append(t[ORDER_ID_LENGTH:])
    909 
    910 		# remove first group to list items in next group
    911 		del walk_group[0]
    912 	return result
    913 
    914 ## generates a random task id
    915 # @return new task id
    916 # @ingroup EDI_CORE
    917 def generate_id():
    918 	return ''.join([random.choice(ID_BASE) for _ in range(ID_LENGTH)])
    919 
    920 ## converts decimal number to BASE (base 65)
    921 # @return string representing a number in base BASE
    922 # @param[in] n integer
    923 # @ingroup EDI_CORE
    924 def baseconvert(n):
    925 	s = ""
    926 	while 1:
    927 		r = n % BASE
    928 		s = ID_BASE[r] + s
    929 		n = n / BASE
    930 		if n == 0:
    931 			break
    932 
    933 	s = s.rjust(ORDER_ID_LENGTH, ',')
    934 	return s
    935 
    936 ## converts BASE number to decimal
    937 # @return n integer
    938 # @param[in] n string representing a number in base BASE
    939 # @ingroup EDI_CORE
    940 def baseconvert_to_dec(n):
    941 	r     = 0
    942 	power = 1
    943 	for digit in reversed(list(n)):
    944 		r     += ID_BASE_STRING.find(digit) * power
    945 		power *= BASE
    946 	return r
    947 
    948 ## add task to group folder in database groups
    949 # @param[in] tid task id
    950 # @param[in] group task id
    951 # @ingroup EDI_CORE
    952 # Tasks are added at the top or bottom of the list.
    953 def add_task_to_group_folder(tid,group):
    954 	# Create an entry in group
    955 	tasks              = sorted(os.listdir(generate_group_path(group)))
    956 	# Add +1 to last order_id to have the task last in the list
    957 	if tasks:
    958 		if (len(tasks) == 1) or (add_top_or_bottom == 'bottom'):
    959 			# add tasks in bottom
    960 			order_id = baseconvert(baseconvert_to_dec(tasks[-1][:ORDER_ID_LENGTH])+1)
    961 		else:
    962 			# add tasks on top
    963 			# temporary orderid #
    964 			orderid_and_tid = '#' * ORDER_ID_LENGTH + tid
    965 			if group == 'root':
    966 				to_pos = 0
    967 			else:
    968 				# add new tasks after group title
    969 				to_pos = 1
    970 
    971 			tasks           = tasks[:to_pos] + [orderid_and_tid] + tasks[to_pos:]
    972 
    973 			# Move tasks
    974 			path            = generate_group_path(group)
    975 			for n,t in enumerate(tasks):
    976 				if n == to_pos:
    977 					# set orderid on top
    978 					order_id = baseconvert(n)
    979 				else:
    980 					os.rename('%s/%s'%(path,t),'%s/%s%s'%(path,baseconvert(n),t[ORDER_ID_LENGTH:]))
    981 
    982 	else:
    983 		# start at 0 when group is empty
    984 		order_id = baseconvert(0)
    985 	tid_in_groups_path = '%s/%s%s'%(generate_group_path(group),order_id,tid)
    986 	# remove double slash that can come up
    987 	tid_in_groups_path = tid_in_groups_path.replace('//','/')
    988 	f                  = open(tid_in_groups_path,'w')
    989 	f.close()
    990 
    991 	# return tid_in_groups_path to easily add new tasks to group_directory_file_list and save time
    992 	return tid_in_groups_path
    993 
    994 ## creates a task and opens vi
    995 # @param[in] group task id
    996 # @ingroup EDI_CORE
    997 def create_task(group):
    998 	# Open text editor
    999 	# create task in tasks folder
   1000 #:define create_task_part1
   1001 	tid        =  generate_id()
   1002 
   1003 	# Save text in tasks
   1004 	task_path  = generate_task_path(tid)
   1005 	os.mkdir(task_path)
   1006 #:end
   1007 	os.system('%s %s/description.txt' % (editor,task_path))
   1008 	if not os.path.isfile('%s/description.txt' % task_path):
   1009 		# file doesnt exist, abort task creation
   1010 		shutil.rmtree(task_path)
   1011 		return 'no new task, the description is empty'
   1012 
   1013 	# Save creation time
   1014 #:define save_task_creation_time
   1015 	f          = open('%s/ctime.txt'%task_path,'w')
   1016 	(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/ctime.txt'%task_path)
   1017 	task_mtime = time.ctime(mtime)
   1018 	f.write(task_mtime)
   1019 	f.close()
   1020 #:end
   1021 
   1022 	# create status, active by default
   1023 #:define create_task_part2
   1024 	# Create status
   1025 	f          = open('%s/status'%task_path,'w')
   1026 	f.write(TASK_STATUS[TASK_STATUS_ACTIVE])
   1027 	f.close()
   1028 
   1029 	tid_in_groups_path = add_task_to_group_folder(tid, group)
   1030 
   1031 	global group_directory_file_list
   1032 	if group_directory_file_list:
   1033 		group_directory_file_list.append(tid_in_groups_path)
   1034 
   1035 	# autolink
   1036 	global autolink
   1037 	if autolink:
   1038 		for g in autolink:
   1039 			if is_this_task_a_group(g):
   1040 				# link to groups existing in current database
   1041 				add_task_reference_to_a_group(tid,g)
   1042 #:end
   1043 	edi_log('created %s in group %s'%(tid,group))
   1044 	return tid
   1045 
   1046 ## create task and copy text file
   1047 # @param[in] group task id
   1048 # @param[in] text_file filename of text file to copy
   1049 # @ingroup EDI_CORE
   1050 def add_task(group,text_file):
   1051 #:create_task_part1
   1052 	tid        =  generate_id()
   1053 
   1054 	# Save text in tasks
   1055 	task_path  = generate_task_path(tid)
   1056 	os.mkdir(task_path)
   1057 	shutil.copy(text_file,'%s/description.txt'%task_path)
   1058 #:save_task_creation_time
   1059 	f          = open('%s/ctime.txt'%task_path,'w')
   1060 	(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/ctime.txt'%task_path)
   1061 	task_mtime = time.ctime(mtime)
   1062 	f.write(task_mtime)
   1063 	f.close()
   1064 #:create_task_part2
   1065 	# Create status
   1066 	f          = open('%s/status'%task_path,'w')
   1067 	f.write(TASK_STATUS[TASK_STATUS_ACTIVE])
   1068 	f.close()
   1069 
   1070 	tid_in_groups_path = add_task_to_group_folder(tid, group)
   1071 
   1072 	global group_directory_file_list
   1073 	if group_directory_file_list:
   1074 		group_directory_file_list.append(tid_in_groups_path)
   1075 
   1076 	# autolink
   1077 	global autolink
   1078 	if autolink:
   1079 		for g in autolink:
   1080 			if is_this_task_a_group(g):
   1081 				# link to groups existing in current database
   1082 				add_task_reference_to_a_group(tid,g)
   1083 	edi_log('created %s in group %s'%(tid,group))
   1084 	return tid
   1085 
   1086 ## create task with description text
   1087 # @param[in] group task id
   1088 # @param[in] text string
   1089 # @ingroup EDI_CORE
   1090 def add_text(group,text):
   1091 #:create_task_part1
   1092 	tid        =  generate_id()
   1093 
   1094 	# Save text in tasks
   1095 	task_path  = generate_task_path(tid)
   1096 	os.mkdir(task_path)
   1097 	f = open('%s/description.txt'%task_path,'w')
   1098 	f.write(text)
   1099 	f.close()
   1100 #:save_task_creation_time
   1101 	f          = open('%s/ctime.txt'%task_path,'w')
   1102 	(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/ctime.txt'%task_path)
   1103 	task_mtime = time.ctime(mtime)
   1104 	f.write(task_mtime)
   1105 	f.close()
   1106 #:create_task_part2
   1107 	# Create status
   1108 	f          = open('%s/status'%task_path,'w')
   1109 	f.write(TASK_STATUS[TASK_STATUS_ACTIVE])
   1110 	f.close()
   1111 
   1112 	tid_in_groups_path = add_task_to_group_folder(tid, group)
   1113 
   1114 	global group_directory_file_list
   1115 	if group_directory_file_list:
   1116 		group_directory_file_list.append(tid_in_groups_path)
   1117 
   1118 	# autolink
   1119 	global autolink
   1120 	if autolink:
   1121 		for g in autolink:
   1122 			if is_this_task_a_group(g):
   1123 				# link to groups existing in current database
   1124 				add_task_reference_to_a_group(tid,g)
   1125 	edi_log('created %s in group %s'%(tid,group))
   1126 	return tid
   1127 
   1128 ## create task and copy text file, filename is task title
   1129 # @param[in] group task id
   1130 # @param[in] text_file filename of text file to copy
   1131 # @ingroup EDI_CORE
   1132 def add_task_and_filename(group,text_file):
   1133 	tid = add_task(group,text_file)
   1134 
   1135 	# Add file name on first line
   1136 	f   = open('%s/description.txt'%generate_task_path(tid))
   1137 	des = f.readlines()
   1138 	f.close()
   1139 	f   = open('%s/description.txt'%generate_task_path(tid),'w')
   1140 	# save text filename only
   1141 	f.write('%s\n'%text_file.split('/')[-1])
   1142 	for l in des:
   1143 		f.write(l)
   1144 	f.close()
   1145 
   1146 	return tid
   1147 
   1148 ## create many tasks from a text file
   1149 # @param[in] group task id
   1150 # @param[in] text_file filename of text file to copy
   1151 # @ingroup EDI_CORE
   1152 def add_many_tasks(group,text_file):
   1153 	r               = ''
   1154 
   1155 	# Copy a task to text string
   1156 	# Remove '#' from first line, first character position
   1157 	# Add task to database
   1158 	f               = open(text_file)
   1159 	status          = 'start'
   1160 	text            = ''
   1161 	number_of_tasks = 0
   1162 	for l in f.readlines():
   1163 		if l.rstrip() == '---':
   1164 			if status != 'start':
   1165 				# add task to database
   1166 				add_text(group,text)
   1167 				number_of_tasks += 1
   1168 			text   = ''
   1169 			status = 'start'
   1170 		else:
   1171 			if (status == 'start') and (l[0] == '#'):
   1172 				l    = l[1:]
   1173 			text   += l
   1174 			status = 'writing task'
   1175 	f.close()
   1176 	if status == 'writing task':
   1177 		# add task to database
   1178 		add_text(group,text)
   1179 		number_of_tasks += 1
   1180 
   1181 	edi_log('created %s tasks in group %s'%(number_of_tasks,group))
   1182 	r               = 'created %s tasks in group %s'%(number_of_tasks,group)
   1183 	return r
   1184 
   1185 ## create many one line tasks from a text file
   1186 # @param[in] group task id
   1187 # @param[in] text_file filename of text file to copy
   1188 # @ingroup EDI_CORE
   1189 def add_many_one_line_tasks(group,text_file):
   1190 	r               = []
   1191 
   1192 	f               = open(text_file)
   1193 	number_of_tasks = 0
   1194 	for l in f.readlines():
   1195 		add_text(group,l.strip())
   1196 		number_of_tasks += 1
   1197 	f.close()
   1198 
   1199 	edi_log('created %s tasks in group %s'%(number_of_tasks,group))
   1200 	r               = 'created %s tasks in group %s'%(number_of_tasks,group)
   1201 	return r
   1202 
   1203 ## create group from text file
   1204 # @param[in] group task id
   1205 # @param[in] text_file with group structure
   1206 # @ingroup EDI_CORE
   1207 def add_many_groups_from_text(group,text_file):
   1208 	r               = []
   1209 
   1210 	f               = open(text_file)
   1211 	number_of_tasks = 0
   1212 	group_stack     = [group]
   1213 	for l in f.readlines():
   1214 		# count space indents
   1215 		l_l = l.split(' ')
   1216 		indent = 0
   1217 		for s in l_l:
   1218 			if s:
   1219 				break
   1220 			indent += 1
   1221 		if indent < len(group_stack)-1:
   1222 			# remove groups from stack if same level or higher levels
   1223 			del group_stack[-(len(group_stack)-1 - indent):]
   1224 		l   = l.strip()
   1225 		tid = add_text(group_stack[-1],l)
   1226 		create_group(tid)
   1227 		group_stack.append(tid)
   1228 		number_of_tasks += 1
   1229 	f.close()
   1230 
   1231 	edi_log('created %s groups in group %s'%(number_of_tasks,group))
   1232 	r               = 'created %s groups in group %s'%(number_of_tasks,group)
   1233 	return r
   1234 
   1235 ## copy description to path using first line of description as filename
   1236 # @return path and filname
   1237 # @param[in] tid task id
   1238 # @param[in] path destination directory for task description
   1239 # @ingroup EDI_CORE
   1240 def export_task_to_a_file(tid,path):
   1241 	f   = open('%s/description.txt'%generate_task_path(tid))
   1242 	fn  = f.readline().strip()
   1243 	des = f.readlines()
   1244 	f.close()
   1245 	f   = open('%s/%s'%(path,fn),'w')
   1246 	for l in des:
   1247 		f.write(l)
   1248 	f.close()
   1249 	r = '%s/%s'%(path,fn)
   1250 	# remove eventual double //
   1251 	return r.replace(os.sep*2,os.sep)
   1252 
   1253 ## print description of task tid
   1254 # @return list of strings
   1255 # @param[in] tid task id
   1256 # @ingroup EDI_CORE
   1257 def display_task(tid):
   1258 	task_path = generate_task_path(tid)
   1259 	f           = open('%s/description.txt'%task_path)
   1260 	description = f.readlines()
   1261 	f.close()
   1262 
   1263 	# print tid, status and first line
   1264 	description[0] = generate_task_string_with_tid(tid,description[0])
   1265 	return description
   1266 
   1267 ## find group containing task tid in groups folder
   1268 # @return tid
   1269 # @param[in] tid task id
   1270 # @ingroup EDI_CORE
   1271 def find_group_containing_task(tid):
   1272 	if tid == 'root':
   1273 		# root has not parent group, return root
   1274 		return tid
   1275 #:define walk_groups
   1276 	global data_location_groups
   1277 	# reuse previously created group list, to save time
   1278 	global group_directory_file_list
   1279 	if not group_directory_file_list:
   1280 		group_directory_file_list = ffind(data_location_groups)
   1281 	groups_and_tasks = group_directory_file_list
   1282 	for t in groups_and_tasks:
   1283 		if (tid == t[-ID_LENGTH:]) and (tid != t.split('/')[-2]):
   1284 			group = t.split('/')[-2]
   1285 #:end
   1286 	try:
   1287 		a = group
   1288 	except:
   1289 		print '\n\nEDI_ERROR: Database inconsistent - run: find %s|grep %s and remove reference.\n'%(data_location, tid)
   1290 		group = 'error'
   1291 	return group
   1292 
   1293 ## find group containing task tid in groups folder and print
   1294 # @return list of strings
   1295 # @param[in] tid task id
   1296 # @ingroup EDI_CORE
   1297 # Shows path in tree and title for each group in path
   1298 def show_group_for_task(tid):
   1299 	global data_location_tree
   1300 	r                 = []
   1301 
   1302 	# set group string to be displayed on first line
   1303 	group_s           = '     '
   1304 	if is_this_task_a_group(tid):
   1305 		group_s = 'GROUP'
   1306 	if is_linked(tid):
   1307 		group_s = ' LINK'
   1308 
   1309 #:walk_groups
   1310 	global data_location_groups
   1311 	# reuse previously created group list, to save time
   1312 	global group_directory_file_list
   1313 	if not group_directory_file_list:
   1314 		group_directory_file_list = ffind(data_location_groups)
   1315 	groups_and_tasks = group_directory_file_list
   1316 	for t in groups_and_tasks:
   1317 		if (tid == t[-ID_LENGTH:]) and (tid != t.split('/')[-2]):
   1318 			group = t.split('/')[-2]
   1319 			tree_path         = find_group_in_tree(group)
   1320 			# p is data_location folder/tree
   1321 			p_l               = data_location_tree.split('/')[-2:]
   1322 			p                 = '/'.join(p_l)
   1323 			# if empty then command is run in tree root
   1324 			if tree_path.split(p)[-1]:
   1325 				# print path of tids: tid/tid...
   1326 				r.append(tree_path.split(p)[-1][1:])
   1327 				group_titles_in_path = []
   1328 				for g in tree_path.split(p)[-1][1:].split('/'):
   1329 					group_titles_in_path.append(get_task_title(g))
   1330 				# print title/title...
   1331 				r.append('/'.join(group_titles_in_path))
   1332 			r.append(generate_group_string_with_tid(group,get_task_title(group)))
   1333 			r.append('')
   1334 
   1335 	# Print media and attachments
   1336 	# media type
   1337 	media             = get_media(tid)[0]
   1338 	# attachment list
   1339 	attachments       = get_attachments(tid)
   1340 
   1341 	# Print task, colors, group list
   1342 	color             = get_forground_color(tid)
   1343 	fc                = '%d,%d,%d,%d' % (color[0],color[1],color[2],color[3])
   1344 	color             = get_background_color(tid)
   1345 	bc                = '%d,%d,%d,%d' % (color[0],color[1],color[2],color[3])
   1346 	# Figure out creation time and modification times for description and status
   1347 	task_ctime        = get_creation_date(tid)[0]
   1348 
   1349 	task_path         = generate_task_path(tid)
   1350 	(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/description.txt'%task_path)
   1351 	description_mtime = time.ctime(mtime)
   1352 	(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/status'%task_path)
   1353 	status_mtime      = time.ctime(mtime)
   1354 	r                 = ['Task:\n%s\n\n'%generate_task_string_with_tid( tid,'%s %s'%(group_s, get_task_title(tid)) )] + ['Media type: %s'%media] + ['\nAttachments:'] + attachments + ['\n\nforeground color:        %s\nbackground color:        %s\nCreation time:           %s\nLast description change: %s\nLast status change:      %s\n\nGroup list:\n' % (fc, bc, task_ctime, description_mtime, status_mtime)] + r
   1355 	return r
   1356 
   1357 ## find group in tree folder
   1358 # @return path_in_tree
   1359 # @param[in] tid task id
   1360 # @ingroup EDI_CORE
   1361 def find_group_in_tree(group):
   1362 	if group == 'root':
   1363 		path_in_tree = data_location_tree
   1364 	else:
   1365 		path_in_tree = ''
   1366 		# list all group paths in tree
   1367 		f            = os.popen('find %s'%data_location_tree)
   1368 		groups       = [i.strip() for i in f.readlines()]
   1369 		f.close()
   1370 		# remove data_location_tree from paths
   1371 		del groups[0]
   1372 		# find the group in group paths
   1373 		for g in groups:
   1374 			if g.split('/')[-1] == group:
   1375 				path_in_tree = g
   1376 	return path_in_tree
   1377 
   1378 ## Determines if a task has multiple references
   1379 # @return integer 0 or 1
   1380 # @param[in] tid task id
   1381 # @ingroup EDI_CORE
   1382 def is_linked(tid):
   1383 	status = 0
   1384 	task_linked_groups_path = '%s/groups/' % generate_task_path(tid)
   1385 	# check if tid/groups exists
   1386 	if os.path.exists(task_linked_groups_path):
   1387 		groups = os.listdir(task_linked_groups_path)
   1388 		# check if task is linked to more than 1 group
   1389 		if len(groups) > 1:
   1390 			status = 1
   1391 	return status
   1392 
   1393 ## convert task to group
   1394 # @return list of stings when there is an error
   1395 # @param[in] tid task id
   1396 # @ingroup EDI_CORE
   1397 def create_group(tid):
   1398 	r                         = []
   1399 	# convert task to group only when task is not linked
   1400 	if is_linked(tid):
   1401 		r.append('Converting linked task to group removes links. The task groups are:')
   1402 		r += show_group_for_task(tid)
   1403 
   1404 		# delete tid/groups because groups are not linked
   1405 		task_linked_groups_path = '%s/groups/' % generate_task_path(tid)
   1406 		groups                  = os.listdir(task_linked_groups_path)
   1407 		# remove all links except for the first group
   1408 		for g in groups[1:]:
   1409 			delete_linked_task(g,tid)
   1410 		if os.path.exists(task_linked_groups_path):
   1411 			shutil.rmtree(task_linked_groups_path)
   1412 		r.append('Created group %s in %s'%(tid,groups[0]))
   1413 	# create new group in groups folder
   1414 	os.mkdir('%s/%s'%(data_location_groups,tid))
   1415 	# First task in group is group title task
   1416 	order_id                  = baseconvert(0)
   1417 	f                         = open('%s/%s%s'%(generate_group_path(tid),order_id,tid),'w')
   1418 	f.close()
   1419 
   1420 	# Add group in tree
   1421 	# update group list
   1422 	global group_directory_file_list
   1423 	group_directory_file_list = []
   1424 	group                     = find_group_containing_task(tid)
   1425 	if group == 'root':
   1426 		os.mkdir('%s/%s'%(data_location_tree,tid))
   1427 	else:
   1428 		os.mkdir('%s/%s'%(find_group_in_tree(group),tid))
   1429 
   1430 	# Change status active to void by default
   1431 	# To avoid filtering groups
   1432 	if get_status(tid) == TASK_STATUS[TASK_STATUS_ACTIVE]:
   1433 		# set status to void
   1434 		set_status(tid,TASK_STATUS_VOID)
   1435 	edi_log('created group %s'%tid)
   1436 	return r
   1437 
   1438 ## convert group to task
   1439 # @param[in] group group to convert to task
   1440 # @ingroup EDI_CORE
   1441 def convert_group_to_task(group):
   1442 	r = []
   1443 
   1444 	# list all tasks (data_location_groups/GROUP/'ORDER_ID''TASK_ID') in groups folder
   1445 	groups_and_tasks = ffind(data_location_groups)
   1446 
   1447 	convert_group_status = 'delete'
   1448 	for t2 in groups_and_tasks:
   1449 		# search a task in group that is not tid
   1450 		if ('%s/'%group in t2) and (group not in t2.split('/')[-1]):
   1451 			convert_group_status = 'There is another task in the group, keep group.'
   1452 			r.append(convert_group_status)
   1453 	if (convert_group_status == 'delete') and (not 'root' in group):
   1454 		# Delete group title and group folder
   1455 		os.remove('%s/%s%s'%(generate_group_path(group),baseconvert(0),group))
   1456 		os.rmdir(generate_group_path(group))
   1457 		# Delete group in tree
   1458 		if find_group_in_tree(group):
   1459 			shutil.rmtree(find_group_in_tree(group))
   1460 		# change state from void to active
   1461 		set_status(group,TASK_STATUS_ACTIVE)
   1462 		edi_log('converted group %s to task'%group)
   1463 	return r
   1464 
   1465 ## delete task tid in all groups
   1466 # @return group id
   1467 # @param[in] tid task id
   1468 # @ingroup EDI_CORE
   1469 def delete_task(tid):
   1470 	# save original tid parameter for log
   1471 	tid_param        = tid
   1472 	# Delete task in tasks
   1473 	shutil.rmtree(generate_task_path(tid))
   1474 
   1475 	# Delete task reference in groups
   1476 	# list all tasks (data_location_groups/GROUP/'ORDER_ID''TASK_ID') in groups folder
   1477 	groups_and_tasks = ffind(data_location_groups)
   1478 
   1479 	# Identify if task is a group
   1480 	is_a_group       = is_this_task_a_group(tid)
   1481 
   1482 	# Return group task, it changes when first task is deleted
   1483 	group_id         = ''
   1484 	# list groups to reorder at the end
   1485 	reorder_groups   = []
   1486 	for t in groups_and_tasks:
   1487 		# Delete reference in upper group, not the title task of the group
   1488 		if (tid == t[-ID_LENGTH:]) and (tid != t.split('/')[-2]):
   1489 			# group is the group for task tid
   1490 			group    = t.split('/')[-2]
   1491 			group_id = group
   1492 			# Delete task reference in group
   1493 			os.remove(t)
   1494 			reorder_groups.append(group_id)
   1495 			if is_a_group:
   1496 				# First task becomes a group, add a reference in group group
   1497 				# list tasks in order in tid group
   1498 				group_tasks = sorted(os.listdir(generate_group_path(tid)))
   1499 				# Delete emtpy group or first task in group becomes the group title.
   1500 				if len(group_tasks) == 1:
   1501 					shutil.rmtree(generate_group_path(tid))
   1502 					# Delete group in tree
   1503 					# when a group is deleted, subgroups are automatically deleted in the tree
   1504 					if find_group_in_tree(tid):
   1505 						shutil.rmtree(find_group_in_tree(tid))
   1506 				else:
   1507 					# Remove order_id, keep task id only
   1508 					first_task  = group_tasks[1][ORDER_ID_LENGTH:]
   1509 					group_id    = first_task
   1510 					# Create an entry in group at the same position as tid had
   1511 					order_id    = t.split('/')[-1][:ORDER_ID_LENGTH]
   1512 					f           = open('%s/%s%s'%(generate_group_path(group),order_id,first_task),'w')
   1513 					f.close()
   1514 
   1515 					# Delete group task of group of more then 2 tasks, first task becomes a group
   1516 					# delete orderidtaskid
   1517 					os.remove('%s/%s'%(generate_group_path(tid),group_tasks[0]))
   1518 					os.rename('%s/%s'%(generate_group_path(tid),group_tasks[1]),'%s/%s%s'%(generate_group_path(tid),baseconvert(0),first_task))
   1519 					# reorder tasks to remove gap between group title task and first task
   1520 					path        = generate_group_path(tid)
   1521 					tasks       = sorted(os.listdir(path))
   1522 					for n,t in enumerate(tasks):
   1523 						os.rename('%s/%s'%(path,t),'%s/%s%s'%(path,baseconvert(n),t[ORDER_ID_LENGTH:]))
   1524 					# rename group in groups folder
   1525 					os.rename(generate_group_path(tid),generate_group_path(first_task))
   1526 					# rename group in tree folder
   1527 					group_tree_path       = find_group_in_tree(tid)
   1528 					group_tree_path_l     = group_tree_path.split('/')
   1529 					group_tree_path_l[-1] = first_task
   1530 					new_group_tree_path   = '/'.join(group_tree_path_l)
   1531 					os.rename(group_tree_path,new_group_tree_path)
   1532 
   1533 	# reorder tasks to remove gaps in reorder_groups
   1534 	for tid in reorder_groups:
   1535 		path        = generate_group_path(tid)
   1536 		tasks       = sorted(os.listdir(path))
   1537 		for n,t in enumerate(tasks):
   1538 			os.rename('%s/%s'%(path,t),'%s/%s%s'%(path,baseconvert(n),t[ORDER_ID_LENGTH:]))
   1539 
   1540 	# Return group task
   1541 	edi_log('deleted %s in %s'%(tid_param,' '.join(reorder_groups)))
   1542 	return group_id
   1543 
   1544 ## Delete task only if it is linked in one group
   1545 # @return group id
   1546 # @param[in] group task id
   1547 # @param[in] tid task id
   1548 # @ingroup EDI_CORE
   1549 def delete_linked_task(group,tid):
   1550 	group_id = group
   1551 	if not is_linked(tid):
   1552 		group_id = delete_task(tid)
   1553 	else:
   1554 		# Delete task reference in group
   1555 		# find task in group: ORDER_IDTASK_ID
   1556 		tasks = os.listdir(generate_group_path(group))
   1557 		for t in tasks:
   1558 			if t[ORDER_ID_LENGTH:] == tid:
   1559 				os.remove('%s%s' % (generate_group_path(group), t))
   1560 
   1561 		# delete group in tid/groups
   1562 		os.remove('%s/groups/%s' % (generate_task_path(tid), group))
   1563 
   1564 		# reorder tasks to remove gaps in group
   1565 		path  = generate_group_path(group)
   1566 		tasks = os.listdir(path)
   1567 		for n,t in enumerate(tasks):
   1568 			os.rename('%s/%s'%(path,t),'%s/%s%s'%(path,baseconvert(n),t[ORDER_ID_LENGTH:]))
   1569 
   1570 		edi_log('deleted %s in group %s'%(tid,group))
   1571 
   1572 	return group_id
   1573 
   1574 ## delete group tid
   1575 # @return group id
   1576 # @param[in] tid task id
   1577 # @ingroup EDI_CORE
   1578 def delete_group(tid):
   1579 	# Delete tasks in group
   1580 	group_tasks = sorted(os.listdir(generate_group_path(tid)))
   1581 	if (len(group_tasks) == 0) and (tid == 'root'):
   1582 		# the database is already empty
   1583 		return tid
   1584 	if tid != 'root':
   1585 		# root group doesnt have a title
   1586 		# Remove group title from the loop to delete tasks only and then group
   1587 		del group_tasks[0]
   1588 
   1589 	# Delete tasks and groups recursively
   1590 	for t in group_tasks:
   1591 		# Remove order_id, keep task id only
   1592 		oid = t[ORDER_ID_LENGTH:]
   1593 		if not is_this_task_a_group(oid):
   1594 			# delete tasks that are linked only once
   1595 			delete_linked_task(tid, oid)
   1596 		else:
   1597 			delete_group(oid)
   1598 
   1599 	if tid != 'root':
   1600 		# never delete root group
   1601 		return delete_task(tid)
   1602 	else:
   1603 		# return root and keep root task
   1604 		return tid
   1605 
   1606 ## edit task with vi
   1607 # @param[in] tid task id
   1608 # @ingroup EDI_CORE
   1609 def edit_task(tid):
   1610 	os.system('%s %s/description.txt' % (editor, generate_task_path(tid)))
   1611 	edi_log('edited %s'%tid)
   1612 
   1613 ## move task from group at at_pos to to_pos and reorder
   1614 # @return list of stings when there is an error
   1615 # @param[in] group task id
   1616 # @param[in] at_pos selected position
   1617 # @param[in] to_pos insert position
   1618 # @ingroup EDI_CORE
   1619 def change_task_order(group,at_pos,to_pos):
   1620 	# save original group parameter for log
   1621 	group_param     = group
   1622 	# List tasks in group
   1623 	r               = []
   1624 	path            = generate_group_path(group)
   1625 	tasks           = sorted(os.listdir(path))
   1626 
   1627 	# Verify position
   1628 	# Get task
   1629 	try:
   1630 		orderid_and_tid = tasks[at_pos]
   1631 	except:
   1632 		r.append('%d is an invalid position.'%at_pos)
   1633 		return r
   1634 	if to_pos == 0:
   1635 		tid = orderid_and_tid[ORDER_ID_LENGTH:]
   1636 		# do not move linked tasks to position 0
   1637 		if is_linked(tid):
   1638 			r.append('Converting linked task to group removes links. The task groups are:')
   1639 			r += show_group_for_task(tid)
   1640 
   1641 			# delete tid/groups because groups are not linked
   1642 			task_linked_groups_path = '%s/groups/' % generate_task_path(tid)
   1643 			groups                  = os.listdir(task_linked_groups_path)
   1644 			# remove all links except for the first group
   1645 			for g in groups:
   1646 				if not g == group:
   1647 					delete_linked_task(g,tid)
   1648 			if os.path.exists(task_linked_groups_path):
   1649 				shutil.rmtree(task_linked_groups_path)
   1650 			parent_group             = find_group_containing_task(group)
   1651 			r.append('Created group %s in %s'%(tid,parent_group))
   1652 		# do not move groups to position 0
   1653 		if is_this_task_a_group(tid):
   1654 			r.append('Having a group in group title is not supported.')
   1655 			return r
   1656 	# Insert task at to_pos
   1657 	if to_pos > at_pos:
   1658 		# +1 because at_pos reference will be deleted and will shift to_pos reference
   1659 		to_pos += 1
   1660 		tasks   = tasks[:to_pos] + [orderid_and_tid] + tasks[to_pos:]
   1661 		# Delete task at at_pos
   1662 		del tasks[at_pos]
   1663 	else:
   1664 		# rename group title, when to_pos in 0
   1665 		if (to_pos == 0) and (not group == 'root'):
   1666 			to_pos_tid = tasks[to_pos][ORDER_ID_LENGTH:]
   1667 
   1668 		tasks   = tasks[:to_pos] + [orderid_and_tid] + tasks[to_pos:]
   1669 		# Delete task at at_pos+1 because the new position is before at_pos
   1670 		# at_pos was shifted when to_pos reference was added above
   1671 		del tasks[at_pos+1]
   1672 
   1673 #:define reorder_tasks
   1674 	# Move tasks
   1675 	for n,t in enumerate(tasks):
   1676 		os.rename('%s/%s'%(path,t),'%s/%s%s'%(path,baseconvert(n),t[ORDER_ID_LENGTH:]))
   1677 #:end
   1678 
   1679 	# rename group title, when to_pos in 0
   1680 	if (to_pos == 0) and (not group == 'root'):
   1681 		# rename group in parent group
   1682 		# group is tid for parent group and to_pos_tid is equal to group input parameter because to_pos is 0
   1683 		group = find_group_containing_task(group)
   1684 		# to_pos_orderid_and_tid is group in parent group
   1685 		parent_group_tasks = os.listdir(generate_group_path(group))
   1686 		for t in parent_group_tasks:
   1687 			if t[ORDER_ID_LENGTH:] == to_pos_tid:
   1688 				to_pos_orderid_and_tid = t
   1689 		# remove order_id, keep task id only for new group
   1690 		group_id    = orderid_and_tid[ORDER_ID_LENGTH:]
   1691 		# create an entry in parent group at the same position as to_pos tid had
   1692 		order_id    = to_pos_orderid_and_tid[:ORDER_ID_LENGTH]
   1693 		f           = open('%s/%s%s'%(generate_group_path(group),order_id,group_id),'w')
   1694 		f.close()
   1695 
   1696 		# delete group task of group
   1697 		# delete orderidtaskid
   1698 		os.remove('%s/%s'%(generate_group_path(group),to_pos_orderid_and_tid))
   1699 
   1700 		# rename group in groups folder
   1701 		os.rename(generate_group_path(to_pos_tid),generate_group_path(group_id))
   1702 		# rename group in tree folder
   1703 		group_tree_path       = find_group_in_tree(to_pos_tid)
   1704 		group_tree_path_l     = group_tree_path.split('/')
   1705 		group_tree_path_l[-1] = group_id
   1706 		new_group_tree_path   = '/'.join(group_tree_path_l)
   1707 		os.rename(group_tree_path,new_group_tree_path)
   1708 
   1709 		# rename group in linked task and former linked task with a 'groups' folder in tasks database folder
   1710 		group_tasks = os.listdir(generate_group_path(group_id))
   1711 		for group_task in group_tasks:
   1712 			if os.path.exists('%s/groups' % generate_task_path(group_task[ORDER_ID_LENGTH:])):
   1713 				# search for to_pos_tid (group input parameter) in task path groups folder
   1714 				link_groups = os.listdir('%s/groups' % generate_task_path(group_task[ORDER_ID_LENGTH:]))
   1715 				for lg in link_groups:
   1716 					if to_pos_tid == lg:
   1717 						# rename group to new group id since the title changed
   1718 						os.rename('%s/groups/%s' % (generate_task_path(group_task[ORDER_ID_LENGTH:]),to_pos_tid), '%s/groups/%s' % (generate_task_path(group_task[ORDER_ID_LENGTH:]),group_id))
   1719 
   1720 	edi_log('changed task order %s to %s in group %s'%(at_pos, to_pos, group_param))
   1721 	return r
   1722 
   1723 
   1724 ## move task to group
   1725 # @return tid
   1726 # @param[in] tgroup task id, select a task in this group
   1727 # @param[in] tid task id
   1728 # @param[in] group task id, destination group
   1729 # @ingroup EDI_CORE
   1730 def move_task_to_a_group(tgroup,tid,group):
   1731 
   1732 	# find task tid in tgroup and remove task
   1733 	path            = generate_group_path(tgroup)
   1734 	tasks     	= sorted(os.listdir(path))
   1735 	task_in_group   = ''
   1736 	for n,t in enumerate(tasks):
   1737 		# remove task from tasks to reorder tgroup
   1738 		if t[ORDER_ID_LENGTH:] == tid:
   1739 			task_in_group = t
   1740 			del tasks[n]
   1741 			break
   1742 	if not task_in_group:
   1743 		return '%s not found in %s'%(tid,tgroup)
   1744 
   1745 	# Move group in tree
   1746 	if is_this_task_a_group(tid):
   1747 		if find_group_in_tree(tid)[:-ID_LENGTH-1] == find_group_in_tree(group):
   1748 			# prevent moving a group on itself, in tree and moving group title position to parent group
   1749 			return tid
   1750 		shutil.move(find_group_in_tree(tid),find_group_in_tree(group))
   1751 
   1752 	# Remove task in source group
   1753 	os.remove('%s/%s'%(path,task_in_group))
   1754 
   1755 #:reorder_tasks
   1756 	# Move tasks
   1757 	for n,t in enumerate(tasks):
   1758 		os.rename('%s/%s'%(path,t),'%s/%s%s'%(path,baseconvert(n),t[ORDER_ID_LENGTH:]))
   1759 	# check that tid is not already linked in destination group
   1760 	if is_linked(tid):
   1761 		link_groups = os.listdir('%s/groups/' % generate_task_path(tid))
   1762 		if group in link_groups:
   1763 			# removed task reference from tgroup. Remove tgroup reference in task. There is already a tid reference in group, nothing more to do
   1764 			os.remove('%s/groups/%s' % (generate_task_path(tid), tgroup))
   1765 			return tid
   1766 
   1767 	# Create reference in destination group
   1768 	add_task_to_group_folder(tid, group)
   1769 
   1770 	# linked tasks, remove source group and add destination group
   1771 	if is_linked(tid):
   1772 		# remove source tgroup from tid/groups
   1773 		os.remove('%s/groups/%s' % (generate_task_path(tid), tgroup))
   1774 		f         = open('%s/groups/%s'%(generate_task_path(tid), group),'w')
   1775 		f.close()
   1776 
   1777 	edi_log('moved %s in group %s to group %s'%(tid,tgroup,group))
   1778 	return tid
   1779 
   1780 ## copy task to group with new tid
   1781 # @return new tid
   1782 # @param[in] tid task id
   1783 # @param[in] group task id, destination group
   1784 # @ingroup EDI_CORE
   1785 def copy_task_to_a_group(tid,group):
   1786 	# Generate new tid
   1787 	newtid = generate_id()
   1788 	# Copy task in tasks
   1789 	shutil.copytree(generate_task_path(tid),generate_task_path(newtid))
   1790 	# delete tid/groups because new task is not linked
   1791 	task_linked_groups_path = '%s/groups/' % generate_task_path(newtid)
   1792 	if os.path.exists(task_linked_groups_path):
   1793 		shutil.rmtree(task_linked_groups_path)
   1794 	# Copy group in groups and in tree
   1795 	if is_this_task_a_group(tid):
   1796 		# create new group
   1797 		shutil.copytree(generate_group_path(tid),generate_group_path(newtid))
   1798 		# Change group title task to newtid
   1799 		shutil.move('%s/%s%s'%(generate_group_path(newtid),baseconvert(0),tid),'%s/%s%s'%(generate_group_path(newtid),baseconvert(0),newtid))
   1800 
   1801 		# delete tasks to be recreated with new tid
   1802 		tasks = sorted(os.listdir(generate_group_path(newtid)))
   1803 		del tasks[0]
   1804 		for t in tasks:
   1805 			os.remove('%s/%s'%(generate_group_path(newtid),t))
   1806 
   1807 		# Add group in tree
   1808 		if group == 'root':
   1809 			os.mkdir('%s/%s'%(data_location_tree,newtid))
   1810 		else:
   1811 			os.mkdir('%s/%s'%(find_group_in_tree(group),newtid))
   1812 
   1813 
   1814 	# Add reference in group
   1815 	tmp_tid = tid
   1816 	tid     = newtid
   1817 	# Create reference in destination group
   1818 	add_task_to_group_folder(tid, group)
   1819 	tid     = tmp_tid
   1820 
   1821 	if is_this_task_a_group(tid):
   1822 		# walk in group
   1823 		# list items in group
   1824 		tasks      = sorted(os.listdir(generate_group_path(tid)))
   1825 
   1826 		# add group found in first group
   1827 		for t in tasks:
   1828 			# Check tasks that are not title task in a group
   1829 			if t[ORDER_ID_LENGTH:] != tid:
   1830 				# copy_task_to_a_group recursively
   1831 				copy_task_to_a_group(t[ORDER_ID_LENGTH:],newtid)
   1832 	edi_log('copied %s to group %s, created %s'%(tid,group,newtid))
   1833 	return newtid
   1834 
   1835 ## create task path in database using database name
   1836 # @return path to task in tasks
   1837 # @param[in] tid task id
   1838 # @param[in] location database name in data section of easydoneit.ini
   1839 # @ingroup EDI_CORE
   1840 def generate_task_path_in_database(tid,location):
   1841 	global selected
   1842 	global selected_path
   1843 
   1844 	z              = dict(zip(selected, selected_path))
   1845 	location_tasks = '%s/tasks'%z[location]
   1846 	return '%s/%s'%(location_tasks,tid)
   1847 
   1848 ## create group path in database using database name
   1849 # @return path to group in groups
   1850 # @param[in] tid task id
   1851 # @param[in] location database name in data section of easydoneit.ini
   1852 # @ingroup EDI_CORE
   1853 def generate_group_path_in_database(tid,location):
   1854 	global selected
   1855 	global selected_path
   1856 
   1857 	z              = dict(zip(selected, selected_path))
   1858 	location_groups = '%s/groups'%z[location]
   1859 	return '%s/%s/'%(location_groups,tid)
   1860 
   1861 ## find group in tree folder in database
   1862 # @return path to group in tree
   1863 # @param[in] group task id
   1864 # @param[in] location database name in data section of easydoneit.ini
   1865 # @ingroup EDI_CORE
   1866 def find_group_in_tree_in_database(group,location):
   1867 	global selected
   1868 	global selected_path
   1869 
   1870 	z              = dict(zip(selected, selected_path))
   1871 	location_tree  = '%s/tree'%z[location]
   1872 
   1873 	if group == 'root':
   1874 		path_in_tree = location_tree
   1875 	else:
   1876 		path_in_tree = ''
   1877 		# list all group paths in tree
   1878 		f            = os.popen('find %s'%location_tree)
   1879 		groups       = [i.strip() for i in f.readlines()]
   1880 		f.close()
   1881 		# remove data_location_tree from paths
   1882 		del groups[0]
   1883 		# find the group in group paths
   1884 		for g in groups:
   1885 			if g.split('/')[-1] == group:
   1886 				path_in_tree = g
   1887 	return path_in_tree
   1888 
   1889 ## add task with new tid in selected database
   1890 # @param[in] tid task id
   1891 # @param[in] group task id
   1892 # @param[in] location database name in data section of easydoneit.ini
   1893 # @ingroup EDI_CORE
   1894 def add_task_to_group_folder_in_database(tid,group,location):
   1895 	# Create an entry in group
   1896 	tasks     = sorted(os.listdir(generate_group_path_in_database(group,location)))
   1897 	# Add +1 to last order_id to have the task last in the list
   1898 	if tasks:
   1899 		order_id = baseconvert(baseconvert_to_dec(tasks[-1][:ORDER_ID_LENGTH])+1)
   1900 	else:
   1901 		# start at 0 when group is empty
   1902 		order_id = baseconvert(0)
   1903 	f         = open('%s/%s%s'%(generate_group_path_in_database(group,location),order_id,tid),'w')
   1904 	f.close()
   1905 
   1906 ## copy task to group in selected database with new tid
   1907 # @return new tid
   1908 # @param[in] tid task id
   1909 # @param[in] group task id
   1910 # @param[in] location database name in data section of easydoneit.ini
   1911 # @ingroup EDI_CORE
   1912 def copy_task_to_database(tid,location,group):
   1913 	# Generate new tid
   1914 	newtid = generate_id()
   1915 	# Copy task in tasks
   1916 	shutil.copytree(generate_task_path(tid),generate_task_path_in_database(newtid,location))
   1917 	# delete tid/groups because new task is not linked
   1918 	task_linked_groups_path = '%s/groups/' % generate_task_path_in_database(newtid,location)
   1919 	if os.path.exists(task_linked_groups_path):
   1920 		shutil.rmtree(task_linked_groups_path)
   1921 	# Copy group in groups and in tree
   1922 	if is_this_task_a_group(tid):
   1923 		# create new group
   1924 		shutil.copytree(generate_group_path(tid),generate_group_path_in_database(newtid,location))
   1925 		# Change group title task to newtid
   1926 		shutil.move('%s/%s%s'%(generate_group_path_in_database(newtid,location),baseconvert(0),tid),'%s/%s%s'%(generate_group_path_in_database(newtid,location),baseconvert(0),newtid))
   1927 
   1928 		# delete tasks to be recreated with new tid
   1929 		tasks = sorted(os.listdir(generate_group_path_in_database(newtid,location)))
   1930 		del tasks[0]
   1931 		for t in tasks:
   1932 			os.remove('%s/%s'%(generate_group_path_in_database(newtid,location),t))
   1933 
   1934 		# Add group in tree
   1935 		global selected
   1936 		global selected_path
   1937 
   1938 		z              = dict(zip(selected, selected_path))
   1939 		location_tree  = '%s/tree'%z[location]
   1940 		if group == 'root':
   1941 			os.mkdir('%s/%s'%(location_tree,newtid))
   1942 		else:
   1943 			os.mkdir('%s/%s'%(find_group_in_tree_in_database(group,location),newtid))
   1944 
   1945 
   1946 	# Add reference in group
   1947 	# Create reference in destination group
   1948 	add_task_to_group_folder_in_database(newtid, group, location)
   1949 
   1950 	if is_this_task_a_group(tid):
   1951 		# walk in group
   1952 		# list items in group
   1953 		tasks      = sorted(os.listdir(generate_group_path(tid)))
   1954 
   1955 		# add group found in first group
   1956 		for t in tasks:
   1957 			# Check tasks that are not title task in a group
   1958 			if t[ORDER_ID_LENGTH:] != tid:
   1959 				# copy_task_to_database recursively
   1960 				copy_task_to_database(t[ORDER_ID_LENGTH:],location,newtid)
   1961 	return newtid
   1962 
   1963 ## move task to database/group
   1964 # @param[in] tgroup group id for task tid
   1965 # @param[in] tid task id
   1966 # @param[in] location database name in data section of easydoneit.ini
   1967 # @param[in] group destination group id
   1968 # @ingroup EDI_CORE
   1969 # copy and delete
   1970 def move_task_to_a_group_to_database(tgroup,tid,location,group):
   1971 	newtid = copy_task_to_database(tid,location,group)
   1972 	if is_this_task_a_group(tid):
   1973 		delete_group(tid)
   1974 	else:
   1975 		delete_linked_task(tgroup,tid)
   1976 	return newtid
   1977 
   1978 ## add reference to tid in group
   1979 # @param[in] tid task id
   1980 # @param[in] group destination group id
   1981 # @ingroup EDI_CORE
   1982 # Used in edi ln to link tasks
   1983 def add_task_reference_to_a_group(tid,group):
   1984 	prints = []
   1985 	if is_this_task_a_group(tid):
   1986 		prints.append('Select a task to link instead of a group')
   1987 	else:
   1988 		# add group to task folder
   1989 		task_path = generate_task_path(tid)
   1990 		if not os.path.exists('%s/groups/' % task_path):
   1991 			os.mkdir('%s/groups/' % task_path)
   1992 
   1993 			# add first group when task is linked to tid/groups/
   1994 			first_group = find_group_containing_task(tid)
   1995 			f         = open('%s/groups/%s'%(task_path,first_group),'w')
   1996 			f.close()
   1997 		f         = open('%s/groups/%s'%(task_path,group),'w')
   1998 		f.close()
   1999 
   2000 		# add task to group
   2001 		add_task_to_group_folder(tid, group)
   2002 
   2003 	edi_log('linked %s to group %s'%(tid,group))
   2004 	return prints
   2005 
   2006 ## set status for tid
   2007 # @param[in] tid task id
   2008 # @param[in] status_number index in edi_core.TASK_STATUS
   2009 # @ingroup EDI_CORE
   2010 def set_status(tid,status_number):
   2011 	# Change status
   2012 	f         = open('%s/status'%generate_task_path(tid),'w')
   2013 	f.write(TASK_STATUS[status_number])
   2014 	f.close()
   2015 	edi_log('set status for %s to %s'%(tid,TASK_STATUS[status_number].strip()))
   2016 
   2017 ## set all tasks in group to active
   2018 # @param[in] tid task id
   2019 # @ingroup EDI_CORE
   2020 def reset_group_status(tid):
   2021 	group_tasks = os.listdir(generate_group_path(tid))
   2022 	for t in group_tasks:
   2023 		if (not is_this_task_a_group(t[ORDER_ID_LENGTH:])) or (is_this_task_a_group(t[ORDER_ID_LENGTH:]) and (get_status(t[ORDER_ID_LENGTH:]) != TASK_STATUS[TASK_STATUS_VOID])):
   2024 			set_status(t[ORDER_ID_LENGTH:],TASK_STATUS_ACTIVE)
   2025 	edi_log('reset group %s'%tid)
   2026 
   2027 ## search string in tasks folder
   2028 # @param[in] search query string
   2029 # @ingroup EDI_CORE
   2030 def search_string(search):
   2031 	global data_location_tasks
   2032 	global status_filters_d
   2033 	global STATUS_FILTER_STATES
   2034 
   2035 	tasks  = sorted(os.listdir(data_location_tasks))
   2036 
   2037 	# search in descriptions only
   2038 	all_grep_r = []
   2039 	for tid in tasks:
   2040 		if tid != 'root':
   2041 			# search in visible tasks only (filter enable status)
   2042 			task_status = get_status(tid)
   2043 			if status_filters_d[task_status] == STATUS_FILTER_STATES[0]:
   2044 				f           = os.popen('grep -i -R "%s" %s/description.txt'%(search,generate_task_path(tid)))
   2045 				# add tid and filename to results
   2046 				grep_r      = ['/%s/description.txt:%s'%(tid,i) for i in f.readlines()]
   2047 				f.close()
   2048 				all_grep_r  += grep_r
   2049 
   2050 	grep_r     = all_grep_r
   2051 
   2052 	# r lists all hits
   2053 	r      = []
   2054 	for l in grep_r:
   2055 		# replace filename with tid and title (first line in file)
   2056 		# read first line in file
   2057 		l_l          = l.split(':')
   2058 		hit_filename = l_l[0]
   2059 		f            = open('%s%s'%(data_location_tasks,hit_filename))
   2060 		title        = f.readline().strip()
   2061 		f.close()
   2062 		# set tid
   2063 		tid          = hit_filename.split('/')[1]
   2064 		group = '     '
   2065 		if is_this_task_a_group(tid):
   2066 			group = 'GROUP'
   2067 		if is_linked(tid):
   2068 			group = ' LINK'
   2069 		l_l[0]       = '%s %s - %s'%(tid,group,title)
   2070 		l            = ':'.join(l_l)
   2071 		r.append(l)
   2072 
   2073 	return r
   2074 
   2075 ## search string in group
   2076 # @param[in] group task id
   2077 # @param[in] search query string
   2078 # @ingroup EDI_CORE
   2079 def search_string_in_group(group,search):
   2080 
   2081 	tasks  = sorted(os.listdir(generate_group_path(group)))
   2082 
   2083 	# r lists all hits
   2084 	r      = []
   2085 	for orderidtid in tasks:
   2086 		# tid for current task in group
   2087 		tid         = orderidtid[ORDER_ID_LENGTH:]
   2088 		# search in visible tasks only (filter enable status)
   2089 		task_status = get_status(tid)
   2090 		grep_r      = []
   2091 		if status_filters_d[task_status] == STATUS_FILTER_STATES[0]:
   2092 			f           = os.popen('grep -i -R "%s" %s/description.txt'%(search,generate_task_path(tid)))
   2093 			# add tid and filename to results
   2094 			grep_r      = ['/%s/description.txt:%s'%(tid,i) for i in f.readlines()]
   2095 			f.close()
   2096 
   2097 		for l in grep_r:
   2098 			# replace filename with tid and title (first line in file)
   2099 			# read first line in file
   2100 			l_l          = l.split(':')
   2101 			hit_filename = l_l[0]
   2102 			f            = open('%s%s'%(data_location_tasks,hit_filename))
   2103 			title        = f.readline().strip()
   2104 			f.close()
   2105 			# set tid
   2106 			tid          = hit_filename.split('/')[1]
   2107 			group = '     '
   2108 			if is_this_task_a_group(tid):
   2109 				group = 'GROUP'
   2110 			if is_linked(tid):
   2111 				group = ' LINK'
   2112 			l_l[0]       = '%s %s - %s'%(tid,group,title)
   2113 			l            = ':'.join(l_l)
   2114 			r.append(l)
   2115 
   2116 	return r
   2117 
   2118 ## search string in tree
   2119 # @param[in] group task id
   2120 # @param[in] search query string
   2121 # @ingroup EDI_CORE
   2122 def search_string_in_tree(group,search):
   2123 	# walk_group is the list of groups to visit. FIFO
   2124 	r          = []
   2125 	walk_group = [group]
   2126 	# the while loop goes through all the group that are found
   2127 	while walk_group:
   2128 		# list items in first group
   2129 		tasks      = sorted(os.listdir(generate_group_path(walk_group[0])))
   2130 		r += search_string_in_group(walk_group[0],search)
   2131 
   2132 		# add group found in first group
   2133 		for t in tasks:
   2134 			# Check tasks that are not title task in a group
   2135 			if (t[ORDER_ID_LENGTH:] != walk_group[0]) and is_this_task_a_group(t[ORDER_ID_LENGTH:]):
   2136 				walk_group.append(t[ORDER_ID_LENGTH:])
   2137 
   2138 		# remove first group to list items in next group
   2139 		del walk_group[0]
   2140 	return r
   2141 
   2142 ## show tree
   2143 #  @ingroup EDI_CORE
   2144 # print all trees: group tids and titles
   2145 def show_tree():
   2146 	r       = []
   2147 	f       = os.popen('cd %s;find .'%data_location_tree)
   2148 	# remove first empty line
   2149 	tidtree = f.readlines() [1:]
   2150 	f.close()
   2151 
   2152 	# remove './' from path
   2153 	tidtree = [i[2:] for i in tidtree]
   2154 
   2155 	# print titles path\n group tid path\n\n
   2156 	for l in tidtree:
   2157 		# find title for group tids
   2158 		group_titles_in_path = []
   2159 		for g in l.strip().split(os.sep):
   2160 			group_titles_in_path.append(get_task_title(g))
   2161 		if user_interface == 'web':
   2162 			# convert / to - to be able to create links correctly
   2163 			group_titles_in_path = [i.replace('/', '-') for i in group_titles_in_path]
   2164 		# create string title/title...
   2165 		group_titles_in_path_s = '/'.join(group_titles_in_path)
   2166 		r.append('%s\n'%group_titles_in_path_s)
   2167 		r.append('%s\n'%l)
   2168 
   2169 	return r
   2170 
   2171 ## group statistics
   2172 #  @ingroup EDI_CORE
   2173 # print statistics for a group recursively
   2174 def group_statistics(group):
   2175 	global stats
   2176 	global stats_total
   2177 	global stats_creation_dates
   2178 	global stats_overtime
   2179 
   2180 	# compute total number of tasks in group
   2181 	path = generate_group_path(group)
   2182 
   2183 	tasks         = []
   2184 	# remove group title
   2185 	for i in os.listdir(path):
   2186 		if not baseconvert(0) in i[:ORDER_ID_LENGTH]:
   2187 			tasks.append(i[ORDER_ID_LENGTH:])
   2188 
   2189 	stats_total   += len(tasks)
   2190 
   2191 	# compute number of tasks in each state, groups and links
   2192 	for tid in tasks:
   2193 		task_status        = get_status(tid)
   2194 		stats[task_status] += 1
   2195 		if is_this_task_a_group(tid):
   2196 			stats['   Group'] += 1
   2197 			group_statistics(tid)
   2198 		if is_linked(tid):
   2199 			stats['  Linked'] += 1
   2200 
   2201 		#stats_creation_dates
   2202 		# Figure out creation time and modification time for description
   2203 		task_ctime         = get_creation_date(tid)[1]
   2204 
   2205 		task_path         = generate_task_path(tid)
   2206 		(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/description.txt'%task_path)
   2207 		# 'if' below to be compatible with first database format
   2208 		if task_ctime == 0:
   2209 			# ctime not available
   2210 			task_ctime = mtime
   2211 		stats_creation_dates.append(task_ctime)
   2212 
   2213 		(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/status'%task_path)
   2214 
   2215 		cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime))
   2216 		sdate = time.strftime("%Y-%m-%d", time.localtime(mtime))
   2217 		if not stats_overtime.has_key(cdate):
   2218 			# initialize date dict
   2219 			stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))]))
   2220 		stats_overtime[cdate]['Creation'] += 1
   2221 		if not stats_overtime.has_key(sdate):
   2222 			# initialize date dict
   2223 			stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))]))
   2224 		stats_overtime[sdate][task_status] += 1
   2225 		#end
   2226 
   2227 ## statistics
   2228 #  @ingroup EDI_CORE
   2229 # print statistics for a group or a database
   2230 # compute speed
   2231 def statistics(group):
   2232 	global stats
   2233 	global stats_total
   2234 	global stats_creation_dates
   2235 	global stats_overtime
   2236 	r = []
   2237 
   2238 	# initialize stats dictionary
   2239 	stat_keys     = [TASK_STATUS[i] for i in range(len(TASK_STATUS))]
   2240 	stat_keys.append('   Group')
   2241 	stat_keys.append('  Linked')
   2242 	state_amounts = [0 for i in range(len(stat_keys))]
   2243 	stats         = dict(zip(stat_keys,state_amounts))
   2244 
   2245 	if group == 'for database':
   2246 		# compute total number of tasks, excluding root
   2247 		path          = data_location_tasks
   2248 		tasks         = []
   2249 		for i in os.listdir(path):
   2250 			if i != 'root':
   2251 				tasks.append(i)
   2252 
   2253 		# compute number of tasks in each state, groups and links
   2254 		for tid in tasks:
   2255 			task_status        = get_status(tid)
   2256 			stats[task_status] += 1
   2257 			if is_this_task_a_group(tid):
   2258 				stats['   Group'] += 1
   2259 			if is_linked(tid):
   2260 				stats['  Linked'] += 1
   2261 
   2262 #:define stats_creation_dates
   2263 			# Figure out creation time and modification time for description
   2264 			task_ctime         = get_creation_date(tid)[1]
   2265 
   2266 			task_path         = generate_task_path(tid)
   2267 			(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/description.txt'%task_path)
   2268 			if task_ctime == 0:
   2269 				# ctime not available
   2270 				task_ctime = mtime
   2271 			stats_creation_dates.append(task_ctime)
   2272 
   2273 			(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/status'%task_path)
   2274 
   2275 			cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime))
   2276 			sdate = time.strftime("%Y-%m-%d", time.localtime(mtime))
   2277 			if not stats_overtime.has_key(cdate):
   2278 				# initialize date dict
   2279 				stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))]))
   2280 			stats_overtime[cdate]['Creation'] += 1
   2281 			if not stats_overtime.has_key(sdate):
   2282 				# initialize date dict
   2283 				stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))]))
   2284 			stats_overtime[sdate][task_status] += 1
   2285 #:end
   2286 
   2287 		stats_total   = len(tasks)
   2288 	else:
   2289 		# compute total number of tasks in group
   2290 		path = generate_group_path(group)
   2291 
   2292 		tasks         = []
   2293 		for i in os.listdir(path):
   2294 			if group == 'root':
   2295 				tasks.append(i[ORDER_ID_LENGTH:])
   2296 			else:
   2297 				# the group task is not counted in the statistics
   2298 				if not baseconvert(0) in i[:ORDER_ID_LENGTH]:
   2299 					tasks.append(i[ORDER_ID_LENGTH:])
   2300 
   2301 		stats_total   = len(tasks)
   2302 
   2303 		# compute number of tasks in each state, groups and links, recursively
   2304 		for tid in tasks:
   2305 			task_status        = get_status(tid)
   2306 			stats[task_status] += 1
   2307 			if is_this_task_a_group(tid):
   2308 				stats['   Group'] += 1
   2309 				group_statistics(tid)
   2310 			if is_linked(tid):
   2311 				stats['  Linked'] += 1
   2312 
   2313 #:stats_creation_dates
   2314 			# Figure out creation time and modification time for description
   2315 			task_ctime         = get_creation_date(tid)[1]
   2316 
   2317 			task_path         = generate_task_path(tid)
   2318 			(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/description.txt'%task_path)
   2319 			if task_ctime == 0:
   2320 				# ctime not available
   2321 				task_ctime = mtime
   2322 			stats_creation_dates.append(task_ctime)
   2323 
   2324 			(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat('%s/status'%task_path)
   2325 
   2326 			cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime))
   2327 			sdate = time.strftime("%Y-%m-%d", time.localtime(mtime))
   2328 			if not stats_overtime.has_key(cdate):
   2329 				# initialize date dict
   2330 				stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))]))
   2331 			stats_overtime[cdate]['Creation'] += 1
   2332 			if not stats_overtime.has_key(sdate):
   2333 				# initialize date dict
   2334 				stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))]))
   2335 			stats_overtime[sdate][task_status] += 1
   2336 
   2337 	if not stats_total:
   2338 		r.append('0 task in statistics.')
   2339 		return r
   2340 
   2341 	# csv format, not used for now
   2342 	#col_names = ['Tasks'] + sorted(stats.keys())
   2343 	#col_names = [i strip for i in col_names]
   2344 	#print ','.join(col_names)
   2345 	#csv = '%d'%len(tasks)
   2346 	#for k in sorted(stats.keys())
   2347 	#	csv += ',%d'%stats[k]
   2348 	#r append csv
   2349 	#r append ''
   2350 
   2351 	r.append('Number of items: %d\n'%stats_total)
   2352 	for k in sorted(stats.keys()):
   2353 		r.append('%s - %d'%(k, stats[k]))
   2354 	r.append('')
   2355 
   2356 	# speed
   2357 	start_time        = sorted(stats_creation_dates)[0]
   2358 	now               = time.time()
   2359 	done_and_inactive = stats[TASK_STATUS[TASK_STATUS_DONE]] + stats[TASK_STATUS[TASK_STATUS_INACTIVE]]
   2360 	if not done_and_inactive:
   2361 		r.append('Nothing is done or inactive')
   2362 	else:
   2363 		# subtract tasks in state void, because tasks in void state are informative or groups
   2364 		number_of_tasks   = stats_total - stats[TASK_STATUS[TASK_STATUS_VOID]]
   2365 		remaining_tasks   = number_of_tasks - done_and_inactive
   2366 		remaining_time    = (now - start_time) / float(done_and_inactive) * remaining_tasks
   2367 		r.append('Number of tasks (excluding voids): %d'%number_of_tasks)
   2368 		r.append('Remaining tasks:                   %d'%remaining_tasks)
   2369 		r.append('Remaining days:                    %.3f'%(remaining_time/86400))
   2370 		r.append('Start date:                        %s'%time.strftime("%Y-%m-%d", time.localtime(start_time)))
   2371 		r.append("Today's date:                      %s"%time.strftime("%Y-%m-%d", time.localtime(now)))
   2372 		r.append('Finish date:                       %s'%time.strftime("%Y-%m-%d", time.localtime(now + remaining_time)))
   2373 
   2374 	# create csv stats from stats_overtime
   2375 	f = open('%s/stats.csv'%data_location,'w')
   2376 	f.write('date,%s\n'%','.join([i.strip() for i in STATS_OVERTIME_KEYS]))
   2377 
   2378 	for d in sorted(stats_overtime.keys()):
   2379 		f.write('%s,%s\n'%(d, ','.join([str(stats_overtime[d][i]) for i in STATS_OVERTIME_KEYS])))
   2380 	f.close()
   2381 
   2382 	# create creation, done and inactive stats
   2383 	f = open('%s/stats_creation_done_inactive.csv'%data_location,'w')
   2384 	f.write('date,Creation,Done/Inactive\n')
   2385 
   2386 	for d in sorted(stats_overtime.keys()):
   2387 		f.write('%s,%d,%d\n'%(d, stats_overtime[d]['Creation'], stats_overtime[d][TASK_STATUS[TASK_STATUS_DONE]] + stats_overtime[d][TASK_STATUS[TASK_STATUS_INACTIVE]]))
   2388 	f.close()
   2389 
   2390 	r.append('')
   2391 	return r
   2392 
   2393 # test core functions
   2394 #def test
   2395 #	# test
   2396 #	list_group('root')
   2397 #
   2398 #	t1 = add_task('root', 'task1.txt')
   2399 #	add_task('root', 'task2.txt')
   2400 #	g3 = add_task('root', 'task3.txt')
   2401 #
   2402 #	create_group(g3)
   2403 #	t4 = add_task(g3, 'task4.txt')
   2404 #
   2405 #	list_group('root')
   2406 #	list_group(g3)
   2407 #
   2408 #	#display_task(t1)
   2409 #	#display_task(t4)
   2410 #
   2411 #	# Delete task in a group of 2 tasks
   2412 #	print '# Delete task in a group of 2 tasks'
   2413 #	delete_task(t4)
   2414 #
   2415 #	list_group('root')
   2416 #
   2417 #	create_group(g3)
   2418 #	t4 = add_task(g3, 'task4.txt')
   2419 #
   2420 #	list_group('root')
   2421 #
   2422 #	# Delete group first task
   2423 #	print '# Delete group first task'
   2424 #	delete_task(g3)
   2425 #
   2426 #	list_group('root')
   2427 #
   2428 #	# Delete group task of a group with more than 2 tasks
   2429 #	print '# Delete group task of a group with more than 2 tasks'
   2430 #	delete_task(t4)
   2431 #
   2432 #	g3 = add_task('root', 'task3.txt')
   2433 #	create_group(g3)
   2434 #	t4 = add_task(g3, 'task4.txt')
   2435 #	t2 = add_task(g3, 'task2.txt')
   2436 #
   2437 #	list_group('root')
   2438 #	list_group(g3)
   2439 #	new_group_id = delete_task(g3)
   2440 #	print 'New group id %s'%new_group_id
   2441 #
   2442 #	list_group('root')
   2443 #	list_group(new_group_id)
   2444 #
   2445 #	# Delete group
   2446 #	print '# Delete group'
   2447 #	delete_group(new_group_id)
   2448 #
   2449 #	list_group('root')
   2450 #
   2451 #	print baseconvert(100)
   2452 #	print baseconvert_to_dec(baseconvert(100))
   2453 #	print
   2454 #
   2455 #	#edit_task(t1)
   2456 #
   2457 #	# Change order
   2458 #	print '# Change order'
   2459 #	g3 = add_task('root', 'task3.txt')
   2460 #	change_task_order('root',1,0)
   2461 #	change_task_order('root',0,2)
   2462 #
   2463 #	list_group('root')
   2464 #
   2465 #	# Change status
   2466 #	print '# Change status'
   2467 #	create_group(g3)
   2468 #	t4 = add_task(g3, 'task4.txt')
   2469 #	t2 = add_task(g3, 'task2.txt')
   2470 #
   2471 #	list_group('root')
   2472 #
   2473 #	set_status(t1,TASK_STATUS_DONE)
   2474 #	set_status(t4,TASK_STATUS_DONE)
   2475 #
   2476 #	list_group('root')
   2477 #	list_group(g3)
   2478 #
   2479 #	# Reset status
   2480 #
   2481 #	reset_group_status(g3)
   2482 #
   2483 #	list_group('root')
   2484 #	list_group(g3)
   2485 #
   2486 #	reset_group_status('root')
   2487 #
   2488 #	list_group('root')
   2489 #	list_group(g3)
   2490 #
   2491 #	# Create a group in group
   2492 #	print '# Create a group in group'
   2493 #	create_group(t4)
   2494 #	t2 = add_task(t4,'task2.txt')
   2495 #
   2496 #	list_group('root')
   2497 #	list_group(g3)
   2498 #	list_group(t4)
   2499 #
   2500 #	# Delete task in a group of 2 tasks not in root
   2501 #	print '# Delete task in a group of 2 tasks not in root'
   2502 #	delete_task(t2)
   2503 #
   2504 #	list_group('root')
   2505 #	list_group(g3)
   2506 #
   2507 #	# Delete group with groups in it
   2508 #	print '# Delete group with groups in it'
   2509 #	create_group(t4)
   2510 #	t2 = add_task(t4,'task2.txt')
   2511 #
   2512 #	list_group('root')
   2513 #	list_group(g3)
   2514 #	list_group(t4)
   2515 #
   2516 #	delete_group(g3)
   2517 #
   2518 #	list_group('root')
   2519 #
   2520 #	# Search string
   2521 #	print '# Search string'
   2522 #
   2523 #	search_string('AND')
   2524 #
   2525 #	# List tree
   2526 #	print '# List tree'
   2527 #	g3 = add_task('root', 'task3.txt')
   2528 #	create_group(g3)
   2529 #	t4 = add_task(g3, 'task4.txt')
   2530 #	t2 = add_task(g3, 'task2.txt')
   2531 #	create_group(t4)
   2532 #	t2 = add_task(t4,'task2.txt')
   2533 #
   2534 #	list_tree('root')
   2535 #
   2536 #	# Show group for a task
   2537 #	print '# Show group for a task'
   2538 #
   2539 #	print 'Show group for %s - %s' %(t2,get_task_title(t2))
   2540 #	show_group_for_task(t2)
   2541 #	print 'Show group for %s - %s' %(t4,get_task_title(t4))
   2542 #	show_group_for_task(t4)
   2543 #	print 'Show group for %s - %s' %(t1,get_task_title(t1))
   2544 #	show_group_for_task(t1)
   2545 
   2546 	# create task
   2547 	#create_task(t4)
   2548 	#list_group(t4)
   2549 
   2550 ## start - always called at startup.
   2551 # @ingroup EDI_CORE
   2552 # @param[in] interface selects current user interface. cli loads the configuration from user home, web loads the configuration located in edi_web folder.
   2553 # creates default .easydoneit.ini<br>
   2554 # creates database folders<br>
   2555 # loads .easydoneit.ini
   2556 def start(interface='cli'):
   2557 	global user_interface
   2558 	user_interface = interface
   2559 
   2560 	if interface=='web':
   2561 		# Do not try to access ~/.easydoneit.ini
   2562 		inipath = '.easydoneit.ini'
   2563 	else:
   2564 		inipath = '~/.easydoneit.ini'
   2565 		# create ~/.easydoneit.ini if it doesnt exist, (not in web interface mode)
   2566 		if not os.path.isfile(os.path.expanduser('~/.easydoneit.ini')):
   2567 			f = open(os.path.expanduser('~/.easydoneit.ini'),'w')
   2568 			f.write('[data]\n')
   2569 			f.write('location=~/easydoneit_data\n')
   2570 			f.write('1=~/easydoneit_data\n')
   2571 			f.write('\n')
   2572 			f.write('[locations]\n')
   2573 			f.write('selected=1\n')
   2574 			f.write('default_add_in=1\n')
   2575 			f.write('\n')
   2576 			f.write('[filters]\n')
   2577 			# enable all status filters
   2578 			fi = zip(TASK_STATUS,status_filters)
   2579 			for name,nfilter in fi:
   2580 				# strip to remove spaces in status strings
   2581 				f.write("%s=%s\n"%(name.strip(),nfilter))
   2582 			f.write('\n')
   2583 			f.write('[colors]\n')
   2584 			co = zip(TASK_STATUS,status_fgColors)
   2585 			for name,color in co:
   2586 				f.write('%s_fgColor=%d,%d,%d,%d\n' % (name.strip(),color[0],color[1],color[2],color[3]))
   2587 			co = zip(TASK_STATUS,status_bgColors)
   2588 			for name,color in co:
   2589 				f.write('%s_bgColor=%d,%d,%d,%d\n' % (name.strip(),color[0],color[1],color[2],color[3]))
   2590 
   2591 			f.write('\n[settings]\n')
   2592 			f.write('editor=vi\n')
   2593 
   2594 			f.close()
   2595 			# default permissions 600 rw for user no access for group and world
   2596 			os.chmod(os.path.expanduser('~/.easydoneit.ini'),stat.S_IRUSR|stat.S_IWUSR)
   2597 
   2598 	# load config from inipath
   2599 	config              = ConfigParser.ConfigParser()
   2600 	config.readfp(open(os.path.expanduser(inipath)))
   2601 
   2602 	# convert ~ to home path
   2603 	global data_location
   2604 	data_location       = os.path.expanduser(config.get('data','location'))
   2605 
   2606 	# load available databases
   2607 	global databases
   2608 	data_section        = config.items('data')
   2609 	# remove location which is a path to a database
   2610 	DATABASE_NAME = 0
   2611 	DATABASE_PATH = 1
   2612 	# clear databases for reentrant testing
   2613 	databases = []
   2614 	for d in data_section:
   2615 		if d[DATABASE_NAME] != 'location':
   2616 			databases.append(d)
   2617 
   2618 	# load add_top_or_bottom
   2619 	if 'add_top_or_bottom' in dict(config.items('locations')).keys():
   2620 		global add_top_or_bottom
   2621 		add_top_or_bottom = config.get('locations','add_top_or_bottom')
   2622 
   2623 	# load selected database paths
   2624 	global selected
   2625 	global default_add_in
   2626 	global selected_path
   2627 	selected            = config.get('locations','selected').split(',')
   2628 	default_add_in      = config.get('locations','default_add_in')
   2629 	for d in selected:
   2630 		selected_path.append(os.path.expanduser(config.get('data',d)))
   2631 
   2632 	# load autolink groups
   2633 	if 'autolink' in dict(config.items('locations')).keys():
   2634 		global autolink
   2635 		autolink_cfg        = config.get('locations','autolink')
   2636 		autolink            = [ autolink_cfg[i:i+ID_LENGTH] for i in range(0,len(autolink_cfg), ID_LENGTH+1)]
   2637 
   2638 	# load list groups
   2639 	if 'list' in dict(config.items('locations')).keys():
   2640 		global list_of_groups
   2641 		list_cfg        = config.get('locations','list')
   2642 		list_of_groups  = [ list_cfg[i:i+ID_LENGTH] for i in range(0,len(list_cfg), ID_LENGTH+1)]
   2643 
   2644 	# load status filters and status colors
   2645 	config_filter_names = dict(config.items('filters')).keys()
   2646 	config_color_names  = dict(config.items('colors')).keys()
   2647 	for n,nfilter in enumerate(TASK_STATUS):
   2648 		# strip to remove spaces in status strings
   2649 		# check that task_status filter is in config file, to avoid problems between config file versions
   2650 		if nfilter.strip().lower() in config_filter_names:
   2651 			status_filters[n]  = config.get('filters',nfilter.strip())
   2652 		if '%s_fgcolor'%nfilter.strip().lower() in config_color_names:
   2653 			status_fgColors[n] = tuple([int(i) for i in config.get('colors','%s_fgcolor'%nfilter.strip()).split(',')])
   2654 		if '%s_bgcolor'%nfilter.strip().lower() in config_color_names:
   2655 			status_bgColors[n] = tuple([int(i) for i in config.get('colors','%s_bgcolor'%nfilter.strip()).split(',')])
   2656 
   2657 	# Set text editor
   2658 	global editor
   2659 	editor              = config.get('settings','editor')
   2660 
   2661 	# set user name and email
   2662 	global user
   2663 	if 'username' in dict(config.items('settings')).keys():
   2664 		user  = config.get('settings','username')
   2665 	else:
   2666 		user  = getpass.getuser()
   2667 	if 'useremail' in dict(config.items('settings')).keys():
   2668 		global email
   2669 		email = config.get('settings','useremail')
   2670 
   2671 	# init
   2672 	init()
   2673