# -*- 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,...)