예제 #1
0
    def create(self, req, attributes):
        """ Create a structure milestone object."""
        name = attributes.get('summary')
        try:
            if StructuredMilestone(self.env, name).exists:
                raise TracError('Milestone with name %s already exists' % name)
        except ResourceNotFound:
            pass

        milestone = StructuredMilestone(self.env)
        milestone.name = name
        req.perm.require('MILESTONE_CREATE',
                         Resource(milestone.resource.realm))

        milestone.description = attributes.get('description')

        milestone.ticket.populate(attributes)

        def set_date(name, attr_name=None):
            val = attributes.get(name)
            if val is None:
                return
            val = val and parse_date(val, tzinfo=req.tz) or None
            if attr_name is not None:
                setattr(milestone, attr_name, val)
            milestone.ticket[name] = val and str(to_timestamp(val))

        set_date('duedate', 'due')
        set_date('completedate', 'completed')
        set_date('started')

        milestone.ticket.values['reporter'] = attributes.get(
            'author') or get_reporter_id(req)
        milestone.insert()
        return self._milestone_as_dict(milestone)
예제 #2
0
 def _resolve_milestone(self, name, include_kids, show_completed):
     def _flatten_and_get_names(mil, include_kids, show_completed):
         names= []
         if mil:
             mil = isinstance(mil, StructuredMilestone) and [mil,] or mil
             for m in mil:
                 if show_completed or not m.completed:
                     names.append(m.name)
                     if include_kids:
                         names.extend(_flatten_and_get_names(m.kids, include_kids, show_completed))
         return names
     if name=='nearest':
         db = self.env.get_db_cnx()
         cursor = db.cursor()
         cursor.execute(
             'SELECT name FROM milestone WHERE due>%s ORDER BY due LIMIT 1', \
             (to_timestamp(datetime.now(utc)),))
         row = cursor.fetchone()
         name=row and row[0] or 'none'
     elif name=='not_completed_milestones':
         return _flatten_and_get_names(StructuredMilestone.select(self.env, False), \
             include_kids, show_completed)
     if name=='none':
         return ''
     try:    
         mil = StructuredMilestone(self.env, name)            
         names = _flatten_and_get_names(mil, include_kids, show_completed)
         if not names:
             names = mil.name
         return names
     except ResourceNotFound:
         return ''
예제 #3
0
    def update(self, req, name, comment, attributes=None):
        """ Updates a structure milestone object."""
        milestone = StructuredMilestone(self.env, name)
        if not milestone.exists:
            raise TracError('Milestone with name %s does not exist' % name)
        req.perm.require('MILESTONE_MODIFY',
                         Resource(milestone.resource.realm))
        if attributes is not None:
            milestone.name = attributes.get('summary')
            milestone.description = attributes.get('description')
            milestone.ticket.populate(attributes)

            def set_date(name, attr_name=None):
                val = attributes.get(name)
                if val is None:
                    return
                val = val and parse_date(val, tzinfo=req.tz) or None
                if attr_name is not None:
                    setattr(milestone, attr_name, val)
                milestone.ticket[name] = val and str(to_timestamp(val))

            set_date('duedate', 'due')
            set_date('completedate', 'completed')
            set_date('started')

        milestone.save_changes(get_reporter_id(req, 'author'), comment)
        return self._milestone_as_dict(milestone)
예제 #4
0
 def get(self, req, name):
     """ Get a structure milestone object."""
     milestone = StructuredMilestone(self.env, name)
     req.perm.require('MILESTONE_VIEW', Resource(milestone.resource.realm))
     if not milestone.exists:
         raise TracError('Milestone with name %s does not exist' % name)
     return self._milestone_as_dict(milestone)
예제 #5
0
 def delete(self, req, name):
     """ Deletes structure milestone object."""
     milestone = StructuredMilestone(self.env, name)
     if not milestone.exists:
         raise TracError('Milestone with name %s does not exist' % name)
     req.perm.require('MILESTONE_DELETE',
                      Resource(milestone.resource.realm))
     milestone.delete()
     return {'name': milestone.name, 'description': milestone.description}
예제 #6
0
    def _get_job_done(self, mil_names, tkt_type=None, db=None):
        started_at = to_timestamp(datetime(tzinfo=localtz, \
            *(StructuredMilestone(self.env, mil_names[0]).started.timetuple()[:3])))
        db = db or self.env.get_db_cnx()
        cursor = db.cursor()

        base_sql = None
        params = list(mil_names)
        group_by = " GROUP BY t.id, t.type"
        final_statuses = IttecoEvnSetup(self.env).final_statuses
        status_params = ("%s," * len(final_statuses))[:-1]
        params = final_statuses + final_statuses + params
        if self.count_burndown_on == 'quantity':
            base_sql = """SELECT MAX(c.time), t.id, t.type, 1
                FROM ticket t 
                    LEFT JOIN milestone m ON m.name=t.milestone 
                    LEFT OUTER JOIN ticket_change c ON t.id=c.ticket 
                        AND c.field='status' AND c.newvalue IN (%s) 
                WHERE IN (%s) AND m.name IN (%s)""" % \
                    (
                        status_params,
                        status_params,
                        ("%s,"*len(mil_names))[:-1]
                    )
        else:
            base_sql = "SELECT MAX(c.time), t.id, t.type, "+db.cast(db.concat('0','tc.value'),'int')+ \
                """FROM ticket t 
                    LEFT JOIN milestone m ON m.name=t.milestone 
                    LEFT JOIN ticket_custom tc ON t.id=tc.ticket AND tc.name=%%s
                    LEFT OUTER JOIN ticket_change c ON t.id=c.ticket AND c.field='status' 
                        AND c.newvalue IN (%s) 
                WHERE t.status IN (%s) AND m.name IN (%s)""" % \
                    (
                        status_params,
                        status_params,
                        ("%s,"*len(mil_names))[:-1]
                    )
            params = [self.count_burndown_on] + params
            group_by += ", tc.value"
        if tkt_type:
            if isinstance(tkt_type, basestring):
                base_sql += " AND t.type=%s"
                params += [tkt_type]
            else:
                base_sql += " AND t.type IN (%s)" % ("%s," *
                                                     len(tkt_type))[:-1]
                params += list(tkt_type)

        cursor.execute(base_sql + group_by + " ORDER BY 1", params)
        data = [(to_datetime((dt < started_at) and started_at
                             or dt), ttype, sum or 0)
                for dt, tkt_id, ttype, sum in cursor]
        return data
예제 #7
0
    def _comment_milestone(self, req, mil_id):
        milestone = StructuredMilestone(self.env, mil_id)
        if not milestone.exists:
            raise ResourceNotFound('Milestone %s does not exist.' % mil_id,
                                   'Invalid Milestone Name')

        req.perm.require('MILESTONE_MODIFY', milestone.resource)
        changes = TicketModule(self.env).rendered_changelog_entries(
            req, milestone.ticket)
        return 'itteco_milestone_comment.html', {
            'milestone': milestone,
            'changes': changes
        }, 'text/html'
예제 #8
0
 def _render_link(self, context, name, label, extra=''):
     try:
         milestone = StructuredMilestone(self.env, name)
     except TracError:
         milestone = None
     # Note: the above should really not be needed, `Milestone.exists`
     # should simply be false if the milestone doesn't exist in the db
     # (related to #4130)
     href = context.href.milestone(name)
     if milestone and milestone.exists:
         if 'MILESTONE_VIEW' in context.perm(milestone.resource):
             closed = milestone.is_completed and 'closed ' or ''
             return tag.a(label,
                          class_='%smilestone' % closed,
                          href=href + extra)
     elif 'MILESTONE_CREATE' in context.perm('milestone', name):
         return tag.a(label,
                      class_='missing milestone',
                      href=href + extra,
                      rel='nofollow')
     return tag.a(label, class_='missing milestone')
예제 #9
0
    def milestone(self, req):
        mil_id = req.args.get('obj_id')
        if mil_id:
            req.perm.require('MILESTONE_MODIFY', Resource('milestone', mil_id))
        else:
            req.perm.require('MILESTONE_CREATE')

        milestone = StructuredMilestone(self.env, mil_id)
        descriptor = WhiteboardModule(self.env).get_new_ticket_descriptor(
            [type.name for type in Type.select(self.env)], milestone.ticket.id)

        data = {
            'structured_milestones':
            StructuredMilestone.select(self.env),
            'new_ticket_descriptor':
            descriptor,
            'milestone':
            milestone,
            'action_controls':
            self._get_action_controls(req, descriptor['ticket']),
        }
        return 'itteco_milestone_quick_edit.html', data, 'text/html'
예제 #10
0
    def process_request(self, req):
        milestone_id = req.args.get('id')
        req.perm('milestone', milestone_id).require('MILESTONE_VIEW')

        add_link(req, 'up', req.href.roadmap(), _('Roadmap'))

        db = self.env.get_db_cnx()  # TODO: db can be removed
        milestone = StructuredMilestone(self.env, milestone_id, db)
        action = req.args.get('action', 'view')

        if req.method == 'POST':
            if req.args.has_key('cancel'):
                req.redirect(req.href.roadmap())
            elif action == 'delete':
                self._do_delete(req, db, milestone)
            return self._do_save(req, db, milestone)

        elif action in ('new', 'edit', 'view'):
            return self._render_editor(req, db, milestone)
        elif action == 'delete':
            return self._render_confirm(req, db, milestone)

        req.redirect(req.href.roadmap())
예제 #11
0
    def metrics(self, milestone):
        mil = StructuredMilestone(self.env, milestone)
        if not mil.is_started:
            return None

        scope = self._milestone_scope(mil)

        scope_types = IttecoEvnSetup(self.env).scope_element
        types = self._work_types()
        one_day_delta = timedelta(1)

        metrics = [{
            'datetime': mil.started,
            'burndown': scope,
            'burnup': [0] * len(types)
        }]

        def clone_item(item, new_time):
            item = deepcopy(item)
            item['datetime'] = new_time
            metrics.append(item)
            return item

        for ts, ttype, sum in self._burnup_info(mil):
            last = metrics[-1]
            if ts != last['datetime']:
                if self.fill_idle_days:
                    time_delta = ts - last['datetime']
                    if time_delta.days > 1:
                        last = clone_item(last, ts - one_day_delta)
                last = clone_item(last, ts)

            if ttype in self.initial_plan_element:
                #this ticket type should influence burndown
                last['burndown'] -= sum
            elif ttype not in scope_types:
                #burnup by ticket type turned around axis X, in order to look like burndown
                idx = types.index(ttype)
                last['burnup'][idx] += sum

        calculated_end_date = None
        if len(metrics) > 1:  # do we have any progress?
            start = metrics[0]
            end = metrics[-1]
            if start['burndown'] != end['burndown']:
                # do we have any progress to perform approximations?
                start_ts = to_timestamp(start['datetime'])
                end_ts = to_timestamp(end['datetime'])
                calc_ts = start_ts+ \
                    start['burndown']*(end_ts - start_ts)/ \
                        (start['burndown']-end['burndown'])
                end['approximation'] = end['burndown']
                calculated_end_date = to_datetime(calc_ts)

        if mil.due:  #if milestone is fully planned, add line of ideal progress
            if mil.due > metrics[-1]['datetime']:
                metrics.append({'datetime': mil.due})
            metrics[0]['ideal'] = scope
            metrics[-1]['ideal'] = 0

        if calculated_end_date:
            if calculated_end_date > metrics[-1]['datetime']:
                metrics.append({
                    'datetime': calculated_end_date,
                    'approximation': 0
                })
            else:
                #let's find a correct place on timeline for calculated end date
                for i in xrange(0, len(metrics)):
                    metric = metrics[i]
                    if metric['datetime'] >= calculated_end_date:
                        if metric['datetime'] > calculated_end_date:
                            metric = {'datetime': calculated_end_date}
                            metrics.insert(i, metric)
                        metric['approximation'] = 0
                        break

        return metrics, types
예제 #12
0
    def _chart_settings(self, milestone):
        burndown_info = self.burndown_info_provider.metrics(milestone)
        mils =[]
        def flatten(mil):
            mils.append(mil)
            for kid in mil.kids:
                flatten(kid)
        flatten(StructuredMilestone(self.env, milestone))
        fmt_date = lambda x: format_datetime(x, '%Y-%m-%dT%H:%M:%S')
        cvs_data = graphs = events = None
        if burndown_info:
            metrics, graphs = burndown_info
            def get_color(tkt_type):
                tkt_cfg = self.ticket_type_config
                if tkt_cfg:
                    cfg = tkt_cfg.get(tkt_type)
                    if cfg:
                        return cfg.get('max_color')

            graphs = [{'name': graph, 'color': get_color(graph)} for graph in graphs]

            burndown_cvs_data = burnup_cvs_data = []
            keys = ['burndown', 'approximation', 'ideal']
            milestone_dates= dict([(mil.completed or mil.due, mil) for mil in mils ])
            events =[]
            prev_burndown = metrics[0]['burndown']
            prev_burnup = 0
            
            def genitems(metric):
                yield fmt_date(metric['datetime'])
                for key in keys:
                    yield str(metric.get(key,''))
                    
            for metric in metrics:
                ts = metric['datetime']
                line = ",".join(genitems(metric))
                burnup_sum = 0
                burnup = metric.get('burnup',[])
                for item in burnup:
                    burnup_sum -= item
                    line +=','+str(-1*item)
                if burnup:
                    line +=','+str(burnup_sum)
                burndown_cvs_data.append(line)

                if ts in milestone_dates:
                    mil = milestone_dates[ts]
                    if mil.is_completed:
                        del milestone_dates[ts]
                        burndown = metric['burndown']
                        events.append({'datetime': fmt_date(mil.completed),
                            'extended': True, 
                            'text': '"%s" completed\nBurndown delta %d\nBurnup delta %d.' \
                                % (mil.name, prev_burndown-burndown, prev_burnup-burnup_sum) ,
                            'url': self.env.abs_href('milestone',mil.name)})
                        burndown_delta =0
                        prev_burnup = burnup_sum
                        prev_burndown = burndown
            events.extend([{'datetime': fmt_date(mil.due),
                            'text': '"%s" is planned to be completed.' % mil.name ,
                            'url': self.env.abs_href('milestone',mil.name)} for mil in milestone_dates.itervalues()])
            cvs_data = "<![CDATA["+"\n".join(burndown_cvs_data)+"]]>"

        data = {'data': cvs_data, 'graphs': graphs, 'events': events}
        return 'iiteco_chart_settings.xml', data, 'text/xml'
예제 #13
0
    def _do_save(self, req, db, milestone):
        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')

        ticket_module = TicketModule(self.env)
        ticket_module._populate(req, milestone.ticket, False)
        if not milestone.exists:
            reporter_id = get_reporter_id(req, 'author')
            milestone.ticket.values['reporter'] = reporter_id

        action = req.args.get('action', 'leave')

        field_changes, problems = ticket_module.get_ticket_changes(
            req, milestone.ticket, action)
        if problems:
            for problem in problems:
                add_warning(req, problem)
                add_warning(
                    req,
                    tag(
                        tag.p('Please review your configuration, '
                              'probably starting with'),
                        tag.pre('[trac]\nworkflow = ...\n'),
                        tag.p('in your ', tag.tt('trac.ini'), '.')))

        ticket_module._apply_ticket_changes(milestone.ticket, field_changes)

        old_name = milestone.name
        new_name = milestone.ticket['summary']

        milestone.name = new_name
        milestone.description = milestone.ticket['description']

        due = req.args.get('duedate', '')
        milestone.due = due and parse_date(due, tzinfo=req.tz) or None
        milestone.ticket['duedate'] = milestone.due and str(
            to_timestamp(milestone.due)) or None

        completed = req.args.get('completedate', '')
        retarget_to = req.args.get('target')

        # Instead of raising one single error, check all the constraints and
        # let the user fix them by going back to edit mode showing the warnings
        warnings = []

        def warn(msg):
            add_warning(req, msg)
            warnings.append(msg)

        # -- check the name
        if new_name:
            if new_name != old_name:
                # check that the milestone doesn't already exists
                # FIXME: the whole .exists business needs to be clarified
                #        (#4130) and should behave like a WikiPage does in
                #        this respect.
                try:
                    other_milestone = StructuredMilestone(
                        self.env, new_name, db)
                    warn(
                        _(
                            'Milestone "%(name)s" already exists, please '
                            'choose another name',
                            name=new_name))
                except ResourceNotFound:
                    pass
        else:
            warn(_('You must provide a name for the milestone.'))

        # -- check completed date
        if action in MilestoneSystem(self.env).starting_action:
            milestone.ticket['started'] = str(to_timestamp(datetime.now(utc)))
        if action in MilestoneSystem(self.env).completing_action:
            milestone.completed = datetime.now(utc)

        if warnings:
            return self._render_editor(req, db, milestone)

        # -- actually save changes
        if milestone.exists:
            cnum = req.args.get('cnum')
            replyto = req.args.get('replyto')
            internal_cnum = cnum
            if cnum and replyto:  # record parent.child relationship
                internal_cnum = '%s.%s' % (replyto, cnum)

            now = datetime.now(utc)
            milestone.save_changes(get_reporter_id(req, 'author'),
                                   req.args.get('comment'),
                                   when=now,
                                   cnum=internal_cnum)
            # eventually retarget opened tickets associated with the milestone
            if 'retarget' in req.args and completed:
                cursor = db.cursor()
                cursor.execute(
                    "UPDATE ticket SET milestone=%s WHERE "
                    "milestone=%s and status != 'closed'",
                    (retarget_to, old_name))
                self.env.log.info('Tickets associated with milestone %s '
                                  'retargeted to %s' % (old_name, retarget_to))
        else:
            milestone.insert()
        db.commit()

        add_notice(req, _('Your changes have been saved.'))
        jump_to = req.args.get('jump_to', 'roadmap')
        if jump_to == 'roadmap':
            req.redirect(req.href.roadmap())
        elif jump_to == 'whiteboard':
            req.redirect(
                req.href.whiteboard('team_tasks') + '#' + milestone.name)
        else:
            req.redirect(req.href.milestone(milestone.name))
예제 #14
0
    def _render_admin_panel(self, req, cat, page, milestone):
        req.perm.require('TICKET_ADMIN')
        add_stylesheet(req, 'itteco/css/common.css')
        add_jscript(
            req, 
            [
                'stuff/ui/ui.core.js',
                'stuff/ui/ui.resizable.js',
                'custom_select.js'
            ],
            IttecoEvnSetup(self.env).debug
        )
        # Detail view?
        if milestone:
            mil = StructuredMilestone(self.env, milestone)
            if req.method == 'POST':
                if req.args.get('save'):
                    mil.name = req.args.get('name')
                    mil.due = mil.completed = None
                    due = req.args.get('duedate', '')
                    if due:
                        mil.due = parse_date(due, req.tz)
                    if req.args.get('completed', False):
                        completed = req.args.get('completeddate', '')
                        mil.completed = parse_date(completed, req.tz)
                        if mil.completed > datetime.now(utc):
                            raise TracError(_('Completion date may not be in '
                                              'the future'),
                                            _('Invalid Completion Date'))
                    mil.description = req.args.get('description', '')
                    mil.parent = req.args.get('parent', None)
                    if mil.parent and mil.parent==mil.name:
                        raise TracError(_('Milestone cannot be parent for itself,Please, give it another thought.'),
                                        _('Something is wrong with Parent Milestone. Will you check it please?'))

                    if mil.parent and not StructuredMilestone(self.env, mil.parent).exists:
                        raise TracError(_('Milestone should have a valid parent. It does not look like this is the case.'),
                                        _('Something is wrong with Parent Milestone. Will you check it please?'))
                    mil.update()
                    req.redirect(req.href.admin(cat, page))
                elif req.args.get('cancel'):
                    req.redirect(req.href.admin(cat, page))

            add_script(req, 'common/js/wikitoolbar.js')
            data = {'view': 'detail', 'milestone': mil}

        else:
            if req.method == 'POST':
                # Add Milestone
                if req.args.get('add') and req.args.get('name'):
                    name = req.args.get('name')
                    try:
                        StructuredMilestone(self.env, name)
                    except ResourceNotFound:
                        mil = StructuredMilestone(self.env)
                        mil.name = name
                        if req.args.get('duedate'):
                            mil.due = parse_date(req.args.get('duedate'),
                                                 req.tz)
                        mil.parent = req.args.get('parent', None)
                        if mil.parent and not StructuredMilestone(self.env, mil.parent).exists:
                            raise TracError(_('Milestone should have a valid parent. It does not look like this is the case'),
                                            _('Something is wrong with Parent Milestone. Will you check it please?'))

                        mil.insert()
                        req.redirect(req.href.admin(cat, page))
                    else:
                        raise TracError(_('Sorry, milestone %s already exists.') % name)

                # Remove milestone
                elif req.args.get('remove'):
                    sel = req.args.get('sel')
                    if not sel:
                        raise TracError(_('Please, select the milestone.'))
                    if not isinstance(sel, list):
                        sel = [sel]
                    db = self.env.get_db_cnx()
                    for name in sel:
                        mil = StructuredMilestone(self.env, name, db=db)
                        mil.delete(db=db)
                    db.commit()
                    req.redirect(req.href.admin(cat, page))

                # Set default milestone
                elif req.args.get('apply'):
                    if req.args.get('default'):
                        name = req.args.get('default')
                        self.config.set('ticket', 'default_milestone', name)
                        self.config.save()
                        req.redirect(req.href.admin(cat, page))

            data = {
                'view': 'list',
                'default': self.config.get('ticket', 'default_milestone'),
            }
            
        # Get ticket count
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        milestones = []
        structured_milestones = StructuredMilestone.select(self.env)
        mil_names = self._get_mil_names(structured_milestones)
        
        cursor.execute("SELECT milestone, COUNT(*) FROM ticket "
                   "WHERE milestone IN (%s) GROUP BY milestone" % ("%s,"*len(mil_names))[:-1], mil_names)
        mil_tkt_quantity = {}
        for mil, cnt in cursor:
            mil_tkt_quantity[mil]=cnt

        data.update({
            'date_hint': get_date_format_hint(),
            'milestones': [(mil, 0) for mil in structured_milestones],# we recover this anyway
            'structured_milestones': structured_milestones,
            'milestone_tickets_quantity': mil_tkt_quantity,
            'max_milestone_level': self.milestone_levels and len(self.milestone_levels)-1 or 0,
            'datetime_hint': get_datetime_format_hint()
        })
        return 'itteco_admin_milestones.html', data