# -*- coding: utf-8 -*-
#   Copyright 2007-2008 Agile42 GmbH - Andrea Tomasini 
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
# 
# Authors:
#     - Andrea Tomasini <andrea.tomasini__at__agile42.com>

import csv
from datetime import timedelta
import re
from StringIO import StringIO

from genshi.builder import tag
from pkg_resources import resource_filename
from trac.core import Component, implements, TracError
from trac.mimeview import Mimeview
from trac.mimeview.api import IContentConverter, Context
from trac.resource import IResourceManager, Resource
from trac.ticket.model import Milestone
from trac.util import get_reporter_id
from trac.util.datefmt import to_datetime, format_datetime, parse_date
from trac.util.translation import _
from trac.web.chrome import add_link, add_stylesheet, add_script, \
    add_warning, Chrome, ITemplateProvider

from agilo.api import view
from agilo.charts.chart_generator import ChartGenerator
from agilo.scrum import BACKLOG_URL
from agilo.scrum.backlog.controller import BacklogController
from agilo.scrum.backlog.json_ui import ConfiguredChildTypesView
from agilo.scrum.contingent import ContingentWidget
from agilo.scrum.metrics import MetricsController
from agilo.scrum.sprint import SprintController
from agilo.scrum.team import TeamController
from agilo.ticket.api import AgiloTicketSystem
from agilo.ticket.model import AgiloTicketModelManager
from agilo.ticket.renderers import TimePropertyRenderer
from agilo.utils import controls, Action, Key, Role, BacklogType, \
    Type, Status, Realm, use_jquery_13
from agilo.utils.compat import json
from agilo.utils.config import AgiloConfig
from agilo.ticket.web_ui import AgiloTicketModule

# Regexp to match ticket paramaters change
T_PARAM = re.compile(r'col_(?P<id>[0-9]+)_(?P<col>[\w]+)')

BACKLOG_CONVERSION_KEY = 'agilo.scrum.backlog.Backlog'

# Add values for the totals of a backlog
def add_value(actual_val, prop_val):
    """Sums the values if are addable and returns the new value"""
    result = 0
    if actual_val:
        result += actual_val
    if prop_val and \
            (isinstance(prop_val, int) or \
             isinstance(prop_val, basestring) and prop_val.isdigit()):
        result += int(prop_val)
    return result

class BacklogAction(object):
    """Represent an action for the backlog"""
    CALCULATE = 'calculate'
    CONFIRM = 'confirm'
    DELETE = 'delete'
    EDIT = 'edit'
    REMOVE = 'remove'
    SAVE = 'save'
    SORT = 'sort'
    EDIT_SAVE_SORT = [EDIT, SAVE, SORT]
    EDIT_SAVE_SORT_REMOVE = EDIT_SAVE_SORT + [REMOVE]


class BacklogModule(Component):
    """Represent a Trac Component to manage Backlogs"""
    
    implements(IResourceManager, ITemplateProvider)
    
    #=============================================================================
    # ITemplateProvider methods
    #=============================================================================
    def get_htdocs_dirs(self):
        return [('agilo', resource_filename('agilo.scrum.backlog', 'htdocs'))]
    
    def get_templates_dirs(self):
        return [resource_filename('agilo.scrum.backlog', 'templates')]
    
    def send_backlog_list_data(self, req, data):
        """add to the data array the backlog data necessary to show 
        the backlog list"""
        def milestone_options(milestones, selected):
            """
            Return three lists of tuple (milestone_name, selected) for 
            the milestones which are:
                - Open with a due date
                - Open without a due date
                - Closed
            """
            open_with_due_date = list()
            open_without_due_date = list()
            closed = list()
            for m in milestones:
                m_list = None
                if m.is_completed:
                    m_list = closed
                elif m.due:
                    m_list = open_with_due_date
                else:
                    m_list = open_without_due_date
                # append the milestone to the list
                m_list.append((m.name, m.name==selected))
            
            return open_with_due_date, open_without_due_date, closed
        
        # Maximum items in the pulldown, ordered by status and by time
        # The number is the maximum per status, so 5 closed, 5 running
        # and 5 to start
        MAX_ITEMS = 5
        
        sprint_list = milestone_list = None
        if data is not None and Action.BACKLOG_VIEW in req.perm:
            # get sprint data
            get_options = SprintController.GetSprintOptionListCommand(self.env)
            closed, running, to_start = \
                SprintController(self.env).process_command(get_options)
            s_sprint = self.get_session_sprint(req)
            # Running Sprints
            running_sprints = [(name, name==s_sprint) for \
                               name in running]
            # Ready to Start
            ready_to_start_sprints = [(name, name==s_sprint) for \
                                      name in to_start][:MAX_ITEMS]
            # Closed Sprints
            closed_sprints = [(name, name==s_sprint) \
                              for name in closed][-MAX_ITEMS:]
            # The close by date needs to be reversed on the last items
            # so the last closed appears on top
            closed_sprints.reverse()
            sprint_list = [
                {Key.LABEL: _('Running (by Start Date)'),
                 Key.OPTIONS: running_sprints},
                {Key.LABEL: _('To Start (by Start Date)'),
                 Key.OPTIONS: ready_to_start_sprints},
                {Key.LABEL: _('Closed (by End Date)'),
                 Key.OPTIONS: closed_sprints},
            ]
            data['sprint_list'] = sprint_list
            cmd_list = BacklogController.ListBacklogsCommand(self.env)
            data['backlog_list'] = \
                BacklogController(self.env).process_command(cmd_list)
            s_milestone = self.get_session_milestone(req)
            open_due, open, closed = \
                milestone_options(Milestone.select(self.env), s_milestone)
            milestone_list = [
                {Key.LABEL: _('Open (by Due Date)'),
                 Key.OPTIONS: open_due},
                {Key.LABEL: _('Open (no Due Date)'),
                 Key.OPTIONS: open},
                {Key.LABEL: _('Closed'),
                 Key.OPTIONS: closed[:MAX_ITEMS]}]
            data['milestone_list'] = milestone_list

    def _get_scope_for_backlog_type_from_session(self, req,
                                                 b_type=BacklogType.SPRINT):
        return req.session.get('agilo-%s-scope' % b_type)
    
    def _save_scope_for_backlog_type_in_session(self, req, scope, 
                                                b_type=BacklogType.SPRINT):
        if scope is not None:
            old_scope = \
                self._get_scope_for_backlog_type_from_session(req, b_type)
            if scope != old_scope:
                req.session['agilo-%s-scope' % b_type] = scope
    
    def reset_session_scope(self, req, b_type=BacklogType.SPRINT):
        """Resets any Agilo preset session scope, used in case a 
        stored value proves invalid"""
        key = 'agilo-%s-scope' % b_type
        if req.session.has_key(key):
            del req.session[key]
    
    def get_session_sprint(self, req):
        """Returns the sprint stored in the session"""
        return self._get_scope_for_backlog_type_from_session(req, BacklogType.SPRINT)
    
    def get_session_milestone(self, req):
        """Returns the milestone stored in the session"""
        return self._get_scope_for_backlog_type_from_session(req, BacklogType.MILESTONE)

    #=============================================================================
    # IResourceManager methods
    #=============================================================================
    def get_resource_realms(self):
        """Return resource realms managed by the component.
        :rtype: `basestring` generator"""
        yield Realm.BACKLOG

    def get_resource_url(self, resource, href, **kwargs):
        """Return the canonical URL for displaying the given resource.

        :param resource: a `Resource`
        :param href: an `Href` used for creating the URL

        Note that if there's no special rule associated to this realm 
        for creating URLs (i.e. the standard convention of using 
        realm/id applies), then it's OK to not define this method."""
        pass
        
    def _render_link(self, context, name, label, scope=None):
        """Renders the backlog Link"""
        backlog = self._get_backlog(context, name=name, scope=scope)
        href = context.href(BACKLOG_URL, name, scope)
        if backlog.exists and \
                Action.BACKLOG_VIEW in context.perm(backlog.resource):
            return tag.a(label, class_='backlog', href=href, rel="nofollow")

    def get_resource_description(self, resource, format='default', context=None,
                                 **kwargs):
        """Return a string representation of the resource, according to the
        `format`."""
        desc = resource.id
        if format != 'compact':
            desc =  _('Backlog (%(name)s)', name=resource.id)
        if context:
            return self._render_link(context, resource.id, desc)
        else:
            return desc
    
    def _get_sprint(self, req, sprint_name):
        """Retrieve the Sprint for the given name"""
        get_sprint = SprintController.GetSprintCommand(self.env, 
                                                       sprint=sprint_name)
        sprint = SprintController(self.env).process_command(get_sprint)
        # we need to convert sprint dates into local timezone
        sprint.start = format_datetime(sprint.start, tzinfo=req.tz)
        sprint.end = format_datetime(sprint.end, tzinfo=req.tz)
        if sprint.team is None:
            msg = _("No Team has been assigned to this Sprint...")
            if not msg in req.chrome['warnings']:
                add_warning(req, msg)
        return sprint
    
    def _get_backlog(self, req, name, scope=None, reload=True, filter_by=None):
        """Retrieve the Backlog with the given name and scope, and sets it as
        default in the session of the user"""
        cmd_get_backlog = \
            BacklogController.GetBacklogCommand(self.env, name=name, 
                                scope=scope, reload=reload, filter_by=filter_by)
        backlog = BacklogController(self.env).process_command(cmd_get_backlog)
        if scope:
            self._save_scope_for_backlog_type_in_session(req, scope, 
                                                         backlog.b_type)
        return backlog


class BacklogContentConverter(Component):
    
    implements(IContentConverter)
    
    def get_supported_conversions(self):
        yield ('csv', _('Comma-delimited Text'), 'csv',
               BACKLOG_CONVERSION_KEY, 'text/csv', 9)
    
    def convert_content(self, req, mimetype, backlog, key):
        """Convert the given content from mimetype to the output MIME type
        represented by key. Returns a tuple in the form (content,
        output_mime_type) or None if conversion is not possible."""
        if key == 'csv':
            return self.export_csv(req, backlog, mimetype='text/csv')
    
    def _get_field_names(self, backlog):
        """Return an ordered collection of all field names which appear in one 
        of the tickets for this backlog."""
        ticket_types = set()
        for bi in backlog:
            ticket_types.add(bi[Key.TYPE])
        
        field_names = set()
        ats = AgiloTicketSystem(self.env)
        for ticket_type in ticket_types:
            for field in ats.get_ticket_fields(ticket_type):
                field_names.add(field[Key.NAME])
        return list(field_names)
    
    def _export_ticket(self, req, ticket, writer, field_names):
        
        cols = [unicode(ticket.id)]
        for name in field_names:
            value = ticket[name] or ''
            if name in ('cc', 'reporter'):
                context = Context.from_request(req, ticket.resource)
                value = Chrome(self.env).format_emails(context, value, ' ')
            cols.append(value.encode('utf-8'))
        writer.writerow(cols)
    
    def export_csv(self, req, backlog, sep=',', mimetype='text/plain'):
        field_names = self._get_field_names(backlog)
        content = StringIO()
        writer = csv.writer(content, delimiter=sep, quoting=csv.QUOTE_MINIMAL)
        writer.writerow(['id'] + [unicode(name) for name in field_names])
        
        for bi in backlog:
            ticket = bi.ticket
            ticket_resource = Resource('ticket', ticket.id)
            if Action.TICKET_VIEW in req.perm(ticket_resource):
                self._export_ticket(req, ticket, writer, field_names)
        return (content.getvalue(), '%s;charset=utf-8' % mimetype)


class BacklogListView(view.HTTPView):
    """A view to enlist existing configured backlogs"""
    template = 'agilo_backlog_list.html'
    controller_class = BacklogController
    url = BACKLOG_URL
    url_regex = '/?$'
    
    def do_get(self, req):
        data = {}
        # Actually this code is executed in the agilo.utils.web_ui - but only if
        # the Agilo UI is enabled. In this view we need the data anyway so we 
        # need to add the variables here anyway.
        # fs: This looks a bit like a smell to me. Better assign explicit 
        # variables and use them? Make the backlog_list_block a widget?
        # adds to data the info needed to visualize the Backlog list
        BacklogModule(self.env).send_backlog_list_data(req, data)
        return data

class BacklogDetailView(view.HTTPView):
    """A view to display the details of a backlog"""
    template = 'agilo_backlog_detail.html'
    controller_class = BacklogController
    url = BACKLOG_URL
    url_regex = '/(?P<name>[^/]+)(/(?P<scope>[^/]+))?/?$'
    
    def _add_conversion_links(self, req, backlog):
        mime = Mimeview(self.env)
        
        for conversion in mime.get_supported_conversions(BACKLOG_CONVERSION_KEY):
            format = conversion[0]
            backlog_href = req.href(BACKLOG_URL, backlog.name, 
                                    backlog.scope, format=format)
            add_link(req, 'alternate', backlog_href, conversion[1],
                     conversion[4], format)
    
    def _add_milestone_backlog_buttons(self, milestone):
        allowed_actions = []
        # Is a milestone backlog, check the milestone is still open
        if milestone.completed is None:
            allowed_actions += BacklogAction.EDIT_SAVE_SORT_REMOVE
        return allowed_actions
    
    def _add_sprint_backlog_buttons(self, sprint, backlog, perm):
        allowed_actions = []
        if sprint and not sprint.is_closed:
            # The sprint is not closed we can still modify it
            allowed_actions += BacklogAction.EDIT_SAVE_SORT
            
            if Role.SCRUM_MASTER in perm:
                if backlog.filter_by is None:
                    sprint_started_at_max_one_day_ago = \
                        (to_datetime(None) - parse_date(sprint.start)) < timedelta(days=1)
                    if sprint_started_at_max_one_day_ago:
                        allowed_actions.append(BacklogAction.CONFIRM)
                    # enable the calculate button too
                    allowed_actions.append(BacklogAction.CALCULATE)
                    allowed_actions.append(BacklogAction.REMOVE)
        return allowed_actions
    
    def _configured_child_types(self, req):
        view = ConfiguredChildTypesView(self.env)
        configured_child_types = view.do_get(req, req.args)
        return view.as_json(configured_child_types)
    
    def _backlog_information(self, req, backlog):
        backlog_info = dict(type=BacklogType.LABELS[backlog.b_type],
                            name=backlog.name,
                            sprint_or_release=backlog.scope)
        return json.dumps(backlog_info)
    
    def _load_chart_widgets(self, req, backlog):
        """Returns a list containing all configured chart widgets for this 
        backlog given the current scope."""
        charts = list()
        filter_by = self._get_additional_filter_attribute(req)
        # Check if there are charts for the backlog or not
        conf_charts = AgiloConfig(self.env).backlog_charts.get(backlog.name)
        if conf_charts is not None:
            generator = ChartGenerator(self.env)
            for chart_name in conf_charts:
                cached_data = dict(tickets=backlog)
                widget = generator.get_chartwidget(chart_name, 
                                                   sprint_name=backlog.scope,
                                                   cached_data=cached_data, 
                                                   filter_by=filter_by)
                widget.prepare_rendering(req)
                charts.append(widget)
        return charts
    
    def _get_attribute_filter_options(self):
        """Return the list of options for the additional filter attribute 
        specified in the configuration. Return if the configured attribute is
        not a select field or the field is unknown."""
        filter_by_attr = AgiloConfig(self.env).backlog_filter_attribute
        if filter_by_attr is not None:
            ats = AgiloTicketSystem(self.env)
            for field in ats.get_ticket_fields():
                if field[Key.NAME] == filter_by_attr:
                    if Key.OPTIONS in field:
                        return field[Key.OPTIONS]
                    break
        return None
    
    def _get_stories_for_ids(self, story_ids):
        tm = AgiloTicketModelManager(self.env)
        criteria = {
            'id': 'in (%s)' % ', '.join([str(st_id) for st_id in story_ids]),
            'type': Type.USER_STORY}
        return tm.select(criteria=criteria)
    
    def _display_options(self, req):
        # Read from the session the current display settings
        my_tickets = int(req.session.get(Key.MY_TICKETS, '0'))
        closed_tickets = int(req.session.get(Key.CLOSED_TICKETS, '0'))
        return {'my_tickets': my_tickets, 'closed_tickets': closed_tickets}

    def _do_show(self, req, data, backlog):
        """Prepare data to send to Genshi for display"""
        if backlog is not None:
            columns = [
                {'name': 'id', 'editable': False, 'type': 'text', 'label': 'ID'}, 
                {'name': 'summary', 'editable': False, 'type': 'text', 'label': _('Summary')},
            ]
            data['backlog'] = backlog
            data['backlog_name'] = backlog.name
            data['username'] = req.authname
            sprint = None
            if backlog.scope != None and backlog.b_type == BacklogType.SPRINT:
                widget = ContingentWidget(self.env, sprint=backlog.scope, backlog=True)
                widget.prepare_rendering(req)
                data['contingent_widget'] = widget
                # Load the sprint as well
                sprint = BacklogModule(self.env)._get_sprint(req, backlog.scope)
                if sprint and sprint.exists:
                    data[Key.SPRINT] = sprint
                    data[Key.TEAM] = sprint.team
                    
            # Add columns and headers
            conf_columns = AgiloConfig(self.env).backlog_columns.get(backlog.name)
            # Store the sum of add-able fields
            sums = dict()
            if conf_columns is not None:
                columns += conf_columns
                # FIXME: Put this in the template as soon as the Genshi bug is solved
                # Calculate the totals where possible
                for col in conf_columns:
                    name = col[Key.NAME]
                    if name == Key.REMAINING_TIME and \
                            backlog.b_type == BacklogType.SPRINT:
                        cmd_get_total = \
                            SprintController.GetTotalRemainingTimeCommand(self.env,
                                                sprint=sprint.name, tickets=backlog)
                        total_rem_time = SprintController(self.env).process_command(cmd_get_total)
                        sums[name] = TimePropertyRenderer(self.env, total_rem_time)
                    else:
                        # TODO: move out the sum from here, is an N*M, try to choose
                        # sumable columns before and use them as pivot, so it will be
                        # possible to sum only in one cycle on the backlog.
                        for bi in backlog:
                            if bi.is_visible:
                                sums[name] = add_value(sums.get(name), bi[name])
            
            data['headers'] = [h['label'] for h in columns]
            data['columns'] = columns
            data['backlog_totals'] = sums
            data['attribute_filter_options'] = self._get_attribute_filter_options()
            data['child_type_mapping'] = self._configured_child_types(req)
            data['backlog_info'] = self._backlog_information(req, backlog)
            
            chart_widgets = self._load_chart_widgets(req, backlog)
            data['charts'] = chart_widgets
            self._add_conversion_links(req, backlog)
        elif not AgiloConfig(self.env).is_agilo_ui:
            self.send_backlog_list_data(req, data)

    def _do_calculate(self, req, backlog):
        sprint = BacklogModule(self.env)._get_sprint(req, backlog.scope)
        if sprint.team == None:
            raise TracError(_('No team selected for this sprint'))
        story_ids = self._extract_selected_ticket_ids(req)
        stories = self._get_stories_for_ids(story_ids)
        if len(stories) == 0:
            # we can't use add_warning here because we do a redirect
            # at the end of the request and trac does not provide a
            # mechanism to preserve the message for the next page to be
            # shown
            raise TracError(_('No stories selected'))
        else:
            cmd = \
                SprintController.CalculateRemainingTimeUSPRatioCommand(self.env, 
                                            sprint=sprint.name, stories=stories)
            ratio = SprintController(self.env).process_command(cmd)
            if ratio == None:
                msg = _('Could not calculate Remaining Time/User Story Point ' + \
                        'ratio. Probably no remaining time set for tasks.')
                raise TracError(msg)
            else:
                cmd_class = MetricsController.StoreMetricsCommand
                cmd = cmd_class(self.env, sprint=sprint.name, 
                                name=Key.RT_USP_RATIO, value=ratio)
                MetricsController(self.env).process_command(cmd)

    def _prepare_permissions(self, req, data, backlog):
        """Prepares the permissions for the current user and backlog"""
        # Store allowed actions
        allowed_actions = self.get_allowed_actions(req, backlog)
        may_edit_backlog = BacklogAction.EDIT in allowed_actions
        
        # Send permission for editing
        def can_edit(req, ticket):
            """Returns True if the given user can edit the ticket"""
            if req is not None and ticket is not None and may_edit_backlog:
                if ticket[Key.STATUS] != Status.CLOSED:
                    return AgiloTicketModule(self.env).can_edit_ticket(req, ticket)
            return False
        
        def can_select_ticket_to_do_some_action(req, ticket):
            if not may_edit_backlog:
                return False
            
            can_delete_ticket = BacklogAction.DELETE in allowed_actions
            can_remove_ticket = BacklogAction.REMOVE in allowed_actions
            can_calculate_rtusp = (BacklogAction.CALCULATE in allowed_actions) and (ticket.is_readable_field(Key.STORY_POINTS))
            if can_delete_ticket or can_remove_ticket or can_calculate_rtusp:
                return True
            
            return False
        
        data['ticket_actions'] = {
            'delete': {'show': BacklogAction.DELETE in allowed_actions, 
                       'button': controls[BacklogAction.DELETE]},
                       'remove': {'show': BacklogAction.REMOVE in allowed_actions, 
                                  'button': controls[BacklogAction.REMOVE]}
        }
        data['global_actions'] = {
            'save': {'show': BacklogAction.SAVE in allowed_actions,
                     'button': controls[BacklogAction.SAVE]}, 
                     'sort': {'show': BacklogAction.SORT in allowed_actions,
                              'button': controls[BacklogAction.SORT]}
        }
        data['planning_actions'] = {
            'calculate': {'show': BacklogAction.CALCULATE in allowed_actions,
                          'button': controls[BacklogAction.CALCULATE]},
                          'confirm': {'show': BacklogAction.CONFIRM in allowed_actions,
                                      'button': controls[BacklogAction.CONFIRM]}
        }
        data['may_edit_backlog'] = may_edit_backlog
        data['can_edit_ticket'] = can_edit
        data['can_select_ticket_to_do_some_action'] = can_select_ticket_to_do_some_action
        
        # AT: This should go into the _do_show, but there will require an
        # additional call to is_allowed, and here we are checking permissions
        # anyway
        if AgiloConfig(self.env).is_agilo_ui and \
                BacklogAction.EDIT in allowed_actions and \
                (backlog.filter_by is None):
            # Only enable drag&drop for users which can actually save the 
            # backlog
            add_script(req, 'agilo/js/jquery.tablednd_0_5.js')
    
    def _get_additional_filter_attribute(self, req):
        filter_by = req.args.get('filter_by', '').strip()
        if filter_by == '':
            filter_by = None
        return filter_by
    
    def get_allowed_actions(self, req, backlog):
        """Return the list of allowed actions for the given backlog and the
        currently logged in user."""
        # Actions for this backlog
        allowed_actions = list()
        
        if backlog:
            permission_cache = req.perm(backlog.resource)
            sprint = None
            
            if Action.BACKLOG_EDIT in permission_cache:
                # check if the backlog is scoped
                if backlog.scope not in (None, Key.GLOBAL):
                    if backlog.b_type == BacklogType.SPRINT:
                        # Is a sprint, check if it is still open
                        sprint = BacklogModule(self.env)._get_sprint(req, backlog.scope)
                        sprint_buttons = \
                            self._add_sprint_backlog_buttons(sprint, backlog, 
                                                             permission_cache)
                        allowed_actions.extend(sprint_buttons)
                    elif backlog.b_type == BacklogType.MILESTONE:
                        m = Milestone(self.env, backlog.scope)
                        milestone_buttons = \
                            self._add_milestone_backlog_buttons(m)
                        allowed_actions.extend(milestone_buttons)
                
                else:
                    # Nothing to check, is not scoped, just put in all the 
                    # standard permissions
                    allowed_actions += BacklogAction.EDIT_SAVE_SORT
                    # Remove doesn't make sense in global backlog, the ticket
                    # will pop out again due to the SQL query
                    #allowed_actions.append(BacklogAction.REMOVE)
            
            elif Role.TEAM_MEMBER in permission_cache and backlog.scope and \
                    backlog.b_type == BacklogType.SPRINT:
                # get the sprint
                sprint = BacklogModule(self.env)._get_sprint(req, backlog.scope)
                if sprint and not sprint.is_closed and sprint.team:
                    # No explicit permission, but can still be a team member if there 
                    # is a sprint backlog.
                    team_member_names = [m.name for m in sprint.team.members]
                    if req.authname in team_member_names:
                        allowed_actions += BacklogAction.EDIT_SAVE_SORT
            
            if Action.TICKET_DELETE in permission_cache:
                allowed_actions.append(BacklogAction.DELETE)
        
        return allowed_actions

    def is_allowed_to(self, req, backlog, action):
        """Return True if the current request is allowed to perform the given
        action."""
        return action in self.get_allowed_actions(req, backlog)

    def _check_request_and_params(self, req):
        """Checks the request is valid and returns the backlog and data"""
        name = req.args.get('name')
        scope = req.args.get('scope') or req.args.get('bscope')
        filter_by = self._get_additional_filter_attribute(req)
        if not name:
            raise TracError(_("Please provide at least a backlog name"))
        
        data = self._display_options(req)
        
        backlog = BacklogModule(self.env)._get_backlog(req, name, scope=scope, 
                                                       filter_by=filter_by)
        # Now check the permissions
        if backlog:
            req.perm(backlog.resource).require(Action.BACKLOG_VIEW)
        
        add_stylesheet(req, 'common/css/report.css')
        add_stylesheet(req, 'agilo/stylesheet/backlog.css')
        add_script(req, 'agilo/js/json2.js')
        add_script(req, 'agilo/js/querystring_parser.js')
        add_script(req, 'agilo/js/backlog_ui.js')
        
        return backlog, data

    def _perform_action(self, req, backlog, action, data):
        """Perform the given do_method action, if allowed, or reload with a
        warning if not allowed"""
        if self.is_allowed_to(req, backlog, action):
            do_action = getattr(self, '_do_' + action)
            do_action(req, backlog)
            # To make it REST we redirect after the save
            params = {'name': backlog.name}
            if backlog.scope is not None:
                params['scope'] = backlog.scope
            self.redirect(req, BacklogDetailView, backlog.name, backlog.scope)
        else:
            add_warning(req, "You are not allowed to perform %s on this Backlog!" % action)
            self._do_show(req, data, backlog)

    def _extract_selected_ticket_ids(self, req):
        ticket_ids = []
        for k, v in req.args.items():
            if k.startswith('chk_') and v == 'on':
                t_id = int(k[len('chk_'):])
                ticket_ids.append(t_id)
        return ticket_ids
    
    def _do_confirm(self, req, backlog):
        """Confirms the initial team commitment, setting the estimated 
        velocity for this sprint"""
        sprint = BacklogModule(self.env)._get_sprint(req, backlog.scope)
        if sprint.team:
            velocity_class = TeamController.StoreTeamVelocityCommand
            velocity_cmd = velocity_class(self.env, sprint=sprint,
                                          team=sprint.team,
                                          estimated=True)
            commitment_class = TeamController.CalculateAndStoreTeamCommitmentCommand
            commitment_cmd = commitment_class(self.env, sprint=sprint,
                                              team=sprint.team,
                                              tickets=backlog)
            capacity_class = TeamController.CalculateDailyCapacityCommand
            capacity_cmd = capacity_class(self.env, sprint=sprint,
                                          team=sprint.team)
            # Now execute the commands
            t_controller = TeamController(self.env)
            t_controller.process_command(velocity_cmd)
            t_controller.process_command(commitment_cmd)
            capacity = t_controller.process_command(capacity_cmd)
            if len(capacity) > 0:
                store_capacity_class = MetricsController.StoreMetricsCommand
                store_capacity_cmd = store_capacity_class(self.env,
                                                          sprint=sprint,
                                                          name=Key.CAPACITY,
                                                          value=sum(capacity))
                MetricsController(self.env).process_command(store_capacity_cmd)
                
    def _do_remove(self, req, backlog, delete=False):
        """Removes the selected tickets from the backlog. If delete is True
        (default False), the tickets will be deleted permanently."""
        for t_id in self._extract_selected_ticket_ids(req):
            backlog.remove(t_id, delete=delete)
        # Theoretically we could decide to check if there is a sprint associated
        # with the backlog and invalidate only that sprint but I'm not sure if
        # this will really cover all cases (maybe some tasks don't have a 
        # sprint?)
        ChartGenerator(self.env).invalidate_cache()
        self._save_backlog(req, backlog)
    
    def _do_sort(self, req, backlog):
        """Sorts the backlog according to the set keys"""
        backlog.sort()
        self._save_backlog(req, backlog)
    
    def _do_save(self, req, backlog):
        """Saves the backlog, updating it with the request data"""
        def update_ticket_changes(changes, t_id, key, value):
            """
            Updates the backlog dictionary to include 
            the key:value for this t_id
            """
            if changes is not None:
                if not changes.has_key(t_id):
                    changes[t_id] = dict()
                changes[t_id].update({key: value})
        
        # Stores ticket changes by id, so we can save backlog items
        # in one shot instead of retrieving them for every parameter
        backlog_changes = {} # t_id: {key: value}
        # Check if there has been some position change
        for k, v in req.args.items():
            t_id = field = None
            if k.startswith('pos'):
                field, t_id = k.split('_')
                if v not in (None, ''):
                    v = int(v)
            elif T_PARAM.match(k):
                m_param = T_PARAM.match(k)
                t_id = m_param.group('id')
                field = m_param.group('col')
            if field:
                # update the ticket changes ignore other parameters
                update_ticket_changes(backlog_changes, t_id, field, v)
        
        # Now update the backlog items
        for t_id, changes in backlog_changes.items():
            # Get the backlog item
            bi = backlog.get_item_by_id(t_id)
            if bi is not None:
                # get all ticket changes
                for k, v in changes.items():
                    # Check if we have to move
                    if k == 'pos':
                        if v is not None and bi.pos != v:
                            bi.pos = v
                    else:
                        # change through the backlog item will set the modified
                        # flag to true, so that trac ticket validation will be 
                        # called.
                        bi[k] = v
                # Replace the timestamp in the req before validation
                # We have multiple tickets here
                req.args['ts'] = req.args.get('%s_ts' % bi.id)
                if bi.modified and not \
                        AgiloTicketModule(self.env)._validate_ticket(req, bi.ticket):
                    # Trac will take care of replacing the old valid ticket values in the
                    # ticket in case of validation failure.
                    bi.modified = False
                    bi.style = 'warning'
                    
        # Saves the ticket changes too
        return self._save_backlog(req, backlog, 
                                  author=get_reporter_id(req, Key.AUTHOR))
    
    def _save_backlog(self, req, backlog, author=None, comment=None):
        """
        Saves the given backlog and intercept Exceptions re-routing
        them as warning or errors
        """
        cmd_params = ['name', 'ticket_types', 'sorting_keys', 'scope', 'b_type', 
                      'description', 'b_strict', 'filter_by']
        # prepare the params to send to the backlog
        params = dict([(name, getattr(backlog, name)) for name in cmd_params])
        params.update({'tickets': backlog._tickets, 'author': author, 'comment': comment})
        save_cmd = BacklogController.SaveBacklogCommand(self.env, **params)
        return self.controller.process_command(save_cmd)
        
    def _do_delete(self, req, backlog):
        """Delete the selected tickets from the backlog"""
        self._do_remove(req, backlog, delete=True)
    
    def do_get(self, req):
        use_jquery_13(req)
        # Should get the name of the backlog and load it
        backlog, data = self._check_request_and_params(req)
        
        # Check the format of the backlog
        format = req.args.get('format')
        if format and (backlog is not None):
            mime = Mimeview(self.env)
            # This method will handle the request completely, will not return
            mime.send_converted(req, BACKLOG_CONVERSION_KEY, backlog, format, 
                                filename=backlog.scope)
        
        # Send data
        self._do_show(req, data, backlog)
        self._prepare_permissions(req, data, backlog)
        return data

    def do_post(self, req):
        # manage the POST request
        # Should get the name of the backlog and load it
        backlog, data = self._check_request_and_params(req)
        
        if Action.BACKLOG_EDIT not in req.perm(backlog.resource) and \
                Role.TEAM_MEMBER not in req.perm(backlog.resource):
            add_warning(req, _("You are not allowed to perform this operation..."))
        
        if req.args.has_key('sort'):
            self._perform_action(req, backlog, 'sort', data)
        elif req.args.has_key('save'):
            self._perform_action(req, backlog, 'save', data)
        elif req.args.has_key('remove'):
            self._perform_action(req, backlog, 'remove', data)
        elif req.args.has_key('delete'):
            self._perform_action(req, backlog, 'delete', data)
        elif req.args.has_key('calculate'):
            self._perform_action(req, backlog, 'calculate', data)
        elif req.args.has_key('confirm'):
            self._perform_action(req, backlog, 'confirm', data)
            
        # Send data
        self._do_show(req, data, backlog)
        self._prepare_permissions(req, data, backlog)
        return data
