ctdo-trac/TracRendezVous/tracrendezvous/rendezvous/workflow.py

385 lines
17 KiB
Python

# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from trac.core import Component, implements
#from trac.perm import PermissionSystem
from trac.env import IEnvironmentSetupParticipant
from trac.config import Configuration
from trac.util.datefmt import utc
from trac.util.compat import set
from trac.util.translation import _
from genshi.builder import tag
from model import RendezVousDate, RendezVous, RendezVousType, RendezVousLocation
from tracrendezvous.location.model import ItemLocation
from tracrendezvous.event.model import Event
from api import RendezVousSystem, IRendezVousActionController
from ctdotools.utils import gen_wiki_page
# -- Utilities for the RendezVous workflow
__all__ = ['RendezVousWorkflow',]
def parse_workflow_config(rawactions):
"""Given a list of options from [rendezvous-workflow]"""
actions = {}
for option, value in rawactions:
parts = option.split('.')
action = parts[0]
if action not in actions:
actions[action] = {}
if len(parts) == 1:
# Base name, of the syntax: old,states,here -> newstate
try:
oldstates, newstate = [x.strip() for x in value.split('->')]
except ValueError:
raise Exception('Bad option "%s"' % (option, )) # 500, no _
actions[action]['newstate'] = newstate
actions[action]['oldstates'] = oldstates
else:
action, attribute = option.split('.')
actions[action][attribute] = value
# Fill in the defaults for every action, and normalize them to the desired
# types
for action, attributes in actions.items():
# Default the 'name' attribute to the name used in the ini file
if 'name' not in attributes:
attributes['name'] = action
# If not specified, an action is not the default.
if 'default' not in attributes:
attributes['default'] = 0
else:
attributes['default'] = int(attributes['default'])
if 'icon' not in attributes:
attributes['icon'] = ""
else:
attributes['icon'] = unicode(attributes['icon'])
# If operations are not specified, that means no operations
if 'operations' not in attributes:
attributes['operations'] = []
else:
attributes['operations'] = [a.strip() for a in
attributes['operations'].split(',')]
# If no permissions are specified, then no permissions are needed
if 'permissions' not in attributes:
attributes['permissions'] = []
else:
attributes['permissions'] = [a.strip() for a in
attributes['permissions'].split(',')]
# Normalize the oldstates
attributes['oldstates'] = [x.strip() for x in
attributes['oldstates'].split(',')]
return actions
def get_workflow_config(config):
"""Usually passed self.config, this will return the parsed rendezvous-workflow
section.
"""
raw_actions = list(config.options('rendezvous-workflow'))
actions = parse_workflow_config(raw_actions)
return actions
def load_workflow_config_snippet(config, filename):
"""Loads the rendezvous-workflow section from the given file (expected to be in
the 'workflows' tree) into the provided config.
"""
from pkg_resources import resource_filename
filename = resource_filename(__name__,
'workflows/%s' % filename)
new_config = Configuration(filename)
for name, value in new_config.options('rendezvous-workflow'):
config.set('rendezvous-workflow', name, value)
class RendezVousWorkflow(Component):
def __init__(self, *args, **kwargs):
Component.__init__(self, *args, **kwargs)
self.actions = get_workflow_config(self.config)
if not '_reset' in self.actions:
# Special action that gets enabled if the current status no longer
# exists, as no other action can then change its state. (#5307)
self.actions['_reset'] = {
'default': 0,
'name': 'reset',
'newstate': 'new',
'icon': 'new.png',
'oldstates': [], # Will not be invoked unless needed
'operations': ['reset_workflow'],
'permissions': []}
self.log.debug('Workflow actions at initialization: %s\n' %
str(self.actions))
implements(IRendezVousActionController, IEnvironmentSetupParticipant)
# IEnvironmentSetupParticipant methods
def environment_created(self):
"""When an environment is created, we provide the basic-workflow,
unless a rendezvous-workflow section already exists.
"""
if not 'rendezvous-workflow' in self.config.sections():
load_workflow_config_snippet(self.config, 'rendezvous_workflow.ini')
self.config.save()
self.actions = get_workflow_config(self.config)
def environment_needs_upgrade(self, db):
"""The environment needs an upgrade if there is no [rendezvous-workflow]
section in the config.
"""
return not list(self.config.options('rendezvous-workflow'))
def upgrade_environment(self, db):
"""Insert a [rendezvous-workflow] section using the original-workflow"""
load_workflow_config_snippet(self.config, 'rendezvous_workflow.ini')
self.config.save()
self.actions = get_workflow_config(self.config)
def get_rendezvous_actions(self, req, rendezvous):
"""Returns a list of (weight, action) tuples that are valid for this
request and this rendezvous."""
# Get the list of actions that can be performed
# Determine the current status of this rendezvous. If this rendezvous is in
# the process of being modified, we need to base our information on the
# pre-modified state so that we don't try to do two (or more!) steps at
# once and get really confused.
status = rendezvous.status
allowed_actions = []
for action_name, action_info in self.actions.items():
oldstates = action_info['oldstates']
if oldstates == ['*'] or status in oldstates:
# This action is valid in this state. Check permissions.
allowed = 0
required_perms = action_info['permissions']
if required_perms:
for permission in required_perms:
if permission in req.perm:
allowed = 1
break
else:
allowed = 1
if allowed:
allowed_actions.append((action_info['default'],
action_name))
if not (status in ['new', 'closed'] or \
status in RendezVousSystem(self.env).get_all_status()):
# State no longer exists - add a 'reset' action if admin.
allowed_actions.append((0, '_reset'))
return allowed_actions
def get_all_status(self):
"""Return a list of all states described by the configuration.
"""
all_status = set()
for action_name, action_info in self.actions.items():
all_status.update(action_info['oldstates'])
all_status.add(action_info['newstate'])
all_status.discard('*')
return all_status
def get_status_position(self):
"""Return a list of all states described by the configuration.
"""
def pred(a,b):
if a[1] < b[1]:
return -1
elif a[1] > b[1]:
return 1
return 0
all_status = list()
for action_name, action_info in self.actions.items():
if action_info.has_key("position"):
all_status.append((action_info['newstate'], int(action_info['position']), bool(int(action_info["show"]))))
return sorted(all_status, pred)
def render_rendezvous_action_control(self, req, rendezvous, action):
self.log.debug('render_ticket_action_control: action "%s"' % action)
this_action = self.actions[action]
status = this_action['newstate']
operations = this_action['operations']
control = [] # default to nothing
hints = []
if 'reset_workflow' in operations:
control.append(tag("from invalid state "))
hints.append(_("Current state no longer exists"))
if 'revote_rendezvous' in operations:
hints.append(_("The rendezvous will be prepared for a new voting pass. All comments, dates and votes for that item will be deleted. You have to publish it again to become visible for other users"))
if 'publish_rendezvous' in operations:
hints.append(_("The rendezvous will be published for voting"))
if 'schedule_rendezvous' in operations:
dates = RendezVousDate.fetch_by_rendezvous(self.env, rendezvous.rendezvous_id)
control.append(tag([_("to "), tag.select(
[tag.option("%s" % x.time_begin.strftime('%Y.%m.%d %H:%M'), value=x.date_id)
for x in dates],
id="elected", name="elected")]))
hints.append(_("The rendezvous will be scheduled (no more voting, but remains visible)"))
if 'start_rendezvous' in operations:
hints.append(_("This status may be ignored, since I can handle RendezVous starts automatically, but you may wish to start manually before the scheduled beginning date and time"))
if 'expire_rendezvous' in operations:
hints.append(_("This status may be ignored, since I can handle expired RendezVouses automatically, but you may wish to stop manually before the scheduled date and time. It's an end status"))
if 'cancel_rendezvous' in operations:
hints.append(_("The rendezvous will not takes place. It's an end status"))
if 'leave_status' in operations:
control.append('as %s ' % rendezvous.status)
else:
if status != '*':
hints.append(_("Next status will be '%s'") % status)
return (this_action['name'], tag(*control), '. '.join(hints))
def _change_rendezvous(self, operation, rendezvous, args, req):
if operation == 'reset_workflow':
rendezvous.status = 'new'
elif operation == 'revote_rendezvous':
#RendezVousDate.delete_by_rendezvous(self.env, rendezvous.rendezvous_id)
rendezvous.elected = 0
rendezvous.update()
elif operation == 'schedule_rendezvous':
if args.has_key("elected"):
date_id = int(args["elected"])
# TODO: making all this into a transaction based commit
try:
now = datetime.now(utc)
date = rendezvous.get_date(date_id)
date.elected = True
date.update()
rendezvous.elected = date_id
dt = RendezVousDate.fetch_one(self.env, rendezvous.elected)
location = RendezVousLocation.fetch_one(self.env, rendezvous.location_id)
event = Event(self.env, 0, rendezvous.name, req.authname, now, now, dt.time_begin, dt.time_end, rendezvous.location_id, tags=rendezvous.tags, attendees=" ".join([vote.user for vote in dt.votes]))
event.commit()
event.wikipage = "events/%d" % event.e_id
event.update()
try:
gen_wiki_page(self.env, req.authname, event.wikipage, rendezvous.description and u" = %s =\n[events:%d]\n\n%s" % (event.name, event.srv_id, rendezvous.description) or u" = %s =\n[events:%d]\n\n%s" % (event.name, event.e_id, u"Diese Page kann mit Ideen und Inhalten des Events/Treffs gefüllt werden"), req.remote_addr)
except Exception:
pass
RendezVous.delete(self.env, rendezvous.rendezvous_id)
req.redirect(req.href.event("edit", event.e_id))
except ValueError, e:
self.env.log.warning("%s with date_id '%d'" % (str(e), date_id))
def change_rendezvous_workflow(self, req, rendezvous, action):
this_action = self.actions[action]
# Status changes
status = this_action['newstate']
if status != '*':
rendezvous.status = status
for operation in this_action['operations']:
self._change_rendezvous(operation, rendezvous, req.args, req)
def process_expired(self, r_list):
"""we have to check if scheduled rendezvouses are passed, and change their status to closed"""
for rendezvous in r_list:
if rendezvous.elected:
n = datetime.now(utc)
d = rendezvous.get_date(rendezvous.elected)
if d.time_end < datetime.now(utc):
self.env.log.debug("RendezVous #%d expired!" % rendezvous.rendezvous_id)
rendezvous.status = 'expired'
rendezvous.update()
if d.time_begin < n < d.time_end:
rendezvous.status = 'running'
rendezvous.update()
def scheduled_rendezvouses(self, r_list=None, check=False):
"""Called without r_list argument fetches the result from db.
If check=True, the intermediate result will be checked for expired (past) elected date
and finally cleaned."""
icon = self.actions["schedule"]["icon"]
if not r_list:
res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'scheduled')
if check:
self.process_expired(res)
for r in res:
if r.status == 'scheduled':
r.icon = icon
yield r
else:
for r in res:
r.icon = icon
yield r
else:
if check:
self.process_expired(r_list)
for i in r_list:
if i.status == 'scheduled':
i.icon = icon
yield i
def canceled_rendezvouses(self, r_list=None):
icon = self.actions["cancel"]["icon"]
if not r_list:
res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'canceled')
for r in res:
r.icon = icon
yield r
else:
for i in r_list:
if i.status == "canceled":
i.icon = icon
yield i
def voting_rendezvouses(self, r_list=None):
icon = self.actions["publish"]["icon"]
if not r_list:
res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'voting')
for r in res:
r.icon = icon
yield r
else:
for i in r_list:
if i.status == "voting":
i.icon = icon
yield i
def running_rendezvouses(self, r_list=None):
icon = self.actions["start"]["icon"]
if not r_list:
res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'running')
for r in res:
r.icon = icon
yield r
else:
for i in r_list:
if i.status == "running":
i.icon = icon
yield i
def expired_rendezvouses(self, r_list=None):
icon = self.actions["expire"]["icon"]
if not r_list:
res = RendezVous._fetch_some(self.env, True, "SELECT * FROM rendezvous WHERE status=%s;", 'expired')
for r in res:
r.icon = icon
yield r
else:
for i in r_list:
if i.status == "expired":
i.icon = icon
yield i
def get_icon(self, status):
for i in self.actions.values():
if i["newstate"] == status:
return i.get("icon", None)
return None
def get_icons(self):
return dict([(i["newstate"], i.get("icon", None))
for i in self.actions.values()])
#def check_for_timer_event(self):
#for rendezvous in current_rendezvous():
#timers = RendezVousTimers().fetch_by_rendezvous(rendezvous.rendezvous_id)
#now = datetime.now(utc)
#for timer in timers:
#if timer.time < now:
#_change_rendevous(timer.operation, rendezvous,...)