Index: multipleworkflow/web_ui.py
===================================================================
--- multipleworkflow/web_ui.py	(revision 16300)
+++ multipleworkflow/web_ui.py	(working copy)
@@ -21,7 +21,7 @@
 from trac.ticket.model import Type
 from genshi.builder import tag
 from genshi.output import HTMLSerializer
-from workflow import get_workflow_config_default, get_workflow_config_by_type, parse_workflow_config
+from workflow import get_workflow_config_by_type, parse_workflow_config
 
 
 def get_workflow_actions_for_error():
@@ -87,20 +87,17 @@
             t = "New custom workflow (not saved)"
         if not actions:
             # We should never end here...
-            actions = get_workflow_config_default(self.config)
+            actions = get_workflow_config_by_type(self.config, 'default')
             t = "Custom workflow is broken. Showing default workflow"
     else:
         t = u""
-        if name == 'default':
-            actions = get_workflow_config_default(self.config)
-        else:
-            actions = get_workflow_config_by_type(self.config, name)
+        actions = get_workflow_config_by_type(self.config, name)
 
     states = list(set(
         [state for action in actions.itervalues()
          for state in action['oldstates']] + [action['newstate'] for action in actions.itervalues()]))
 
-    action_labels = [action_info['name'] for action_name, action_info in actions.items()]
+    action_labels = [action_info['label'] for action_name, action_info in actions.items()]
     action_names = actions.keys()
 
     edges = []
Index: multipleworkflow/workflow.py
===================================================================
--- multipleworkflow/workflow.py	(revision 16300)
+++ multipleworkflow/workflow.py	(working copy)
@@ -6,17 +6,23 @@
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution.
 
+from functools import partial
 from genshi.builder import tag
+from genshi.filters.transform import Transformer
 
 from trac.core import TracError, implements
-from trac.perm import PermissionSystem
+from trac.perm import PermissionCache, PermissionSystem
+from trac.resource import ResourceNotFound
 from trac.ticket import model
 from trac.ticket.default_workflow import ConfigurableTicketWorkflow, parse_workflow_config
 from trac.ticket.api import ITicketActionController, TicketSystem
-from trac.ticket.model import Resolution
+from trac.ticket.model import Component as TicketComponent, Resolution
+from trac.util import get_reporter_id, sub_val
 from trac.util.compat import set
+from trac.util.presentation import separated
 from trac.util.text import obfuscate_email_address
 from trac.util.translation import _, tag_
+from trac.web.api import IRequestFilter, ITemplateStreamFilter
 from trac.web.chrome import Chrome
 
 
@@ -23,15 +29,25 @@
 def get_workflow_config_by_type(config, tipo_ticket):
     """return the [ticket-workflow-type] session"""
     raw_actions = list(config.options('ticket-workflow-%s' % tipo_ticket))
+    if not raw_actions:
+        raw_actions = list(config.options('ticket-workflow'))
     actions = parse_workflow_config(raw_actions)
-    if actions and '_reset' not in actions:
-        actions['_reset'] = {
-            'default': 0,
-            'name': 'reset',
-            'newstate': 'new',
-            'oldstates': [],  # Will not be invoked unless needed
-            'operations': ['reset_workflow'],
-            'permissions': []}
+    reset = {
+        'default': 0,
+        'label': 'reset',
+        'newstate': 'new',
+        'oldstates': [],
+        'operations': ['reset_workflow'],
+        'permissions': ['TICKET_ADMIN']
+    }
+    for key, val in reset.items():
+        actions['_reset'].setdefault(key, val)
+    for name, info in actions.iteritems():
+        for val in ('<none>', '< none >'):
+            sub_val(actions[name]['oldstates'], val, None)
+        if not info['newstate']:
+            self.log.warning("Ticket workflow action '%s' doesn't define "
+                             "any transitions", name)    
     return actions
 
 
@@ -46,6 +62,7 @@
         all_status.add(action_info['newstate'])
     all_status.discard('*')
     all_status.discard('')
+    all_status.discard(None)
     return all_status
 
 
@@ -113,7 +130,7 @@
     verify.permissions = TICKET_MODIFY
     }}}
     """
-    implements(ITicketActionController)
+    implements(ITicketActionController, ITemplateStreamFilter, IRequestFilter)
 
     def __init__(self):
         # This call creates self.actions
@@ -124,6 +141,10 @@
             actions = get_workflow_config_by_type(self.config, t)
             if actions:
                 self.type_actions[t] = actions
+        # set default workflow actions
+        actions = get_workflow_config_by_type(self.config, 'default')
+        if actions:
+            self.type_actions['default'] = actions
 
     def get_workflow_actions_by_type(self, tkt_type):
         """Return the ticket actions defined by the workflow for the given ticket type or {}."""
@@ -133,6 +154,56 @@
             actions = {}
         return actions
 
+    # IRequestFilter methods
+    def pre_process_request(self, req, handler):
+        return handler
+            
+    def post_process_request(self, req, template, data, content_type):
+        """Implements the special behaviour for requests with 'refresh' argument should provide
+        the proper list of available actions."""
+        mine = ['/newticket', '/ticket', '/simpleticket']
+
+        match = False
+        for target in mine:
+            if req.path_info.startswith(target):
+                match = True
+                break
+
+        if match:
+            if 'refresh' in req.args:
+                template = 'ticket_workflow.html'
+
+        return template, data, content_type
+        
+    # ITemplateStreamFilter methods
+    def filter_stream(self, req, method, filename, stream, data):
+        """Returns a transformed stream extended with action refresh functionality."""
+        # The jQuery code responsible for refreshing the list of actions will be injected after
+        # certain points (matched based on regex) in the original ticket template
+        
+        # Note: the refreshing should preserve the previous selection if applicable
+        actionRefreshScript = '''\n
+        $("#field-type").autoSubmit({refresh: '1'}, function(data, reply) {
+            var previouslyCheckedAction = $("#action input[name='action']:checked");
+            var items = $(reply);
+            var actions = items.filter('#action');
+            $("#action").replaceWith(actions);
+            var actionToClick = $("#action input[id='" + previouslyCheckedAction.attr('id') + "']");
+            if (actionToClick.length != 0)
+            {
+                actionToClick.click();
+            }
+            else
+                $("#action input[name='action']:first").click();
+        }, "#action .trac-loading");'''
+        # Applying changes only on ticket.html
+        if filename == 'ticket.html':
+            stream = stream | Transformer('.//script').substitute('#ticket \.trac-loading\"\);', \
+            '''#ticket .trac-loading");''' + actionRefreshScript)
+            stream = stream | Transformer('.//script').substitute('#changelog \.trac-loading\"\);', \
+            '''#changelog .trac-loading");''' + actionRefreshScript)
+        return stream
+        
     # ITicketActionController methods
 
     def get_ticket_actions(self, req, ticket):
@@ -144,10 +215,11 @@
         # 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 = ticket._old.get('status', ticket['status']) or 'new'
+        status = ticket._old.get('status', ticket['status'])
+        exists = status is not None
 
         # Calculate actions for ticket type. If no wtype workflow exists use the default workflow.
-        tipo_ticket = ticket._old.get('type', ticket['type'])
+        tipo_ticket = ticket['type']
         actions = self.get_workflow_actions_by_type(tipo_ticket)
         if not actions:
             actions = self.actions
@@ -156,26 +228,34 @@
         allowed_actions = []
         for action_name, action_info in actions.items():
             oldstates = action_info['oldstates']
-            if oldstates == ['*'] or status in oldstates:
+            if exists and oldstates == ['*'] or status in oldstates:
                 # This action is valid in this state.  Check permissions.
                 required_perms = action_info['permissions']
                 if self._is_action_allowed(ticket_perm, required_perms):
                     allowed_actions.append((action_info['default'],
                                             action_name))
-        if not (status in ['new', 'closed'] or
-                status in TicketSystem(self.env).get_all_status()) \
-                and 'TICKET_ADMIN' in ticket_perm:
-            # State no longer exists - add a 'reset' action if admin.
-            allowed_actions.append((0, '_reset'))
-
+        # Append special `_reset` action if status is invalid.
+        reset = actions['_reset']
+        if exists and status not in TicketSystem(self.env).get_all_status():            
+            required_perms = reset['permissions']
+            if self._is_action_allowed(ticket_perm, required_perms):
+                allowed_actions.append((reset['default'], '_reset'))
         # Check if the state is valid for the current ticket type. If not offer the action to reset it.
         type_status = self.get_all_status_for_type(tipo_ticket)
         if not type_status:
-            type_status = calc_status(self.actions)
-        if status not in type_status and (0, '_reset') not in allowed_actions:
-                allowed_actions.append((0, '_reset'))
+            type_status = calc_status(actions)
+        if status not in type_status and exists and (reset['default'], '_reset') not in allowed_actions:
+                allowed_actions.append((reset['default'], '_reset'))
         return allowed_actions
 
+    def _is_action_allowed(self, ticket_perm, required_perms):
+        if not required_perms:
+            return True
+        for permission in required_perms:
+            if permission not in ticket_perm:
+                return False
+        return True
+        
     def get_all_status_for_type(self, t_type):
         actions = self.get_workflow_actions_by_type(t_type)
         return calc_status(actions)
@@ -195,7 +275,7 @@
 
         self.log.debug('render_ticket_action_control: action "%s"' % action)
 
-        tipo_ticket = ticket._old.get('type', ticket['type'])
+        tipo_ticket = ticket['type']
         actions = self.get_workflow_actions_by_type(tipo_ticket)
         if not actions:
             actions = self.actions
@@ -203,108 +283,157 @@
         this_action = actions[action]
         status = this_action['newstate']
         operations = this_action['operations']
-        current_owner = ticket._old.get('owner', ticket['owner'] or '(none)')
-        if not (Chrome(self.env).show_email_addresses
-                or 'EMAIL_VIEW' in req.perm(ticket.resource)):
-            format_user = obfuscate_email_address
-        else:
-            format_user = lambda address: address
-        current_owner = format_user(current_owner)
+        current_owner = ticket._old.get('owner', ticket['owner'])
+        author = get_reporter_id(req, 'author')
+        author_info = partial(Chrome(self.env).authorinfo, req,
+                              resource=ticket.resource)
+        format_author = partial(Chrome(self.env).format_author, req,
+                                resource=ticket.resource)
+                                                 
+        formatted_current_owner = author_info(current_owner)
+        exists = ticket._old.get('status', ticket['status']) is not None
 
         control = []  # default to nothing
         hints = []
         if 'reset_workflow' in operations:
-            control.append(tag("from invalid state "))
+            control.append(_("from invalid state"))
             hints.append(_("Current state no longer exists"))
         if 'del_owner' in operations:
             hints.append(_("The ticket will be disowned"))
-        if 'set_owner' in operations:
-            id = 'action_%s_reassign_owner' % action
-            selected_owner = req.args.get(id, req.authname)
-
+        if 'set_owner' in operations or 'may_set_owner' in operations:
             if 'set_owner' in this_action:
-                owners = [x.strip() for x in
-                          this_action['set_owner'].split(',')]
+                owners = self._to_users(this_action['set_owner'], ticket)
             elif self.config.getbool('ticket', 'restrict_owner'):
                 perm = PermissionSystem(self.env)
                 owners = perm.get_users_with_permission('TICKET_MODIFY')
-                owners.sort()
+                owners = [user for user in owners
+                               if 'TICKET_MODIFY'
+                               in PermissionCache(self.env, user,
+                                                  ticket.resource)]
+                owners = sorted(owners)
             else:
                 owners = None
+            if 'set_owner' in operations:
+                default_owner = author
+            elif 'may_set_owner' in operations:
+                if not exists:
+                    default_owner = TicketSystem(self.env).default_owner
+                else:
+                    default_owner = ticket._old.get('owner',
+                                                    ticket['owner'] or None)
+                if owners is not None and default_owner not in owners:
+                    owners.insert(0, default_owner)
+            else:
+                # Protect against future modification for case that another
+                # operation is added to the outer conditional
+                raise AssertionError(operations)
 
-            if owners is None:
-                owner = req.args.get(id, req.authname)
-                control.append(tag_('to %(owner)s',
-                                    owner=tag.input(type='text', id=id,
-                                                    name=id, value=owner)))
-                hints.append(_("The owner will be changed from "
-                               "%(current_owner)s",
-                               current_owner=current_owner))
+            id = 'action_%s_reassign_owner' % action
+
+            if not owners:
+                owner = req.args.get(id, default_owner)
+                control.append(
+                    tag_("to %(owner)s",
+                         owner=tag.input(type='text', id=id, name=id,
+                                         value=owner)))
+                if not exists or current_owner is None:
+                    hints.append(_("The owner will be the specified user"))
+                else:
+                    hints.append(tag_("The owner will be changed from "
+                                      "%(current_owner)s to the specified "
+                                      "user",
+                                      current_owner=formatted_current_owner))
             elif len(owners) == 1:
                 owner = tag.input(type='hidden', id=id, name=id,
                                   value=owners[0])
-                formatted_owner = format_user(owners[0])
-                control.append(tag_('to %(owner)s ',
-                                    owner=tag(formatted_owner, owner)))
-                if ticket['owner'] != owners[0]:
-                    hints.append(_("The owner will be changed from "
-                                   "%(current_owner)s to %(selected_owner)s",
-                                   current_owner=current_owner,
-                                   selected_owner=formatted_owner))
+                formatted_new_owner = author_info(owners[0])
+                control.append(tag_("to %(owner)s",
+                                    owner=tag(formatted_new_owner, owner)))
+                if not exists or current_owner is None:
+                    hints.append(tag_("The owner will be %(new_owner)s",
+                                      new_owner=formatted_new_owner))
+                elif ticket['owner'] != owners[0]:
+                    hints.append(tag_("The owner will be changed from "
+                                      "%(current_owner)s to %(new_owner)s",
+                                      current_owner=formatted_current_owner,
+                                      new_owner=formatted_new_owner))
             else:
-                control.append(tag_('to %(owner)s', owner=tag.select(
-                    [tag.option(x, value=x,
-                                selected=(x == selected_owner or None))
-                     for x in owners],
+                selected_owner = req.args.get(id, default_owner)
+                control.append(tag_("to %(owner)s", owner=tag.select(
+                    [tag.option(label, value=value if value is not None else '',
+                                selected=(value == selected_owner or None))
+                     for label, value in sorted((format_author(owner), owner)
+                                                for owner in owners)],
                     id=id, name=id)))
-                hints.append(_("The owner will be changed from "
-                               "%(current_owner)s",
-                               current_owner=current_owner))
-        if 'set_owner_to_self' in operations and \
-                ticket._old.get('owner', ticket['owner']) != req.authname:
-            hints.append(_("The owner will be changed from %(current_owner)s "
-                           "to %(authname)s", current_owner=current_owner,
-                           authname=req.authname))
+                if not exists or current_owner is None:
+                    hints.append(_("The owner will be the selected user"))
+                else:
+                    hints.append(tag_("The owner will be changed from "
+                                      "%(current_owner)s to the selected user",
+                                      current_owner=formatted_current_owner))
+        elif 'set_owner_to_self' in operations and \
+                ticket._old.get('owner', ticket['owner']) != author:
+            formatted_author = author_info(author)
+            if not exists or current_owner is None:
+                hints.append(tag_("The owner will be %(new_owner)s",
+                                  new_owner=formatted_author))
+            else:
+                hints.append(tag_("The owner will be changed from "
+                                  "%(current_owner)s to %(new_owner)s",
+                                  current_owner=formatted_current_owner,
+                                  new_owner=formatted_author))
         if 'set_resolution' in operations:
             if 'set_resolution' in this_action:
-                resolutions = [x.strip() for x in
-                               this_action['set_resolution'].split(',')]
+                                                 
+                resolutions = this_action['set_resolution']
             else:
-                resolutions = [val.name for val in Resolution.select(self.env)]
+                resolutions = [r.name for r in Resolution.select(self.env)]
             if not resolutions:
                 raise TracError(_("Your workflow attempts to set a resolution "
                                   "but none is defined (configuration issue, "
                                   "please contact your Trac admin)."))
-            id_ = 'action_%s_resolve_resolution' % action
+            id = 'action_%s_resolve_resolution' % action
             if len(resolutions) == 1:
-                resolution = tag.input(type='hidden', id=id_, name=id_,
+                resolution = tag.input(type='hidden', id=id, name=id,
                                        value=resolutions[0])
-                control.append(tag_('as %(resolution)s',
+                control.append(tag_("as %(resolution)s",
                                     resolution=tag(resolutions[0],
                                                    resolution)))
-                hints.append(_("The resolution will be set to %(name)s",
-                               name=resolutions[0]))
+                hints.append(tag_("The resolution will be set to %(name)s",
+                                  name=resolutions[0]))
             else:
-                selected_option = req.args.get(id_, TicketSystem(self.env).default_resolution)
-                control.append(tag_('as %(resolution)s',
+                selected_option = req.args.get(id,
+                        TicketSystem(self.env).default_resolution)
+                control.append(tag_("as %(resolution)s",
                                     resolution=tag.select(
-                                    [tag.option(x, value=x, selected=(x == selected_option or None))
-                                     for x in resolutions],
-                                     id=id_, name=id_)))
+                    [tag.option(x, value=x,
+                                selected=(x == selected_option or None))
+                     for x in resolutions],
+                    id=id, name=id)))
                 hints.append(_("The resolution will be set"))
         if 'del_resolution' in operations:
             hints.append(_("The resolution will be deleted"))
         if 'leave_status' in operations:
-            control.append(_('as %(status)s ',
-                             status=ticket._old.get('status',
-                                                    ticket['status'])))
+            control.append(tag_("as %(status)s",
+                                status=ticket._old.get('status',
+                                                       ticket['status'])))
+            if len(operations) == 1:
+                hints.append(tag_("The owner will remain %(current_owner)s",
+                                  current_owner=formatted_current_owner)
+                             if current_owner else
+                             _("The ticket will remain with no owner"))
         else:
-            if status != '*':
-                hints.append(_("Next status will be '%(name)s'", name=status))
-        return this_action['name'], tag(*control), '. '.join(hints)
+            if ticket['status'] is None:
+                hints.append(tag_("The status will be '%(name)s'",
+                                  name=status))
+            elif status != '*':
+                hints.append(tag_("Next status will be '%(name)s'",
+                                  name=status))
+        return (this_action['label'], tag(separated(control, ' ')),
+                tag(separated(hints, '. ', '.') if hints else ''))
 
     def get_ticket_changes(self, req, ticket, action):
-        tipo_ticket = ticket._old.get('type', ticket['type'])
+        tipo_ticket = ticket['type']
         actions = self.get_workflow_actions_by_type(tipo_ticket)
         if not actions:
             actions = self.actions
@@ -323,29 +452,60 @@
             updated['status'] = status
 
         for operation in this_action['operations']:
-            if operation == 'reset_workflow':
-                updated['status'] = 'new'
-            elif operation == 'del_owner':
+            if operation == 'del_owner':
+                                         
+                                          
                 updated['owner'] = ''
-            elif operation == 'set_owner':
+            elif operation in ('set_owner', 'may_set_owner'):
+                set_owner = this_action.get('set_owner')
                 newowner = req.args.get('action_%s_reassign_owner' % action,
-                                        this_action.get('set_owner', '').strip())
+                                        set_owner[0] if set_owner else '')
                 # If there was already an owner, we get a list, [new, old],
                 # but if there wasn't we just get new.
                 if type(newowner) == list:
                     newowner = newowner[0]
-                updated['owner'] = newowner
+                updated['owner'] = self._sub_owner_keyword(newowner, ticket)
             elif operation == 'set_owner_to_self':
-                updated['owner'] = req.authname
+                updated['owner'] = get_reporter_id(req, 'author')
             elif operation == 'del_resolution':
                 updated['resolution'] = ''
             elif operation == 'set_resolution':
-                newresolution = req.args.get('action_%s_resolve_resolution' %
-                                             action,
-                                             this_action.get('set_resolution', '').strip())
+                set_resolution = this_action.get('set_resolution')
+                newresolution = req.args.get('action_%s_resolve_resolution'
+                                             % action,
+                                             set_resolution[0]
+                                             if set_resolution else '')
                 updated['resolution'] = newresolution
 
+            # reset_workflow is just a no-op here, so we don't look for it.
             # leave_status is just a no-op here, so we don't look for it.
+
+        # Set owner to component owner for 'new' ticket if:
+        #  - ticket doesn't exist and owner is < default >
+        #  - component is changed
+        #  - owner isn't explicitly changed
+        #  - ticket owner is equal to owner of previous component
+        #  - new component has an owner
+        if not ticket.exists and 'owner' not in updated:
+            updated['owner'] = self._sub_owner_keyword(ticket['owner'], ticket)
+        elif ticket['status'] == 'new' and \
+                'component' in ticket.values and \
+                'component' in ticket._old and \
+                'owner' not in updated:
+            try:
+                old_comp = TicketComponent(self.env, ticket._old['component'])
+            except ResourceNotFound:
+                # If the old component has been removed from the database
+                # we just leave the owner as is.
+                pass
+            else:
+                old_owner = old_comp.owner or ''
+                current_owner = ticket['owner'] or ''
+                if old_owner == current_owner:
+                    new_comp = TicketComponent(self.env, ticket['component'])
+                    if new_comp.owner:
+                        updated['owner'] = new_comp.owner
+
         return updated
 
     # Public methods (for other ITicketActionControllers that want to use
@@ -381,8 +541,10 @@
 
         # Be sure to look at the original status.
         status = ticket._old.get('status', ticket['status'])
-        actions = [(info['default'], action) for action, info in actions.items()
-                   if operation in info['operations'] and
-                   ('*' in info['oldstates'] or status in info['oldstates']) and
-                   self._has_perms_for_action(req, info, ticket.resource)]
+        actions = [(info['default'], action)
+                    for action, info in actions.items()
+                        if operation in info['operations'] and
+                            ('*' in info['oldstates'] or
+                            status in info['oldstates']) and
+                            self._has_perms_for_action(req, info, ticket.resource)]
         return actions
