def closed_statuses_for_all_types(self): """Returns a dictionary where the keys are tickets types and the associated values are statuses from workflow status groups where closed='True'. Essentially if a ticket is in one of these statuses, we consider it closed and from this infer that no more work is required to complete the ticket. """ controller = LogicaOrderController(self.env) closed_statuses = controller.type_and_statuses_for_closed_statusgroups() types_and_statuses = [] for type_, statuses in closed_statuses.iteritems(): types_and_statuses.append(type_) types_and_statuses.extend(statuses) return closed_statuses, types_and_statuses
def _get_ticket_data(self, req, results): ats = AgileToolsSystem(self.env) loc = LogicaOrderController(self.env) closed_statuses = loc.type_and_statuses_for_closed_statusgroups() # TODO calculate which statuses are closed using the query system # when it is able to handle this tickets = [] for result in results: if result['status'] not in closed_statuses[result['type']]: filtered_result = dict((k, v) for k, v in result.iteritems() if k in self.fields) if "remaininghours" in filtered_result: try: hours = float(filtered_result["remaininghours"]) except (ValueError, TypeError): hours = 0 del filtered_result["remaininghours"] else: hours = 0 if "effort" in filtered_result: try: storypoints = float(filtered_result['effort']) except (ValueError, TypeError): storypoints = 0 else: storypoints = 0 reporter = filtered_result["reporter"] session = DetachedSession(self.env, reporter) filtered_result.update({ 'id': result['id'], 'position': ats.position(result['id']), 'hours': hours, 'effort': storypoints, 'reporter': session.get('name', reporter), 'changetime': to_utimestamp(filtered_result['changetime']) }) tickets.append(filtered_result) return tickets
def _capture(db): def calculate_initial_values_for_ticket(ticket_id): # first seen changes will be from the very first information we have about this ticket initial_values_cursor = db.cursor() initial_values_cursor.execute("SELECT time FROM ticket WHERE id = %s", (ticket_id,)) ticket_created = from_utimestamp(initial_values_cursor.fetchone()[0]) history_date = ticket_created.date() # find original values for the ticket for column in ticket_values.keys(): initial_values_cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket = %s AND field = %s ORDER BY time LIMIT 1", (ticket_id, column)) result = initial_values_cursor.fetchone() if result is None: if column in built_in_fields: initial_values_cursor.execute("SELECT %s FROM ticket WHERE id = %%s" % column, (ticket_id,)) result = initial_values_cursor.fetchone() ticket_values[column] = encode_and_escape(result[0]) else: initial_values_cursor.execute("SELECT value FROM ticket_custom WHERE ticket = %s AND name = %s", (ticket_id, column)) result = initial_values_cursor.fetchone() if result: ticket_values[column] = encode_and_escape(result[0]) else: ticket_values[column] = r'\N' else: ticket_values[column] = encode_and_escape(result[0]) ticket_values['id'] = str(ticket_id) ticket_values['time'] = str(ticket_created) ticket_values['changetime'] = str(ticket_created) ticket_values['_resolutiontime'] = r'\N' # assumption that you cannot create a ticket in status closed # so we give isclosed a false value from the off ticket_values['isclosed'] = "0" # PROBLEM: How can we detect when a milestone was renamed (and # tickets updated) - this isn't mentioned in the ticket_change # table. # Maybe we have to search the log file for strings?! # source:trunk/trac/trac/ticket/model.py@8937#L1192 return ticket_values, ticket_created, history_date last_snapshot_cursor = db.cursor() last_snapshot_cursor.execute("SELECT _snapshottime FROM ticket_bi_historical ORDER BY _snapshottime DESC LIMIT 1") last_snapshot_result = last_snapshot_cursor.fetchone() del last_snapshot_cursor if last_snapshot_result: last_snapshot = last_snapshot_result[0] print "Last successful run was at %s" % last_snapshot if until <= last_snapshot: print "Already have data for %s, so can't run with until=%s" % (last_snapshot, until) return False else: last_snapshot = None print "No previous runs" # Get statuses we consider to be closed for each ticket type controller = LogicaOrderController(self.env) closed_statuses = controller.type_and_statuses_for_closed_statusgroups() if only_ticket: ticket_ids = [(int(only_ticket),)] else: ticket_ids_c = db.cursor() ticket_ids_c.execute("SELECT id FROM ticket ORDER BY id") ticket_ids = ticket_ids_c.fetchall() insert_cursor = db.cursor() copy_data_buffer = StringIO() eta_prediction__start_time = datetime.datetime.now() eta_prediction__done = 0 eta_prediction__total = len(ticket_ids) for ticket_id, in ticket_ids: try: eta_prediction = eta_prediction__start_time + ((datetime.datetime.now() - eta_prediction__start_time) / eta_prediction__done) * eta_prediction__total except ZeroDivisionError: eta_prediction = None self.log.info("Working on (after) %s to (end of) %s for ticket %d/%d - ETA %s", last_snapshot, until, ticket_id, eta_prediction__total, eta_prediction) eta_prediction__done += 1 # set up a dictionary to hold the value of the ticket fields, which will change as we step forward in time ticket_values = proto_values.copy() # populate the "initial" values if last_snapshot: history_date = last_snapshot + datetime.timedelta(days=1) last_snapshot_cursor = db.cursor() # we add ticket fields and history columns otherwise # we don't get previous values such as isclosed columns = ticket_values.keys() + history_columns last_snapshot_cursor.execute("SELECT %s FROM ticket_bi_historical " "WHERE id = %%s AND _snapshottime = %%s " "LIMIT 1 " % ",".join(columns), (ticket_id, last_snapshot)) values = last_snapshot_cursor.fetchone() if not values: self.log.warn("No historical data for ticket %s on %s?", ticket_id, last_snapshot) ticket_values, ticket_created, history_date = calculate_initial_values_for_ticket(ticket_id) else: ticket_values.update(dict(zip(columns, values))) # original storage for custom_fields can only store strings, so pretend we had a string for k, v in ticket_values.items(): # everything wants to be a string as we use COPY to insert to the database if k in custom_fields and not v: ticket_values[k] = '' else: ticket_values[k] = encode_and_escape(v) ticket_values['id'] = str(ticket_id) else: # first time we've run the history capture script ticket_values, ticket_created, history_date = calculate_initial_values_for_ticket(ticket_id) # now we're going to get a list of all the changes that this ticket goes through ticket_changes_cursor = db.cursor() ticket_changes_cursor.execute("SELECT time, field, newvalue " "FROM ticket_change " "WHERE ticket = %%s AND field in (%s) " "AND time >= %%s AND time < %%s " "ORDER BY time " % db.parammarks(len(ticket_values)), [ticket_id] + ticket_values.keys() + [memoized_to_utimestamp(startofday(history_date)), memoized_to_utimestamp(startofnextday(until)), ] ) ticket_changes =[(from_utimestamp(time), field, newvalue) for time, field, newvalue in ticket_changes_cursor] # and then we'll update 'ticket_values' to make a representation of the ticket for the end of each day, and store that into the history database def _calculate_totalhours_on_date(date, cursor): cursor.execute("SELECT SUM(seconds_worked)/3600.0 FROM ticket_time WHERE ticket = %s AND time_started < %s", (ticket_values['id'], memoized_to_timestamp(startofnextday(date)))) result = cursor.fetchone() return result[0] if result else 0 def _calculate_remaininghours_on_date(date, cursor): # find the closest absolute value nextdate = startofnextday(date) cursor.execute("SELECT to_timestamp(time / 1000000), oldvalue FROM ticket_change WHERE " "field = 'remaininghours' AND ticket = %s AND time >= %s ORDER BY time ASC LIMIT 1", (ticket_values['id'], memoized_to_utimestamp(nextdate))) next_known = cursor.fetchone() cursor.execute("SELECT to_timestamp(time / 1000000), newvalue FROM ticket_change WHERE " "field = 'remaininghours' AND ticket = %s AND time < %s ORDER BY time DESC LIMIT 1", (ticket_values['id'], memoized_to_utimestamp(nextdate))) previous_known = cursor.fetchone() cursor.execute("SELECT now(), value FROM ticket_custom WHERE ticket = %s AND name = 'remaininghours'", (ticket_values['id'],)) currently = cursor.fetchone() self.log.debug("Finding remaininghours for end of %s", date) self.log.debug("Previous known value: %s", previous_known) self.log.debug("Current known value: %s", currently) self.log.debug("Next known value: %s", next_known) candidates = [] try: candidates.append((currently[0] - nextdate, currently[0], float(currently[1]))) except (TypeError, ValueError), e: self.log.warning("Invalid float in %s for remaininghours on ticket %s", currently, ticket_values['id']) if next_known: try: candidates.append((next_known[0] - nextdate, next_known[0], float(next_known[1]))) except (TypeError, ValueError), e: self.log.warning("Invalid float for next_known in %s for remaininghours on ticket %s", next_known, ticket_values['id']) if previous_known: try: candidates.append((nextdate - previous_known[0], previous_known[0], float(previous_known[1]))) except (TypeError, ValueError), e: self.log.warning("Invalid float for previous_known in %s for remaininghours on ticket %s", previous_known, ticket_values['id'])