Пример #1
0
def _create_requirement_tree(env):
    t_manager = AgiloTicketModelManager(env)
    for req_nr in range(number_of_requirements):
        stories = []
        for story_nr in range(number_of_stories_per_requirement):
            tasks = []
            for task_nr in range(number_of_tasks_per_story):
                task = dict()
                task['t_type'] = Type.TASK
                task[Key.SUMMARY] = 'Task %d.%d.%d' % (req_nr, story_nr, task_nr)
                task[Key.SPRINT] = sprint_name
                t = t_manager.create(**task)
                tasks.append(t)
            story = dict()
            story['t_type'] = Type.USER_STORY
            story[Key.SUMMARY] = 'Story %d.%d' % (req_nr, story_nr)
            story[Key.SPRINT] = sprint_name
            s = t_manager.create(**story)
            for t in tasks:
                s.link_to(t)
            stories.append(s)
        requirement = dict()
        requirement['t_type'] = Type.REQUIREMENT
        requirement[Key.SUMMARY] = 'Requirement %d' % (req_nr)
        r = t_manager.create(**requirement)
        for s in stories:
            r.link_to(s)
    return number_of_requirements * number_of_stories_per_requirement * number_of_tasks_per_story
Пример #2
0
    def __init__(self, env, a_commit_message):
        self.env = env
        self.message = a_commit_message
        # REFACT: rename tm to ticket_model_manager to make the type clearer
        self.tm = AgiloTicketModelManager(self.env)

        self.validation_errors = list()
        self.commands = list()
Пример #3
0
def _create_orphan_tasks(env):
    t_manager = AgiloTicketModelManager(env)
    for task_nr in range(number_of_tasks_per_bug):
        task = dict()
        task['t_type'] = Type.TASK
        task[Key.SUMMARY] = 'Orphan Task %d' % (task_nr)
        task[Key.SPRINT] = sprint_name
        t_manager.create(**task)
    return number_of_tasks_per_bug
Пример #4
0
 def ticket_as_json(self, req, ticket_or_ticket_id):
     if isinstance(ticket_or_ticket_id, AgiloTicket):
         ticket = ticket_or_ticket_id
     else:
         # Use the model manager that uses the request cache
         ticket = AgiloTicketModelManager(self.env).get(tkt_id=ticket_or_ticket_id)
     ticket_json = ticket.as_dict()
     ticket_json['can_edit'] = self.can_edit_ticket(req, ticket)
     return ticket_json
Пример #5
0
 def ticket_as_json(self, req, ticket_or_ticket_id):
     if isinstance(ticket_or_ticket_id, AgiloTicket):
         ticket = ticket_or_ticket_id
     else:
         # Use the model manager that uses the request cache
         ticket = AgiloTicketModelManager(
             self.env).get(tkt_id=ticket_or_ticket_id)
     ticket_json = ticket.as_dict()
     ticket_json['can_edit'] = self.can_edit_ticket(req, ticket)
     return ticket_json
Пример #6
0
def create_emtpy_sprint(env):
    assert sprint_name != None, 'Please configure a sprint name.'
    sprint = SprintModelManager(env).create(name=sprint_name,
                                                  start=now(),
                                                  duration=14)
    assert sprint is not None
    t_manager = AgiloTicketModelManager(env)
    for t_id, t_type, t_status in sprint._fetch_tickets():
        ticket = t_manager.create(tkt_id=t_id, t_type=t_type)
        if ticket[Key.SPRINT] == sprint.name:
            ticket[Key.SPRINT] = None
            t_manager.save(ticket, None, 'reset sprint field for performance measurement')
Пример #7
0
 def _add_links_for_ticket(self, req, ticket):
     try:
         src_ticket_id = int(req.args['src'])
     except:
         pass
     else:
         src_ticket = AgiloTicketModelManager(self.env).get(tkt_id=src_ticket_id)
         if ticket != None and src_ticket != None:
             if src_ticket.is_link_to_allowed(ticket):
                 return src_ticket.link_to(ticket)
             else:
                 msg = 'You may not link #%d (Type %s) to #%d (Type %s)'
                 error(self, msg % (src_ticket.id, src_ticket.get_type(),
                                    ticket.id, ticket.get_type()))
Пример #8
0
 def _add_links_for_ticket(self, req, ticket):
     try:
         src_ticket_id = int(req.args['src'])
     except:
         pass
     else:
         src_ticket = AgiloTicketModelManager(
             self.env).get(tkt_id=src_ticket_id)
         if ticket != None and src_ticket != None:
             if src_ticket.is_link_to_allowed(ticket):
                 return src_ticket.link_to(ticket)
             else:
                 msg = 'You may not link #%d (Type %s) to #%d (Type %s)'
                 error(
                     self,
                     msg % (src_ticket.id, src_ticket.get_type(), ticket.id,
                            ticket.get_type()))
Пример #9
0
 def __init__(self,env, a_commit_message):
     self.env = env
     self.message = a_commit_message
     # REFACT: rename tm to ticket_model_manager to make the type clearer
     self.tm = AgiloTicketModelManager(self.env)
     
     self.validation_errors = list()
     self.commands = list()
Пример #10
0
class CommitMessageCommandParser(object):
    def __init__(self, env, a_commit_message):
        self.env = env
        self.message = a_commit_message
        # REFACT: rename tm to ticket_model_manager to make the type clearer
        self.tm = AgiloTicketModelManager(self.env)

        self.validation_errors = list()
        self.commands = list()

    def validate_and_parse_commit_message(self):
        if '' == self.message:
            self.abort_with_usage("Please provide a comment")

        # Now get all the commands and tickets out of the log and check whether are valid or not
        #print "Message: %s" % repr(self.message)
        #print 'regex:', COMMAND_AND_TICKET_PATTERN
        for match in COMMAND_AND_TICKET_REGEX.finditer(self.message):
            command = match.group('command')
            #print "Expression matching: <%s> <%s> group(0) <%s>" % (match.group('command'), match.group('ticket'), match.group(0))
            for ticket_id, remaining_time in TICKET_REGEX.findall(
                    match.group('ticket')):
                #print 'command', command, 'ticket_id', ticket_id, 'remaining_time', remaining_time
                # Check that the ticket is existing first
                ticket = self.validate_and_get_ticket(ticket_id)

                if not ticket:
                    continue

                if self.does_command_require_remaining_time(command):
                    self.validate_is_remaining_time_set(
                        command, remaining_time)
                    self.validate_includes_configured_unit_in_remaining_time(
                        remaining_time)
                    self.validate_can_change_remaining_time_on_ticket(
                        ticket, remaining_time)
                else:
                    self.validate_no_remaining_time_set(
                        command, remaining_time)

                self.commands.append((command, ticket_id, remaining_time))
        if len(self.validation_errors) > 0:
            #print >> sys.stderr, "\n".join(self.validation_errors)
            raise InvalidAttributeError("\n".join(self.validation_errors))

        return self.commands

    def validate_and_get_ticket(self, ticket_id):
        try:
            t_id = int(ticket_id.replace('#', ''))
            ticket = self.tm.get(tkt_id=t_id, load=True)
            return ticket
        except Exception, e:
            self.validation_errors.append(
                "Unable to verify ticket #%s, reason: '%s'" %
                (ticket_id, to_unicode(e)))
            return None
Пример #11
0
def _create_bug_trees(env):
    t_manager = AgiloTicketModelManager(env)
    for bug_nr in range(number_of_bugs):
        tasks = []
        for task_nr in range(number_of_tasks_per_bug):
            task = dict()
            task['t_type'] = Type.TASK
            task[Key.SUMMARY] = 'Bug Task %d.%d' % (bug_nr, task_nr)
            task[Key.SPRINT] = sprint_name
            t = t_manager.create(**task)
            tasks.append(t)
        bug = dict()
        bug['t_type'] = Type.BUG
        bug[Key.SUMMARY] = 'Bug %d' % (bug_nr)
        bug[Key.SPRINT] = sprint_name
        b = t_manager.create(**bug)
        for t in tasks:
            b.link_to(t)
    return number_of_bugs * number_of_tasks_per_bug
Пример #12
0
 def __init__(self, project, rev, repo, env=None):
     """Initialize the class with the project path and the revision"""
     try:
         self.env = env or Environment(project)
         self.tm = AgiloTicketModelManager(self.env)
         self.repo = repo
         repos = self.env.get_repository(self.repo)
         repos.sync()
     except Exception, e:
         print >> sys.stderr, "An Error occurred while opening Trac project: %s => %s" % (project, to_unicode(e))
         sys.exit(1)
Пример #13
0
 def __init__(self):
     self.action_map = {
         Action.ATTACHMENT_CREATE: self.check_attachment_create,
         Action.CONFIRM_COMMITMENT: self.confirm_commitment,
         Action.TICKET_EDIT: self.check_ticket_edit,
         # We need also to catch trac's Actions for modifying tickets 
         # - otherwise too many things will be allowed!
         Action.TICKET_CHANGE: self.check_ticket_edit,
         Action.TICKET_MODIFY: self.check_ticket_edit,
         Action.TICKET_EDIT_PAGE_ACCESS: self.check_ticket_edit_page_access,
         Action.TICKET_EDIT_DESCRIPTION: self.check_edit_description,
         
         # We don't have add a check for TICKET_APPEND because all three 
         # roles get the TICKET_APPEND permission via meta permissions. 
         # Therefore checking for team members is useless.
         # Action.TICKET_APPEND: self.check_ticket_append,
         Action.LINK_EDIT: self.check_link_edit,
         Action.SAVE_REMAINING_TIME: self.check_save_remaining_time,
         Action.BACKLOG_EDIT: self.check_backlog_edit,
     }
     self.tm = AgiloTicketModelManager(self.env)
Пример #14
0
 def validate(self, value):
     from agilo.ticket.model import AgiloTicket, \
         AgiloTicketModelManager
     from agilo.scrum.backlog import Backlog
     if isinstance(value, (AgiloTicket, Backlog.BacklogItem)):
         return value
     value = self.super()
     try:
         value = AgiloTicketModelManager(self.env).get(tkt_id=value)
     except Exception:
         self.message = "must be a valid AgiloTicket id"
         self.error(value)
     return value
Пример #15
0
class CommitMessageCommandParser(object):
    
    def __init__(self,env, a_commit_message):
        self.env = env
        self.message = a_commit_message
        # REFACT: rename tm to ticket_model_manager to make the type clearer
        self.tm = AgiloTicketModelManager(self.env)
        
        self.validation_errors = list()
        self.commands = list()
    
    def validate_and_parse_commit_message(self):
        if '' == self.message:
            self.abort_with_usage("Please provide a comment")
        
        # Now get all the commands and tickets out of the log and check whether are valid or not
        #print "Message: %s" % repr(self.message)
        #print 'regex:', COMMAND_AND_TICKET_PATTERN
        for match in COMMAND_AND_TICKET_REGEX.finditer(self.message):
            command = match.group('command')
            #print "Expression matching: <%s> <%s> group(0) <%s>" % (match.group('command'), match.group('ticket'), match.group(0))
            for ticket_id, remaining_time in TICKET_REGEX.findall(match.group('ticket')):
                #print 'command', command, 'ticket_id', ticket_id, 'remaining_time', remaining_time
                # Check that the ticket is existing first
                ticket = self.validate_and_get_ticket(ticket_id)
                
                if not ticket:
                    continue
                
                if self.does_command_require_remaining_time(command):
                    self.validate_is_remaining_time_set(command, remaining_time)
                    self.validate_includes_configured_unit_in_remaining_time(remaining_time)
                    self.validate_can_change_remaining_time_on_ticket(ticket, remaining_time)
                else:
                    self.validate_no_remaining_time_set(command, remaining_time)
                
                self.commands.append((command, ticket_id, remaining_time))
        if len(self.validation_errors) > 0:
            #print >> sys.stderr, "\n".join(self.validation_errors)
            raise InvalidAttributeError("\n".join(self.validation_errors))
        
        return self.commands
    
    def validate_and_get_ticket(self, ticket_id):
        try:
            t_id = int(ticket_id.replace('#', ''))
            ticket = self.tm.get(tkt_id=t_id, load=True)
            return ticket
        except Exception, e:
            self.validation_errors.append("Unable to verify ticket #%s, reason: '%s'" % (ticket_id, to_unicode(e)))
            return None
Пример #16
0
 def testTicketWithModelManager(self):
     """Tests the ticket creation via ModelManager"""
     from agilo.ticket.model import AgiloTicketModelManager
     manager = AgiloTicketModelManager(self.env)
     t1 = manager.create(summary='This is a test ticket', 
                               t_type=Type.TASK,
                               remaining_time='12',
                               description='take this')
     self.assert_true(t1.exists)
     self.assertNotEqual(0, t1.id)
     self.assert_equals('This is a test ticket', t1[Key.SUMMARY])
     self.assert_equals('12', t1[Key.REMAINING_TIME])
     self.assert_equals('take this', t1[Key.DESCRIPTION])
     t2 = manager.get(tkt_id=t1.id)
     self.assert_true(t2.exists)
     self.assert_equals(t1.id, t2.id)
     # test cache too
     self.assert_equals(id(t1), id(t2))
     # Now change the summary
     t2.summary = 'A new summary'
     manager.save(t2, author='tester', comment='Updated summary...')
     # is the same object so...
     self.assert_equals(t2.summary, t1.summary)
Пример #17
0
    def get_tickets_matching(self, t_id, summary):
        """
        Returns a list of dictionaries (id: value, summary: value) matching the summary 
        request and excluding the requesting ticket having id = id.
        """
        try:
            t_id = int(t_id)  # Make sure it is an int :-)
            keyword = re.compile(summary, re.IGNORECASE)
            db = self.env.get_db_cnx()

            from agilo.ticket.model import AgiloTicketModelManager
            sql = """SELECT id, type, summary FROM ticket WHERE id != $id $allowed 
                  AND id NOT IN (SELECT dest FROM %s WHERE src = $id UNION
                  SELECT src FROM %s WHERE dest = $id) ORDER BY summary""" \
                    % (LINKS_TABLE, LINKS_TABLE)
            sql_query = string.Template(sql)
            sql_allowed = "AND ticket.type IN ('%s')"
            t_type = AgiloTicketModelManager(
                self.env).get(tkt_id=t_id).get_type()
            linkconfig = LinksConfiguration(self.env)
            if linkconfig.is_allowed_source_type(t_type):
                allowed_types = linkconfig.get_allowed_destination_types(
                    t_type)
                allowed = sql_allowed % '\', \''.join(allowed_types)
            else:
                debug(self, "No Key found for #%s#" % repr(t_type))
                allowed = ''

            sql_query = sql_query.substitute({'id': t_id, 'allowed': allowed})
            debug(self, "SQL: %s" % sql_query)
            cursor = db.cursor()
            cursor.execute(sql_query)

            results = []
            for row in cursor:
                if keyword.search(row[2] or ''):
                    results.append({
                        'id': row[0],
                        'type': row[1],
                        'summary': row[2]
                    })

            debug(self, "Search Results: %s" % str(results))
            return results

        except Exception, e:
            warning(self, e)
            msg = "[%s]: ERROR: Search module unable to complete query!" % \
                    self.__class__.__name__
            raise TracError(msg)
Пример #18
0
        def _execute(self, team_controller, date_converter=None,
                     as_key=None):
            """Runs the command, returns the result or raise a CommandError"""
            from agilo.ticket.model import AgiloTicketModelManager 
            ticket_manager = AgiloTicketModelManager(team_controller.env)
            # ask to agilo config which types have the story points
            # attribute
            from agilo.utils.config import AgiloConfig
            ac = AgiloConfig(team_controller.env)
            types = []
            for t_type, fields in ac.TYPES.items():
                if Key.STORY_POINTS in fields:
                    types.append(t_type)
            # choose the right operator
            types_condition = "in ('%s')" % "', '".join(types)
            if len(types) == 1:
                types_condition = types[0] # just equals that type

            stories = ticket_manager.select(criteria={'type': types_condition,
                                                      'sprint': self.sprint.name})
            if not self.estimated:
                stories = [s for s in stories if s[Key.STATUS] == Status.CLOSED]
            # Sum up the story points of all the stories
            return sum([float(s[Key.STORY_POINTS] or 0) for s in stories])
Пример #19
0
 def __init__(self):
     self.action_map = {
         Action.ATTACHMENT_CREATE: self.check_attachment_create,
         Action.CONFIRM_COMMITMENT: self.confirm_commitment,
         Action.TICKET_EDIT: self.check_ticket_edit,
         # We need also to catch trac's Actions for modifying tickets
         # - otherwise too many things will be allowed!
         Action.TICKET_CHANGE: self.check_ticket_edit,
         Action.TICKET_MODIFY: self.check_ticket_edit,
         Action.TICKET_EDIT_PAGE_ACCESS: self.check_ticket_edit_page_access,
         Action.TICKET_EDIT_DESCRIPTION: self.check_edit_description,
         # We don't have add a check for TICKET_APPEND because all three
         # roles get the TICKET_APPEND permission via meta permissions.
         # Therefore checking for team members is useless.
         # Action.TICKET_APPEND: self.check_ticket_append,
         Action.LINK_EDIT: self.check_link_edit,
         Action.SAVE_REMAINING_TIME: self.check_save_remaining_time,
         Action.BACKLOG_EDIT: self.check_backlog_edit,
     }
     self.tm = AgiloTicketModelManager(self.env)
Пример #20
0
 def testTicketWithModelManager(self):
     """Tests the ticket creation via ModelManager"""
     from agilo.ticket.model import AgiloTicketModelManager
     manager = AgiloTicketModelManager(self.env)
     t1 = manager.create(summary='This is a test ticket',
                         t_type=Type.TASK,
                         remaining_time='12',
                         description='take this')
     self.assert_true(t1.exists)
     self.assertNotEqual(0, t1.id)
     self.assert_equals('This is a test ticket', t1[Key.SUMMARY])
     self.assert_equals('12', t1[Key.REMAINING_TIME])
     self.assert_equals('take this', t1[Key.DESCRIPTION])
     t2 = manager.get(tkt_id=t1.id)
     self.assert_true(t2.exists)
     self.assert_equals(t1.id, t2.id)
     # test cache too
     self.assert_equals(id(t1), id(t2))
     # Now change the summary
     t2.summary = 'A new summary'
     manager.save(t2, author='tester', comment='Updated summary...')
     # is the same object so...
     self.assert_equals(t2.summary, t1.summary)
Пример #21
0
 def process_request(self, req):
     # Check if it has been called with 'src' and 'dest' arguments or abort
     if not req.args.has_key('src') or not req.args.has_key('dest') or not req.args.has_key('cmd'):
         raise TracError("Links should be called with 'cmd', 'src' and 'dest' parameters",
                         "Links: Source and/or Destination are Missing!")
     else:
         # Flag for ticket update
         update_ticket = False
         # Avoid recursive imports
         from agilo.ticket.model import AgiloTicketModelManager
         tm = AgiloTicketModelManager(self.env)
         try:
             src = int(req.args.get('src'))
             # Now that we have the ticket we can check permission to link or edit
             ticket_perm = req.perm('ticket', src)
             if Action.TICKET_EDIT not in ticket_perm or \
                     Action.LINK_EDIT not in ticket_perm:
                 raise TracError("You (%s) are not authorized to edit this ticket" % \
                                 req.authname)
             dest = int(req.args.get('dest'))
             cmd = req.args.get('cmd')
             url = req.args.get('url_orig') or None
         except:
             raise TracError("Source is not valid: src=%s" % req.args.get('src'))
         # Create the LinkEndPoint for the source and destination if not existing
         sle = tm.get(tkt_id=src)
         dle = tm.get(tkt_id=dest)
         src_type = sle.get_type()
         dest_type = dle.get_type()
         
         if cmd == 'create link':
             if not sle.link_to(dle):
                 raise TracError("Links not allowed between %s->%s! The types are incompatible or" \
                                 " the link already exists" % (src_type, dest_type))
             else:
                 req.args[Key.OUTGOING_LINKS] = 'created link %s(#%s)->%s(#%s)' % \
                                                 (src_type, src, dest_type, dest)
                 req.args['comment'] = 'Added link to %s(#%s)' % \
                                        (dest_type, dest)
                 update_ticket = True   
         elif cmd == 'delete link':
             if not sle.del_link_to(dle):
                 raise TracError("Link not existing! %s(#%s)->%s(#%s)" % \
                                 (src_type, src, dest_type, dest))
             else:
                 req.args[Key.OUTGOING_LINKS] = 'deleted link %s(#%s)->%s(#%s)' % \
                                                 (src_type, src, dest_type, dest)
                 req.args['comment'] = 'Deleted link to %s(#%s)' % (dest_type, dest)
                 update_ticket = True
         else:
             raise TracError("ERROR: Unknown Command %s!" % cmd)
         
         #set the link into the request and let TicketWrapper update the custom field
         if update_ticket:
             req.args['id'] = src
             req.args['summary'] = sle[Key.SUMMARY]
             req.args['ts'] = sle.time_changed
             req.args['action'] = 'leave'
         
         # Redirect to original /ticket url to avoid any change in existing view
         req.redirect(url or req.base_url)
Пример #22
0
 def __init__(self, env, number_rows_for_preview=20):
     self.do_preview = False
     self.rows = []
     self.number_rows_for_preview = number_rows_for_preview
     self.env = env
     self.tm = AgiloTicketModelManager(self.env)
Пример #23
0
 def __init__(self):
     """Initialize taking a reference to the ticket model manager"""
     self.manager = AgiloTicketModelManager(self.env)
Пример #24
0
 def __init__(self, env, number_rows_for_preview=20):
     self.do_preview = False
     self.rows = []
     self.number_rows_for_preview = number_rows_for_preview
     self.env = env
     self.tm = AgiloTicketModelManager(self.env)
Пример #25
0
class TestTicketModelManager(AgiloTestCase):
    """Tests AgiloTicket ModelManager, in particular the select method"""
    def setUp(self):
        self.super()
        self.manager = AgiloTicketModelManager(self.env)

    def test_create_ticket(self):
        """Tests the creation of a ticket using the ModelManager"""
        t = self.manager.create(summary="This is a ticket")
        self.assert_true(t.exists, "Ticket not existing...")
        self.assert_equals("This is a ticket", t[Key.SUMMARY])
        # Create without saving
        t2 = self.manager.create(summary="Not saved", save=False)
        self.assert_false(t2.exists, "The ticket has been saved!")
        self.assert_equals("Not saved", t2[Key.SUMMARY])
        # Now add something and change the summary
        t2[Key.DESCRIPTION] = "changed"
        t2[Key.SUMMARY] = "Now saved"
        self.manager.save(t2)
        self.assert_true(t2.exists)
        self.assert_equals("changed", t2[Key.DESCRIPTION])
        self.assert_equals("Now saved", t2[Key.SUMMARY])

    def test_ticket_caching(self):
        """Tests the ticket caching"""
        t1 = self.manager.create(summary="Ticket #1", t_type=Type.USER_STORY)
        t1_dupe = self.manager.get(tkt_id=t1.id)
        self.assert_equals(t1, t1_dupe)

    def test_select_tickets(self):
        """Tests the select method to get tickets"""
        milestone = self.teh.create_milestone('Test')
        sprint = self.teh.create_sprint('Test', milestone=milestone)
        t1 = self.manager.create(summary="Ticket #1",
                                 t_type=Type.USER_STORY,
                                 sprint=sprint.name)
        t2 = self.manager.create(summary="Ticket #2", t_type=Type.TASK)
        # Now the plan select should return both tickets
        tickets = self.manager.select()
        self.assert_true(t1 in tickets, "T1 is not in tickets!?")
        self.assert_true(t2 in tickets, "T2 is not in tickets!?")
        # Now selects all tickets planned for sprint Test
        self.assert_equals(sprint.name, t1[Key.SPRINT])
        tickets = self.manager.select(criteria={Key.SPRINT: 'Test'})
        self.assert_true(t1 in tickets, "T1 is not in ticket!?")
        self.assert_false(t2 in tickets, "T2 is in tickets and should not?!")
        # Now selects all tickets planned for milestone Test
        self.assert_equals('Test', t1[Key.MILESTONE])
        tickets = self.manager.select(criteria={Key.MILESTONE: 'Test'})
        self.assert_true(t1 in tickets, "T1 is not in tickets!?")
        self.assert_false(t2 in tickets, "T2 is in tickets and should not?!")
        # Now tests the select with a limit to 1
        tickets = self.manager.select(limit=1)
        self.assert_equals(1, len(tickets))
        tickets = self.manager.select(limit=2)
        self.assert_equals(2, len(tickets))
        # Now select all the tickets that have been created before now
        tickets = self.manager.select(criteria={'changetime': '<=%s' % \
                                                t1.time_created})
        self.assert_equals(2, len(tickets))
        # Now try out the order by
        tickets = self.manager.select(order_by=['-sprint'])
        self.assert_equals(tickets[0], t1)
        self.assert_equals(tickets[1], t2)

    def test_criteria_not_split_if_no_type(self):
        """Tests the splitting of the criteria in the selct query when
        containing the paramater ticket type"""
        criteria = {
            'summary': 'test',
            Key.REMAINING_TIME: '2',
            'id': 'not in (1, 2, 3)'
        }
        self.assert_none(self.manager._split_ticket_type(criteria))

    def test_criteria_split_if_type(self):
        """Tests the splitting of the criteria in the selct query when
        containing the paramater ticket type"""
        criteria = {
            'summary': 'test',
            Key.REMAINING_TIME: '2',
            'type': "in ('story', 'task')"
        }
        res = self.manager._split_ticket_type(criteria)
        self.assert_not_none(res)
        self.assert_equals('story', res[0].value)
        self.assert_equals('=', res[0].operator)
        self.assert_equals(('task', 'story'), res[1].value)
        self.assert_equals('in', res[1].operator)
Пример #26
0
 def setUp(self):
     self.super()
     self.manager = AgiloTicketModelManager(self.env)
Пример #27
0
class TestTicketModelManager(AgiloTestCase):
    """Tests AgiloTicket ModelManager, in particular the select method"""
    def setUp(self):
        self.super()
        self.manager = AgiloTicketModelManager(self.env)
        
    def test_create_ticket(self):
        """Tests the creation of a ticket using the ModelManager"""
        t = self.manager.create(summary="This is a ticket")
        self.assert_true(t.exists, "Ticket not existing...")
        self.assert_equals("This is a ticket", t[Key.SUMMARY])
        # Create without saving
        t2 = self.manager.create(summary="Not saved", save=False)
        self.assert_false(t2.exists, "The ticket has been saved!")
        self.assert_equals("Not saved", t2[Key.SUMMARY])
        # Now add something and change the summary
        t2[Key.DESCRIPTION] = "changed"
        t2[Key.SUMMARY] = "Now saved"
        self.manager.save(t2)
        self.assert_true(t2.exists)
        self.assert_equals("changed", t2[Key.DESCRIPTION])
        self.assert_equals("Now saved", t2[Key.SUMMARY])
    
    def test_ticket_caching(self):
        """Tests the ticket caching"""
        t1 = self.manager.create(summary="Ticket #1", 
                                 t_type=Type.USER_STORY)
        t1_dupe = self.manager.get(tkt_id=t1.id)
        self.assert_equals(t1, t1_dupe)
        
    def test_select_tickets(self):
        """Tests the select method to get tickets"""
        milestone = self.teh.create_milestone('Test')
        sprint = self.teh.create_sprint('Test', milestone=milestone)
        t1 = self.manager.create(summary="Ticket #1", 
                                 t_type=Type.USER_STORY,
                                 sprint=sprint.name)
        t2 = self.manager.create(summary="Ticket #2",
                                 t_type=Type.TASK)
        # Now the plan select should return both tickets
        tickets = self.manager.select()
        self.assert_true(t1 in tickets, "T1 is not in tickets!?")
        self.assert_true(t2 in tickets, "T2 is not in tickets!?")
        # Now selects all tickets planned for sprint Test
        self.assert_equals(sprint.name, t1[Key.SPRINT])
        tickets = self.manager.select(criteria={Key.SPRINT: 'Test'})
        self.assert_true(t1 in tickets, "T1 is not in ticket!?")
        self.assert_false(t2 in tickets, "T2 is in tickets and should not?!")
        # Now selects all tickets planned for milestone Test
        self.assert_equals('Test', t1[Key.MILESTONE])
        tickets = self.manager.select(criteria={Key.MILESTONE: 'Test'})
        self.assert_true(t1 in tickets, "T1 is not in tickets!?")
        self.assert_false(t2 in tickets, "T2 is in tickets and should not?!")
        # Now tests the select with a limit to 1
        tickets = self.manager.select(limit=1)
        self.assert_equals(1, len(tickets))
        tickets = self.manager.select(limit=2)
        self.assert_equals(2, len(tickets))
        # Now select all the tickets that have been created before now
        tickets = self.manager.select(criteria={'changetime': '<=%s' % \
                                                t1.time_created})
        self.assert_equals(2, len(tickets))
        # Now try out the order by
        tickets = self.manager.select(order_by=['-sprint'])
        self.assert_equals(tickets[0], t1)
        self.assert_equals(tickets[1], t2)

    def test_criteria_not_split_if_no_type(self):
        """Tests the splitting of the criteria in the selct query when
        containing the paramater ticket type"""
        criteria = {'summary': 'test', Key.REMAINING_TIME: '2',
                    'id': 'not in (1, 2, 3)'}
        self.assert_none(self.manager._split_ticket_type(criteria))

    def test_criteria_split_if_type(self):
        """Tests the splitting of the criteria in the selct query when
        containing the paramater ticket type"""
        criteria = {'summary': 'test', Key.REMAINING_TIME: '2',
                    'type': "in ('story', 'task')"}
        res = self.manager._split_ticket_type(criteria)
        self.assert_not_none(res)
        self.assert_equals('story', res[0].value)
        self.assert_equals('=', res[0].operator)
        self.assert_equals(('task', 'story'), res[1].value)
        self.assert_equals('in', res[1].operator)
Пример #28
0
class AgiloPolicy(Component):
    """
    Check access to all Agilo resources against the corresponding roles.
    """

    implements(IPermissionPolicy)

    def __init__(self):
        self.action_map = {
            Action.ATTACHMENT_CREATE: self.check_attachment_create,
            Action.CONFIRM_COMMITMENT: self.confirm_commitment,
            Action.TICKET_EDIT: self.check_ticket_edit,
            # We need also to catch trac's Actions for modifying tickets
            # - otherwise too many things will be allowed!
            Action.TICKET_CHANGE: self.check_ticket_edit,
            Action.TICKET_MODIFY: self.check_ticket_edit,
            Action.TICKET_EDIT_PAGE_ACCESS: self.check_ticket_edit_page_access,
            Action.TICKET_EDIT_DESCRIPTION: self.check_edit_description,
            # We don't have add a check for TICKET_APPEND because all three
            # roles get the TICKET_APPEND permission via meta permissions.
            # Therefore checking for team members is useless.
            # Action.TICKET_APPEND: self.check_ticket_append,
            Action.LINK_EDIT: self.check_link_edit,
            Action.SAVE_REMAINING_TIME: self.check_save_remaining_time,
            Action.BACKLOG_EDIT: self.check_backlog_edit,
        }
        self.tm = AgiloTicketModelManager(self.env)

    # --- permission checks ----------------------------------------------------

    # REFACT: Clean up the whole permission implementation.
    # Currently it's confusing + inconsistent with CREATE_... and edit.
    def check_ticket_edit(self, username, resource, perm, t_type=None):
        """
        Check agilo ticket edit permissions, the schema should be as follows:
            Action.PRODUCT_OWNER: can edit Type.REQUIREMENT, Type.USER_STORY
            Action.SCRUM_MASTER: can link Type.USER_STORY, edit Type.TASK
            Action.TEAM_MEMBER: can link Type.USER_STORY, edit own Type.TASK
                                or unassigned Type.TASK, or Type.TASK where is
                                a Key.RESOURCE.
        The permission try to be as much loose as possible to allow 
        customization the idea is to extend Type.USER_STORY to any Type.TASK
        container.
        """
        # check if is admin cause we don't need to check anything else
        if Action.TRAC_ADMIN in perm or Action.TICKET_ADMIN in perm:
            return True

        if not resource and t_type is None:
            return None

        ticket, t_type = self._get_ticket_and_type(resource, t_type)
        if t_type is None:
            return None

        if t_type not in (Type.TASK, Type.USER_STORY, Type.REQUIREMENT, Type.BUG):
            # We don't make any assumptions about ticket types which are not
            # part of the Agilo Core.
            return None
        if t_type == Type.BUG:
            return perm.has_permission(Action.CREATE_BUG, resource)

        # Task is our leaf, if we would consider task everything with Remaining Time
        # we may still encounter a type like Spike, with remaining time, but indeed
        # a potential task container.
        ticket_is_task = t_type == Type.TASK  # We can't make it more loose
        is_team_member = Role.TEAM_MEMBER in perm
        is_scrum_master = Role.SCRUM_MASTER in perm
        is_ticket_owner = is_reporter = False
        if ticket.get_value() is not None:
            is_ticket_owner = (username == ticket[Key.OWNER]) or (username in ticket.get_resource_list())
            is_reporter = username == ticket[Key.REPORTER]
        ticket_has_no_owner = ticket.get_value() is None or not ticket[Key.OWNER]
        is_product_owner = Role.PRODUCT_OWNER in perm

        if (
            is_ticket_owner
            or (ticket_is_task and (is_team_member or is_reporter) and ticket_has_no_owner)
            or (is_product_owner and not ticket_is_task)
            or (ticket_is_task and is_scrum_master)
        ):
            return True
        # We must stop the default policy which does not know anything about
        # types and does not check owners. Therefore anyone with TICKET_EDIT
        # permission could edit tickets if we don't deny here!
        return False

    def check_ticket_edit_page_access(self, username, resource, perm):
        def can_create_at_least_one_referenced_type(ticket_type):
            for allowed_type in LinksConfiguration(self.env).get_allowed_destination_types(ticket_type):
                permission_name = CoreTemplateProvider(self.env).get_permission_name_to_create(allowed_type)
                if permission_name in perm:
                    return True
            return False

        ticket_type = self._get_ticket_type(resource)
        if ticket_type is None:
            return True
        if can_create_at_least_one_referenced_type(ticket_type):
            return True
        return perm.has_permission(Action.TICKET_MODIFY, resource)

    def check_edit_description(self, username, resource, perm):
        # For now we just ignore trac's TICKET_EDIT_DESCRIPTION privileges
        return self.check_ticket_edit(username, resource, perm)

    def check_attachment_create(self, username, resource, perm):
        # The idea is that any user with ATTACHMENT_CREATE can create
        # attachments *if* she is allowed to edit the ticket. This check
        # places additional constraints on ATTACHMENT_CREATE: It denies access
        # for users who may not edit the ticket but leaves the final decision
        # to other trac policies.
        ticket = self._get_agilo_ticket(resource)
        if not ticket:
            return
        may_edit_ticket = self.check_ticket_edit(username, resource, perm)
        if may_edit_ticket == False:
            return False
        return None

    def check_link_edit(self, username, resource, perm):
        """
        Checks it the current user can edit links on the given resource.
        The rules are:
            - Product Owner can link every ticket which is not a task, this 
            means that the Product Owner will not be allowed to create tasks
            - Team Member and Scrum Master, can create link only to task, this
            means they can edit link on task containers
        This all works fine because the links can only be created from container
        to destination, therefore there is no problem with the direction.
        """
        is_product_owner = Role.PRODUCT_OWNER in perm
        is_tm_or_sm = Role.SCRUM_MASTER in perm or Role.TEAM_MEMBER in perm
        ticket = self._get_agilo_ticket(resource)
        is_task_container = False
        if ticket is not None:
            is_task_container = Type.TASK in [al.get_dest_type() for al in ticket.get_alloweds()]
        return (is_product_owner and not is_task_container) or (is_tm_or_sm and is_task_container) or None

    def check_backlog_edit(self, username, resource, perm):
        """
        Checks if the current user can edit the given Backlog Resource.
        The rules are:
            - Product Backlog: only Product Owner can edit
            - Sprint Backlog: only Scrum Master can fully edit, team 
            member will have rights on individual tickets (see ticket_edit)
            - Other Backlog: every authenticated user, as we can't make any
            other assumption.
        """
        name = self._get_backlog_name(resource)

        is_product_owner = Role.PRODUCT_OWNER in perm
        if is_product_owner and (name in (None, Key.PRODUCT_BACKLOG)):
            return True

        is_scrum_master = Role.SCRUM_MASTER in perm
        if is_scrum_master and (name in (None, Key.SPRINT_BACKLOG)):
            return True

        if name is None:
            return None

        is_custom_backlog = name not in (Key.PRODUCT_BACKLOG, Key.SPRINT_BACKLOG)
        is_authenticated_user = username is not None and username != "anonymous"
        if is_custom_backlog and is_authenticated_user:
            return True
        return None

    def check_save_remaining_time(self, username, resource, perm):
        """
        Checks if the current user can change the remaining time
        on the given ticket resource. The rules are:
            - Scrum Master: can always change the remaining time
            - Team Member: can change remaining time only if owner or resource
            of the given task, or the task has not yet been assigned, in which
            case the current user will become also owner.
        """
        is_scrum_master = Role.SCRUM_MASTER in perm
        if not is_scrum_master:
            return self.check_ticket_edit(username, resource, perm)
        return True

    def sprint(self, sprint_name):
        return SprintModelManager(self.env).get(name=sprint_name)

    def _has_sprint_started_more_than_one_day_ago(self, sprint):
        return now() - sprint.start > timedelta(days=1)

    def confirm_commitment(self, username, resource, perm):
        if (resource is None) or resource.realm != Realm.SPRINT:
            return None
        sprint = self.sprint(resource.id)
        if sprint is None:
            return None

        if sprint.team is None:
            # Actually this check is not really a policy decision (but technical
            # necessity) - however it makes some other code simpler
            return False
        if self._has_sprint_started_more_than_one_day_ago(sprint):
            # TODO: Maybe TRAC_ADMIN should be able to do it anyway?
            return False
        return None

    # IPermissionPolicy methods
    def check_permission(self, action, username, resource, perm):
        """
        Check that the action can be performed by username on the resource
        
        :param action: the name of the permission
        :param username: the username string or 'anonymous' if there's no
                         authenticated user
        :param resource: the resource on which the check applies.
                         Will be `None`, if the check is a global one and
                         not made on a resource in particular
        :param perm: the permission cache for that username and resource,
                     which can be used for doing secondary checks on other
                     permissions. Care must be taken to avoid recursion.
        
        :return: `True` if action is allowed, `False` if action is denied,
                 or `None` if indifferent. If `None` is returned, the next
                 policy in the chain will be used, and so on.
        """
        # run the function associated with this action
        if action in self.action_map:
            return self.action_map[action](username, resource, perm)
        # Return None because we don't care about this action so other
        # PermissionPolicies can vote on this request.
        return None

    # --- helpers -------------------------------------------------------------.

    def _get_resource_id_with_realm(self, resource, realm):
        while resource:
            if resource.realm == realm:
                break
            resource = resource.parent
        if not resource or (resource.realm != realm) or (resource.id is None):
            return None
        return resource.id

    def _get_agilo_ticket(self, resource):
        resource_id = self._get_resource_id_with_realm(resource, Realm.TICKET)
        return self.tm.get(tkt_id=resource_id)

    def _get_backlog_name(self, resource):
        return self._get_resource_id_with_realm(resource, Realm.BACKLOG)

    def _get_ticket_and_type(self, resource=None, ticket_type=None):
        ticket = LazyProxy(lambda: self._get_agilo_ticket(resource))
        if ticket_type is None:
            if ticket.get_value() is None:
                return None, None
            ticket_type = ticket.get_type()
        if ticket_type is not None:
            return ticket, ticket_type
        return None, None

    def _get_ticket_type(self, resource):
        ticket = self._get_agilo_ticket(resource)
        if ticket is not None:
            return ticket.get_type()
        return None
Пример #29
0
 def setUp(self):
     self.super()
     self.manager = AgiloTicketModelManager(self.env)
Пример #30
0
class CSVBasePerformer(object):
    """Base Performer class for CSV parsing"""
    def __init__(self, env, number_rows_for_preview=20):
        self.do_preview = False
        self.rows = []
        self.number_rows_for_preview = number_rows_for_preview
        self.env = env
        self.tm = AgiloTicketModelManager(self.env)
    
    def commit(self, req):
        '''Perform the operation for all processed rows. Return a list of new 
        or changed tickets'''
        raise NotImplementedError
    
    def check_header(self, header):
        """Check the headers for mandatory fields"""
        raise NotImplementedError
    
    def get_preview_rows(self):
        assert self.do_preview
        return self.rows
    
    def interesting_fieldnames(self):
        '''Return a list of field names which this performer will handle.'''
        raise NotImplementedError
    
    def name(self):
        'Return a human readable name of the performer.'
        return self.__class__.__name__
    
    def process(self, fields):
        '''Process the given row and save it for a later commit.'''
        if fields not in [None, {}]:
            if self.do_preview:
                if len(self.rows) < self.number_rows_for_preview:
                    self.rows.append(fields)
            else:
                self.rows.append(fields)
    
    def set_preview_mode(self):
        '''Tell the performer that it should only parse the lines for preview
        mode - therefore no data is changed.'''
        self.do_preview = True
    
    
    def _get_type_for_ticket(self, fields):
        type = Type.REQUIREMENT
        if Key.TYPE in fields:
            type = fields.pop(Key.TYPE).lower()
        return type
    
    def _may_create_ticket(self, perm, ticket_type):
        action_name = 'CREATE_%s' % ticket_type.upper()
        if hasattr(Action, action_name):
            permission = getattr(Action, action_name)
            return (permission in perm)
        return False # should be action_name in perm?
    
    def _get_ticket_from_id_in_csv(self, req, fields):
        ticket = None
        string_id = fields.get(Key.ID, fields.get('ticket'))
        try:
            ticket_id = int(string_id)
        except (ValueError, TypeError):
            add_warning(req, _("Non-numeric ticket ID '%s'") % string_id)
        else:
            try:
                ticket = self.tm.get(tkt_id=int(ticket_id))
            except ResourceNotFound:
                add_warning(req, _("Ticket %d does not exist") % ticket_id)
        return ticket
Пример #31
0
class CSVBasePerformer(object):
    """Base Performer class for CSV parsing"""
    def __init__(self, env, number_rows_for_preview=20):
        self.do_preview = False
        self.rows = []
        self.number_rows_for_preview = number_rows_for_preview
        self.env = env
        self.tm = AgiloTicketModelManager(self.env)

    def commit(self, req):
        '''Perform the operation for all processed rows. Return a list of new 
        or changed tickets'''
        raise NotImplementedError

    def check_header(self, header):
        """Check the headers for mandatory fields"""
        raise NotImplementedError

    def get_preview_rows(self):
        assert self.do_preview
        return self.rows

    def interesting_fieldnames(self):
        '''Return a list of field names which this performer will handle.'''
        raise NotImplementedError

    def name(self):
        'Return a human readable name of the performer.'
        return self.__class__.__name__

    def process(self, fields):
        '''Process the given row and save it for a later commit.'''
        if fields not in [None, {}]:
            if self.do_preview:
                if len(self.rows) < self.number_rows_for_preview:
                    self.rows.append(fields)
            else:
                self.rows.append(fields)

    def set_preview_mode(self):
        '''Tell the performer that it should only parse the lines for preview
        mode - therefore no data is changed.'''
        self.do_preview = True

    def _get_type_for_ticket(self, fields):
        type = Type.REQUIREMENT
        if Key.TYPE in fields:
            type = fields.pop(Key.TYPE).lower()
        return type

    def _may_create_ticket(self, perm, ticket_type):
        action_name = 'CREATE_%s' % ticket_type.upper()
        if hasattr(Action, action_name):
            permission = getattr(Action, action_name)
            return (permission in perm)
        return False  # should be action_name in perm?

    def _get_ticket_from_id_in_csv(self, req, fields):
        ticket = None
        string_id = fields.get(Key.ID, fields.get('ticket'))
        try:
            ticket_id = int(string_id)
        except (ValueError, TypeError):
            add_warning(req, _("Non-numeric ticket ID '%s'") % string_id)
        else:
            try:
                ticket = self.tm.get(tkt_id=int(ticket_id))
            except ResourceNotFound:
                add_warning(req, _("Ticket %d does not exist") % ticket_id)
        return ticket
Пример #32
0
    def process_request(self, req):
        # Check if it has been called with 'src' and 'dest' arguments or abort
        if not req.args.has_key('src') or not req.args.has_key(
                'dest') or not req.args.has_key('cmd'):
            raise TracError(
                "Links should be called with 'cmd', 'src' and 'dest' parameters",
                "Links: Source and/or Destination are Missing!")
        else:
            # Flag for ticket update
            update_ticket = False
            # Avoid recursive imports
            from agilo.ticket.model import AgiloTicketModelManager
            tm = AgiloTicketModelManager(self.env)
            try:
                src = int(req.args.get('src'))
                # Now that we have the ticket we can check permission to link or edit
                ticket_perm = req.perm('ticket', src)
                if Action.TICKET_EDIT not in ticket_perm or \
                        Action.LINK_EDIT not in ticket_perm:
                    raise TracError("You (%s) are not authorized to edit this ticket" % \
                                    req.authname)
                dest = int(req.args.get('dest'))
                cmd = req.args.get('cmd')
                url = req.args.get('url_orig') or None
            except:
                raise TracError("Source is not valid: src=%s" %
                                req.args.get('src'))
            # Create the LinkEndPoint for the source and destination if not existing
            sle = tm.get(tkt_id=src)
            dle = tm.get(tkt_id=dest)
            src_type = sle.get_type()
            dest_type = dle.get_type()

            if cmd == 'create link':
                if not sle.link_to(dle):
                    raise TracError("Links not allowed between %s->%s! The types are incompatible or" \
                                    " the link already exists" % (src_type, dest_type))
                else:
                    req.args[Key.OUTGOING_LINKS] = 'created link %s(#%s)->%s(#%s)' % \
                                                    (src_type, src, dest_type, dest)
                    req.args['comment'] = 'Added link to %s(#%s)' % \
                                           (dest_type, dest)
                    update_ticket = True
            elif cmd == 'delete link':
                if not sle.del_link_to(dle):
                    raise TracError("Link not existing! %s(#%s)->%s(#%s)" % \
                                    (src_type, src, dest_type, dest))
                else:
                    req.args[Key.OUTGOING_LINKS] = 'deleted link %s(#%s)->%s(#%s)' % \
                                                    (src_type, src, dest_type, dest)
                    req.args['comment'] = 'Deleted link to %s(#%s)' % (
                        dest_type, dest)
                    update_ticket = True
            else:
                raise TracError("ERROR: Unknown Command %s!" % cmd)

            #set the link into the request and let TicketWrapper update the custom field
            if update_ticket:
                req.args['id'] = src
                req.args['summary'] = sle[Key.SUMMARY]
                req.args['ts'] = sle.time_changed
                req.args['action'] = 'leave'

            # Redirect to original /ticket url to avoid any change in existing view
            req.redirect(url or req.base_url)
Пример #33
0
class AgiloPolicy(Component):
    """
    Check access to all Agilo resources against the corresponding roles.
    """
    
    implements(IPermissionPolicy)
    
    def __init__(self):
        self.action_map = {
            Action.ATTACHMENT_CREATE: self.check_attachment_create,
            Action.CONFIRM_COMMITMENT: self.confirm_commitment,
            Action.TICKET_EDIT: self.check_ticket_edit,
            # We need also to catch trac's Actions for modifying tickets 
            # - otherwise too many things will be allowed!
            Action.TICKET_CHANGE: self.check_ticket_edit,
            Action.TICKET_MODIFY: self.check_ticket_edit,
            Action.TICKET_EDIT_PAGE_ACCESS: self.check_ticket_edit_page_access,
            Action.TICKET_EDIT_DESCRIPTION: self.check_edit_description,
            
            # We don't have add a check for TICKET_APPEND because all three 
            # roles get the TICKET_APPEND permission via meta permissions. 
            # Therefore checking for team members is useless.
            # Action.TICKET_APPEND: self.check_ticket_append,
            Action.LINK_EDIT: self.check_link_edit,
            Action.SAVE_REMAINING_TIME: self.check_save_remaining_time,
            Action.BACKLOG_EDIT: self.check_backlog_edit,
        }
        self.tm = AgiloTicketModelManager(self.env)
    
    # --- permission checks ----------------------------------------------------
    
    # REFACT: Clean up the whole permission implementation. 
    # Currently it's confusing + inconsistent with CREATE_... and edit.
    def check_ticket_edit(self, username, resource, perm, t_type=None):
        """
        Check agilo ticket edit permissions, the schema should be as follows:
            Action.PRODUCT_OWNER: can edit Type.REQUIREMENT, Type.USER_STORY
            Action.SCRUM_MASTER: can link Type.USER_STORY, edit Type.TASK
            Action.TEAM_MEMBER: can link Type.USER_STORY, edit own Type.TASK
                                or unassigned Type.TASK, or Type.TASK where is
                                a Key.RESOURCE.
        The permission try to be as much loose as possible to allow 
        customization the idea is to extend Type.USER_STORY to any Type.TASK
        container.
        """
        # check if is admin cause we don't need to check anything else
        if Action.TRAC_ADMIN in perm or Action.TICKET_ADMIN in perm:
            return True
        
        if not resource and t_type is None:
            return None
        
        ticket, t_type = self._get_ticket_and_type(resource, t_type)
        if t_type is None:
            return None
        
        if t_type not in (Type.TASK, Type.USER_STORY, Type.REQUIREMENT, Type.BUG):
            # We don't make any assumptions about ticket types which are not 
            # part of the Agilo Core.
            return None
        if t_type == Type.BUG:
            return perm.has_permission(Action.CREATE_BUG, resource)
        
        # Task is our leaf, if we would consider task everything with Remaining Time
        # we may still encounter a type like Spike, with remaining time, but indeed
        # a potential task container.
        ticket_is_task = t_type == Type.TASK # We can't make it more loose
        is_team_member = Role.TEAM_MEMBER in perm
        is_scrum_master = Role.SCRUM_MASTER in perm
        is_ticket_owner = is_reporter = False
        if ticket.get_value() is not None:
            is_ticket_owner = (username == ticket[Key.OWNER]) or \
                (username in ticket.get_resource_list())
            is_reporter = (username == ticket[Key.REPORTER])
        ticket_has_no_owner = (ticket.get_value() is None or not ticket[Key.OWNER])
        is_product_owner = (Role.PRODUCT_OWNER in perm)
        
        if is_ticket_owner or \
            (ticket_is_task and (is_team_member or is_reporter) and ticket_has_no_owner) or \
            (is_product_owner and not ticket_is_task) or \
            (ticket_is_task and is_scrum_master):
            return True
        # We must stop the default policy which does not know anything about
        # types and does not check owners. Therefore anyone with TICKET_EDIT
        # permission could edit tickets if we don't deny here!
        return False
    
    def check_ticket_edit_page_access(self, username, resource, perm):
        def can_create_at_least_one_referenced_type(ticket_type):
            for allowed_type in LinksConfiguration(self.env).get_allowed_destination_types(ticket_type):
                permission_name = CoreTemplateProvider(self.env).get_permission_name_to_create(allowed_type)
                if permission_name in perm:
                    return True
            return False
        
        ticket_type = self._get_ticket_type(resource)
        if ticket_type is None:
            return True
        if can_create_at_least_one_referenced_type(ticket_type):
            return True
        return perm.has_permission(Action.TICKET_MODIFY, resource)
    
    def check_edit_description(self, username, resource, perm):
        # For now we just ignore trac's TICKET_EDIT_DESCRIPTION privileges
        return self.check_ticket_edit(username, resource, perm)
    
    def check_attachment_create(self, username, resource, perm):
        # The idea is that any user with ATTACHMENT_CREATE can create 
        # attachments *if* she is allowed to edit the ticket. This check 
        # places additional constraints on ATTACHMENT_CREATE: It denies access 
        # for users who may not edit the ticket but leaves the final decision
        # to other trac policies.
        ticket = self._get_agilo_ticket(resource)
        if not ticket:
            return
        may_edit_ticket = self.check_ticket_edit(username, resource, perm)
        if may_edit_ticket == False:
            return False
        return None
    
    def check_link_edit(self, username, resource, perm):
        """
        Checks it the current user can edit links on the given resource.
        The rules are:
            - Product Owner can link every ticket which is not a task, this 
            means that the Product Owner will not be allowed to create tasks
            - Team Member and Scrum Master, can create link only to task, this
            means they can edit link on task containers
        This all works fine because the links can only be created from container
        to destination, therefore there is no problem with the direction.
        """
        is_product_owner = Role.PRODUCT_OWNER in perm
        is_tm_or_sm = Role.SCRUM_MASTER in perm or Role.TEAM_MEMBER in perm
        ticket = self._get_agilo_ticket(resource)
        is_task_container = False
        if ticket is not None:
            is_task_container = Type.TASK in [al.get_dest_type() for al in ticket.get_alloweds()]
        return (is_product_owner and not is_task_container) or \
               (is_tm_or_sm and is_task_container) or \
               None
    
    def check_backlog_edit(self, username, resource, perm):
        """
        Checks if the current user can edit the given Backlog Resource.
        The rules are:
            - Product Backlog: only Product Owner can edit
            - Sprint Backlog: only Scrum Master can fully edit, team 
            member will have rights on individual tickets (see ticket_edit)
            - Other Backlog: every authenticated user, as we can't make any
            other assumption.
        """
        name = self._get_backlog_name(resource)
        
        is_product_owner = Role.PRODUCT_OWNER in perm
        if is_product_owner and (name in (None, Key.PRODUCT_BACKLOG)):
            return True
        
        is_scrum_master = Role.SCRUM_MASTER in perm
        if is_scrum_master and (name in (None, Key.SPRINT_BACKLOG)):
            return True
        
        if name is None:
            return None
        
        is_custom_backlog = name not in (Key.PRODUCT_BACKLOG, Key.SPRINT_BACKLOG)
        is_authenticated_user = username is not None and username != 'anonymous'
        if is_custom_backlog and is_authenticated_user:
            return True
        return None
    
    def check_save_remaining_time(self, username, resource, perm):
        """
        Checks if the current user can change the remaining time
        on the given ticket resource. The rules are:
            - Scrum Master: can always change the remaining time
            - Team Member: can change remaining time only if owner or resource
            of the given task, or the task has not yet been assigned, in which
            case the current user will become also owner.
        """
        is_scrum_master = Role.SCRUM_MASTER in perm
        if not is_scrum_master:
            return self.check_ticket_edit(username, resource, perm)
        return True
    
    def sprint(self, sprint_name):
        return SprintModelManager(self.env).get(name=sprint_name)
    
    def _has_sprint_started_more_than_one_day_ago(self, sprint):
        return now() - sprint.start > timedelta(days=1)
    
    def confirm_commitment(self, username, resource, perm):
        if (resource is None) or resource.realm != Realm.SPRINT:
            return None
        sprint = self.sprint(resource.id)
        if sprint is None:
            return None
        
        if sprint.team is None:
            # Actually this check is not really a policy decision (but technical
            # necessity) - however it makes some other code simpler
            return False
        if self._has_sprint_started_more_than_one_day_ago(sprint):
            # TODO: Maybe TRAC_ADMIN should be able to do it anyway?
            return False
        return None
    
    # IPermissionPolicy methods
    def check_permission(self, action, username, resource, perm):
        """
        Check that the action can be performed by username on the resource
        
        :param action: the name of the permission
        :param username: the username string or 'anonymous' if there's no
                         authenticated user
        :param resource: the resource on which the check applies.
                         Will be `None`, if the check is a global one and
                         not made on a resource in particular
        :param perm: the permission cache for that username and resource,
                     which can be used for doing secondary checks on other
                     permissions. Care must be taken to avoid recursion.
        
        :return: `True` if action is allowed, `False` if action is denied,
                 or `None` if indifferent. If `None` is returned, the next
                 policy in the chain will be used, and so on.
        """
        # run the function associated with this action
        if action in self.action_map:
            return self.action_map[action](username, resource, perm)
        # Return None because we don't care about this action so other 
        # PermissionPolicies can vote on this request.
        return None
    
    # --- helpers -------------------------------------------------------------.
    
    def _get_resource_id_with_realm(self, resource, realm):
        while resource:
            if resource.realm == realm:
                break
            resource = resource.parent
        if not resource or (resource.realm != realm) or (resource.id is None):
            return None
        return resource.id
    
    def _get_agilo_ticket(self, resource):
        resource_id = self._get_resource_id_with_realm(resource, Realm.TICKET)
        return self.tm.get(tkt_id=resource_id)
    
    def _get_backlog_name(self, resource):
        return self._get_resource_id_with_realm(resource, Realm.BACKLOG)
    
    def _get_ticket_and_type(self, resource=None, ticket_type=None):
        ticket = LazyProxy(lambda: self._get_agilo_ticket(resource))
        if ticket_type is None:
            if ticket.get_value() is None:
                return None, None
            ticket_type = ticket.get_type()
        if ticket_type is not None:
            return ticket, ticket_type
        return None, None
    
    def _get_ticket_type(self, resource):
        ticket = self._get_agilo_ticket(resource)
        if ticket is not None:
            return ticket.get_type()
        return None