385 lines
17 KiB
Python
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,...) |