class Nag(object): def __init__(self): super(Nag, self).__init__() self.people = People() self.send_nag_mail = True self.data = {} self.nag_date = None self.white_list = [] self.black_list = [] self.escalation = Escalation(self.people) self.triage_owners = {} self.all_owners = None self.query_params = {} @staticmethod def get_from(): return utils.get_config('auto_nag', 'from', '*****@*****.**') def get_cc(self): cc = self.get_config('cc', None) if cc is None: cc = utils.get_config('auto_nag', 'cc', []) return set(cc) def get_priority(self, bug): tracking = bug[self.tracking] if tracking == 'blocking': return 'high' return 'normal' def filter_bug(self, priority): days = (utils.get_next_release_date() - self.nag_date).days weekday = self.nag_date.weekday() return self.escalation.filter(priority, days, weekday) def get_people(self): return self.people def set_people_to_nag(self, bug, buginfo): return bug def escalate(self, person, priority, **kwargs): days = (utils.get_next_release_date() - self.nag_date).days return self.escalation.get_supervisor(priority, days, person, **kwargs) def add(self, person, bug_data, priority='default', **kwargs): if not self.people.is_mozilla(person): return False manager = self.escalate(person, priority, **kwargs) return self.add_couple(person, manager, bug_data) def add_couple(self, person, manager, bug_data): person = self.people.get_moz_mail(person) if manager in self.data: data = self.data[manager] else: self.data[manager] = data = {} if person in data: data[person].append(bug_data) else: data[person] = [bug_data] return True def nag_template(self): return self.name() + '_nag.html' def get_extra_for_nag_template(self): return {} def columns_nag(self): return None def sort_columns_nag(self): return None def _is_in_list(self, mail, _list): for manager in _list: if self.people.is_under(mail, manager): return True return False def is_under(self, mail): if not self.white_list: if not self.black_list: return True return not self._is_in_list(mail, self.black_list) if not self.black_list: return self._is_in_list(mail, self.white_list) return self._is_in_list(mail, self.white_list) and not self._is_in_list( mail, self.black_list ) def add_triage_owner(self, owner, real_owner=None): if owner not in self.triage_owners: to = real_owner if real_owner is not None else owner self.triage_owners[owner] = self.get_query_url_for_triage_owner(to) def get_query_url_for_triage_owner(self, owner): if self.all_owners is None: self.all_owners = utils.get_triage_owners() params = copy.deepcopy(self.query_params) if 'include_fields' in params: del params['include_fields'] comps = self.all_owners[owner] comps = set(comps) params['component'] = sorted(comps) url = utils.get_bz_search_url(params) return url def organize_nag(self, bugs): columns = self.columns_nag() if columns is None: columns = self.columns() key = self.sort_columns_nag() if key is None: key = self.sort_columns() return utils.organize(bugs, columns, key=key) def send_mails(self, title, dryrun=False): if not self.send_nag_mail: return env = Environment(loader=FileSystemLoader('templates')) common = env.get_template('common.html') login_info = utils.get_login_info() From = Nag.get_from() Default_Cc = self.get_cc() mails = self.prepare_mails() for m in mails: Cc = Default_Cc.copy() if m['manager']: Cc.add(m['manager']) body = common.render(message=m['body'], query_url=None) receivers = set(m['to']) | set(Cc) status = 'Success' try: mail.send( From, sorted(m['to']), title, body, Cc=sorted(Cc), html=True, login=login_info, dryrun=dryrun, ) except: # NOQA logger.exception('Tool {}'.format(self.name())) status = 'Failure' db.Email.add(self.name(), receivers, 'individual', status) def prepare_mails(self): if not self.data: return [] template = self.nag_template() if not template: return [] extra = self.get_extra_for_nag_template() env = Environment(loader=FileSystemLoader('templates')) template = env.get_template(template) mails = [] for manager, info in self.data.items(): data = [] To = sorted(info.keys()) for person in To: bug_data = info[person] data += bug_data if len(To) == 1 and To[0] in self.triage_owners: query_url = self.triage_owners[To[0]] else: query_url = None body = template.render( date=self.nag_date, extra=extra, plural=utils.plural, enumerate=enumerate, data=self.organize_nag(data), nag=True, query_url_nag=query_url, table_attrs=self.get_config('table_attrs'), ) m = {'manager': manager, 'to': set(info.keys()), 'body': body} mails.append(m) return mails def reorganize_to_bag(self, data): return data
class RoundRobin(object): def __init__(self, rr=None, people=None): self.feed(rr=rr) self.nicks = {} self.people = People() if people is None else people def feed(self, rr=None): self.data = {} filenames = {} if rr is None: rr = {} for team, path in utils.get_config('round-robin', "teams", default={}).items(): with open('./auto_nag/scripts/configs/{}'.format(path), 'r') as In: rr[team] = json.load(In) filenames[team] = path # rr is dictionary: # - doc -> documentation # - triagers -> dictionary: Real Name -> {bzmail: bugzilla email, nick: bugzilla nickname} # - components -> dictionary: Product::Component -> strategy name # - strategies: dictionay: {duty en date -> Real Name} # Get all the strategies for each team for team, data in rr.items(): if 'doc' in data: del data['doc'] strategies = {} triagers = data['triagers'] fallback_bzmail = triagers['Fallback']['bzmail'] path = filenames.get(team, '') # collect strategies for pc, strategy in data['components'].items(): strategy_data = data[strategy] if strategy not in strategies: strategies[strategy] = strategy_data # rewrite strategy to have a sorted list of end dates for strat_name, strategy in strategies.items(): if 'doc' in strategy: del strategy['doc'] date_name = [] # end date and real name of the triager for date, name in strategy.items(): # collect the tuple (end_date, bzmail) date = lmdutils.get_date_ymd(date) bzmail = triagers[name]['bzmail'] date_name.append((date, bzmail)) # we sort the list to use bisection to find the triager date_name = sorted(date_name) strategies[strat_name] = { 'dates': [d for d, _ in date_name], 'mails': [m for _, m in date_name], 'fallback': fallback_bzmail, 'filename': path, } # finally self.data is a dictionary: # - Product::Component -> dictionary {dates: sorted list of end date # mails: list # fallback: who to nag when we've nobody # filename: the file containing strategy} for pc, strategy in data['components'].items(): self.data[pc] = strategies[strategy] def get_nick(self, bzmail): if bzmail not in self.nicks: def handler(user): self.nicks[bzmail] = user['nick'] BugzillaUser(user_names=[bzmail], user_handler=handler).wait() return self.nicks[bzmail] def is_mozilla(self, bzmail): return self.people.is_mozilla(bzmail) def get(self, bug, date): pc = '{}::{}'.format(bug['product'], bug['component']) if pc not in self.data: mail = bug['triage_owner'] nick = bug['triage_owner_detail']['nick'] return mail, nick date = lmdutils.get_date_ymd(date) strategy = self.data[pc] dates = strategy['dates'] i = bisect.bisect_left(strategy['dates'], date) if i == len(dates): bzmail = strategy['fallback'] else: bzmail = strategy['mails'][i] nick = self.get_nick(bzmail) return bzmail, nick def get_who_to_nag(self, date): fallbacks = {} date = lmdutils.get_date_ymd(date) days = utils.get_config('round-robin', 'days_to_nag', 7) for pc, strategy in self.data.items(): last_date = strategy['dates'][-1] if (last_date - date ).days <= days and strategy['filename'] not in fallbacks: fallbacks[strategy['filename']] = strategy['fallback'] # create a dict: mozmail -> list of filenames to check res = {} for fn, fb in fallbacks.items(): if not self.is_mozilla(fb): raise BadFallback() mozmail = self.people.get_moz_mail(fb) if mozmail not in res: res[mozmail] = [] res[mozmail].append(fn) res = {fb: sorted(fn) for fb, fn in res.items()} return res
class Calendar(object): def __init__(self, cal, fallback, team_name, people=None): super(Calendar, self).__init__() self.cal = cal self.people = People() if people is None else people self.fallback = fallback self.fb_bzmail = self.people.get_bzmail_from_name(self.fallback) self.fb_mozmail = self.people.get_moz_mail(self.fb_bzmail) self.team_name = team_name self.cache = {} def get_fallback(self): return self.fallback def get_fallback_bzmail(self): if not self.fb_bzmail: raise BadFallback('\'{}\' is an invalid fallback'.format(self.fallback)) return self.fb_bzmail def get_fallback_mozmail(self): if not self.fb_mozmail: raise BadFallback('\'{}\' is an invalid fallback'.format(self.fallback)) return self.fb_mozmail def get_team_name(self): return self.team_name def get_persons(self, date): return [] def set_team(self, team, triagers): self.team = [] for p in team: if p in triagers and 'bzmail' in triagers[p]: bzmail = triagers[p]['bzmail'] else: bzmail = self.people.get_bzmail_from_name(p) self.team.append((p, bzmail)) @staticmethod def get(url, fallback, team_name, people=None): data = None if url.startswith('private://'): name = url.split('//', 1)[1] url = utils.get_private()[name] if url.startswith('http'): r = requests.get(url) data = r.text elif os.path.isfile(url): with open(url, 'r') as In: data = In.read() else: data = url if data is None: raise InvalidCalendar('Cannot read calendar: {}'.format(url)) try: cal = json.loads(data) return JSONCalendar(cal, fallback, team_name, people=people) except JSONDecodeError: try: # there is an issue with dateutil.rrule parser when until doesn't have a tz # so a workaround is to add a Z at the end of the string. pat = re.compile(r'^RRULE:(.*)UNTIL=([0-9Z]+)', re.MULTILINE | re.I) def sub(m): date = m.group(1) if date.lower().endswith('z'): return date return date + 'Z' data = pat.sub(sub, data) return ICSCalendar(data, fallback, team_name, people=people) except ValueError: raise InvalidCalendar('Cannot decode calendar: {}'.format(url))
class Nag(object): def __init__(self): super(Nag, self).__init__() self.people = People() self.send_nag_mail = True self.data = {} self.nag_date = None self.white_list = [] self.black_list = [] self.escalation = Escalation(self.people) self.triage_owners = {} self.all_owners = None self.query_params = {} @staticmethod def get_from(): return utils.get_config('auto_nag', 'from', '*****@*****.**') @staticmethod def get_cc(): return set(utils.get_config('auto_nag', 'cc', [])) def get_priority(self, bug): tracking = bug[self.tracking] if tracking == 'blocking': return 'high' return 'normal' def filter_bug(self, priority): days = (utils.get_next_release_date() - self.nag_date).days weekday = self.nag_date.weekday() return self.escalation.filter(priority, days, weekday) def get_people(self): return self.people def set_people_to_nag(self, bug, buginfo): return bug def escalate(self, person, priority, **kwargs): days = (utils.get_next_release_date() - self.nag_date).days return self.escalation.get_supervisor(priority, days, person, **kwargs) def add(self, person, bug_data, priority='default', **kwargs): if not self.people.is_mozilla(person): return False manager = self.escalate(person, priority, **kwargs) return self.add_couple(person, manager, bug_data) def add_couple(self, person, manager, bug_data): person = self.people.get_moz_mail(person) if manager in self.data: data = self.data[manager] else: self.data[manager] = data = {} if person in data: data[person].append(bug_data) else: data[person] = [bug_data] return True def nag_template(self): return self.name() + '_nag.html' def get_extra_for_nag_template(self): return {} def columns_nag(self): return None def sort_columns_nag(self): return None def _is_in_list(self, mail, _list): for manager in _list: if self.people.is_under(mail, manager): return True return False def is_under(self, mail): if not self.white_list: if not self.black_list: return True return not self._is_in_list(mail, self.black_list) if not self.black_list: return self._is_in_list(mail, self.white_list) return self._is_in_list(mail, self.white_list) and not self._is_in_list( mail, self.black_list ) def add_triage_owner(self, owner, real_owner=None): if owner not in self.triage_owners: to = real_owner if real_owner is not None else owner self.triage_owners[owner] = self.get_query_url_for_triage_owner(to) def get_query_url_for_triage_owner(self, owner): if self.all_owners is None: self.all_owners = utils.get_triage_owners() params = copy.deepcopy(self.query_params) if 'include_fields' in params: del params['include_fields'] comps = self.all_owners[owner] comps = set(comps) params['component'] = sorted(comps) url = utils.get_bz_search_url(params) return url def organize_nag(self, bugs): columns = self.columns_nag() if columns is None: columns = self.columns() key = self.sort_columns_nag() if key is None: key = self.sort_columns() return utils.organize(bugs, columns, key=key) def send_mails(self, title, dryrun=False): if not self.send_nag_mail: return env = Environment(loader=FileSystemLoader('templates')) common = env.get_template('common.html') login_info = utils.get_login_info() From = Nag.get_from() Default_Cc = Nag.get_cc() mails = self.prepare_mails() for m in mails: Cc = Default_Cc.copy() if m['manager']: Cc.add(m['manager']) body = common.render(message=m['body'], query_url=None) receivers = set(m['to']) | set(Cc) status = 'Success' try: mail.send( From, sorted(m['to']), title, body, Cc=sorted(Cc), html=True, login=login_info, dryrun=dryrun, ) except: # NOQA logger.exception('Tool {}'.format(self.name())) status = 'Failure' db.Email.add(self.name(), receivers, 'individual', status) def prepare_mails(self): if not self.data: return [] template = self.nag_template() if not template: return [] extra = self.get_extra_for_nag_template() env = Environment(loader=FileSystemLoader('templates')) template = env.get_template(template) mails = [] for manager, info in self.data.items(): data = [] To = sorted(info.keys()) for person in To: bug_data = info[person] data += bug_data if len(To) == 1 and To[0] in self.triage_owners: query_url = self.triage_owners[To[0]] else: query_url = None body = template.render( date=self.nag_date, extra=extra, plural=utils.plural, enumerate=enumerate, data=self.organize_nag(data), nag=True, query_url_nag=query_url, table_attrs=self.get_config('table_attrs'), ) m = {'manager': manager, 'to': set(info.keys()), 'body': body} mails.append(m) return mails def reorganize_to_bag(self, data): return data
class Calendar(object): def __init__(self, cal, fallback, team_name, people=None): super(Calendar, self).__init__() self.cal = cal self.people = People() if people is None else people self.fallback = fallback self.fb_bzmail = self.people.get_bzmail_from_name(self.fallback) self.fb_mozmail = self.people.get_moz_mail(self.fb_bzmail) self.team_name = team_name self.cache = {} def get_fallback(self): return self.fallback def get_fallback_bzmail(self): if not self.fb_bzmail: raise BadFallback('\'{}\' is an invalid fallback'.format( self.fallback)) return self.fb_bzmail def get_fallback_mozmail(self): if not self.fb_mozmail: raise BadFallback('\'{}\' is an invalid fallback'.format( self.fallback)) return self.fb_mozmail def get_team_name(self): return self.team_name def get_persons(self, date): return [] def set_team(self, team, triagers): self.team = [] for p in team: if p in triagers and 'bzmail' in triagers[p]: bzmail = triagers[p]['bzmail'] else: bzmail = self.people.get_bzmail_from_name(p) self.team.append((p, bzmail)) @staticmethod def get(url, fallback, team_name, people=None): data = None if url.startswith('private://'): name = url.split('//', 1)[1] url = utils.get_private()[name] if url.startswith('http'): r = requests.get(url) data = r.text elif os.path.isfile(url): with open(url, 'r') as In: data = In.read() else: data = url if data is None: raise InvalidCalendar('Cannot read calendar: {}'.format(url)) try: cal = json.loads(data) return JSONCalendar(cal, fallback, team_name, people=people) except JSONDecodeError: try: # there is an issue with dateutil.rrule parser when until doesn't have a tz # so a workaround is to add a Z at the end of the string. pat = re.compile(r'^RRULE:(.*)UNTIL=([0-9Z]+)', re.MULTILINE | re.I) def sub(m): date = m.group(1) if date.lower().endswith('z'): return date return date + 'Z' data = pat.sub(sub, data) return ICSCalendar(data, fallback, team_name, people=people) except ValueError: raise InvalidCalendar('Cannot decode calendar: {}'.format(url))