├── .cvsignore ├── .hgignore ├── TEMPLATE-INFO.txt ├── config.ini.template ├── detectors ├── .cvsignore ├── autoassign.py ├── autonosy.py ├── config.ini.template ├── countauditor.py ├── hgrepo.py ├── irker.py ├── issuestates.py ├── messagesummary.py ├── no_texthtml.py ├── nosyreaction.py.off ├── patches.py ├── priorityauditor.py ├── pull_request.py ├── reopenpending.py ├── rietveldreactor.py ├── sendmail.py.off ├── severityauditor.py ├── spambayes.py ├── statusauditor.py ├── textplain.py └── userauditor.py ├── extensions ├── README.txt ├── create_patch.py ├── jnosy.py ├── local_replace.py ├── oic_login.py ├── openid_login.py ├── pull_request.py ├── pydevutils.py ├── rietveldlink.py ├── search_id.py ├── spambayes.py ├── test │ ├── local_replace_data.txt │ └── test_local_replace.py ├── timestamp.py └── timezone.py ├── html ├── _generic.404.html ├── _generic.calendar.html ├── _generic.collision.html ├── _generic.help-empty.html ├── _generic.help-list.html ├── _generic.help-search.html ├── _generic.help-submit.html ├── _generic.help.html ├── _generic.index.html ├── _generic.item.html ├── _generic.keywords_expr.html ├── bullet.gif ├── button-on-bg.png ├── committer.png ├── favicon.ico ├── file.index.html ├── file.item.html ├── gh-icon.png ├── header-bg2.png ├── help.html ├── help_controls.js ├── home.classlist.html ├── home.html ├── issue.index.html ├── issue.item.html ├── issue.item.js ├── issue.search.html ├── issue.stats.html ├── keyword.index.html ├── keyword.item.html ├── main.css ├── msg.index.html ├── msg.item.html ├── nav-off-bg.png ├── nav-on-bg.png ├── openid-16x16.gif ├── osd.xml ├── page.html ├── patch-icon.png ├── pull_request.item.html ├── python-logo-small.png ├── python-logo.gif ├── query.edit.html ├── query.item.html ├── style.css ├── triager.png ├── user.clacheck.html ├── user.committers.html ├── user.devs.html ├── user.experts.html ├── user.forgotten.html ├── user.help-search.html ├── user.help.html ├── user.index.html ├── user.item.html ├── user.openid.html ├── user.register.html ├── user.rego_progress.html └── user_utils.js ├── initial_data.py ├── lib ├── identify_patch.py └── openid2rp.py ├── rietveld.wsgi ├── schema.py └── scripts ├── addoic ├── addpatchsets ├── adjust_user ├── close-pending ├── fillrevs ├── initrietveld ├── issuestats.py ├── mass_reassign ├── remove_py3k ├── roundup-summary ├── set_counts ├── set_text_plain ├── suggest.py ├── update_email.py └── updatecc /.cvsignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | htmlbase.py 4 | *.cover 5 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | config.ini 3 | db/files 4 | db/text-index 5 | **.pyc 6 | **~ 7 | *.swp 8 | -------------------------------------------------------------------------------- /TEMPLATE-INFO.txt: -------------------------------------------------------------------------------- 1 | Name: python-tracker 2 | Description: This is customisation of the "classic" tracker for the Python 3 | Language developers. 4 | Intended-For: http://dev.python.org/ 5 | 6 | -------------------------------------------------------------------------------- /detectors/.cvsignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.cover 4 | -------------------------------------------------------------------------------- /detectors/autoassign.py: -------------------------------------------------------------------------------- 1 | # Auditor to automatically assign issues to a user when 2 | # the component field gets set 3 | 4 | def autoassign(db, cl, nodeid, newvalues): 5 | try: 6 | components = newvalues['components'] 7 | except KeyError: 8 | # Without components, nothing needs to be auto-assigned 9 | return 10 | if newvalues.has_key('assignee'): 11 | # If there is an explicit assignee in the new values 12 | # (even if it is None, in the case unassignment): 13 | # do nothing 14 | return 15 | # If the issue is already assigned, do nothing 16 | if nodeid and db.issue.get(nodeid, 'assignee'): 17 | return 18 | for component in components: 19 | user = db.component.get(component, 'assign_to') 20 | if user: 21 | # If there would be multiple auto-assigned users 22 | # arbitrarily pick the first one we find 23 | newvalues['assignee'] = user 24 | return 25 | 26 | def init(db): 27 | db.issue.audit('create', autoassign) 28 | db.issue.audit('set', autoassign) 29 | -------------------------------------------------------------------------------- /detectors/autonosy.py: -------------------------------------------------------------------------------- 1 | # This auditor automatically adds users and release managers to the nosy 2 | # list when the component fields gets set and the priority is changed to 3 | # 'release blocker' respectively. 4 | # See also the nosyreaction.py script (they should probably be merged to a 5 | # single script). 6 | 7 | RELEASE_MANAGERS = { 8 | 'Python 2.6': '19', # barry 9 | 'Python 2.7': '4455', # benjamin.peterson 10 | 'Python 3.1': '4455', # benjamin.peterson 11 | 'Python 3.2': '93', # georg.brandl 12 | 'Python 3.3': '93', # georg.brandl 13 | 'Python 3.4': '2731', # larry 14 | 'Python 3.5': '2731', # larry 15 | 'Python 3.6': '5248', # ned.deily 16 | 'Python 3.7': '5248', # ned.deily 17 | 'Python 3.8': '12704', # lukasz.langa 18 | 'Python 3.9': '12704', # lukasz.langa 19 | 'Python 3.10': '26865', # pablogsal 20 | } 21 | 22 | def autonosy(db, cl, nodeid, newvalues): 23 | components = newvalues.get('components', []) 24 | 25 | current_nosy = set() 26 | if 'nosy' in newvalues: 27 | # the nosy list changed 28 | # newvalues['nosy'] contains all the user ids (new and old) 29 | nosy = newvalues.get('nosy', []) 30 | nosy = [value for value in nosy if db.hasnode('user', value)] 31 | current_nosy |= set(nosy) 32 | else: 33 | if nodeid: 34 | # the issue already exists 35 | # get the values that were already in the nosy 36 | old_nosy = db.issue.get(nodeid, 'nosy') 37 | current_nosy |= set(old_nosy) 38 | 39 | # make a copy of the current_nosy where to add the new user ids 40 | new_nosy = set(current_nosy) 41 | 42 | for component in components: 43 | users = db.component.get(component, 'add_as_nosy') 44 | new_nosy |= set(users) 45 | 46 | # get the new values if they changed or the already-set ones if they didn't 47 | if 'priority' in newvalues: 48 | priority_id = newvalues['priority'] 49 | elif nodeid is not None: 50 | priority_id = db.issue.get(nodeid, 'priority') 51 | else: 52 | priority_id = None 53 | priority = 'None' 54 | if priority_id is not None: 55 | priority = db.priority.get(priority_id, 'name') 56 | 57 | versions = [] 58 | if 'versions' in newvalues: 59 | versions = newvalues.get('versions', []) 60 | elif nodeid is not None: 61 | versions = db.issue.get(nodeid, 'versions') 62 | 63 | if priority == 'release blocker': 64 | for version in versions: 65 | name = db.version.get(version, 'name') 66 | if name in RELEASE_MANAGERS: 67 | new_nosy.add(RELEASE_MANAGERS[name]) 68 | 69 | if current_nosy != new_nosy: 70 | # some user ids have been added automatically, so update the nosy 71 | newvalues['nosy'] = list(new_nosy) 72 | 73 | 74 | def init(db): 75 | db.issue.audit('create', autonosy) 76 | db.issue.audit('set', autonosy) 77 | 78 | -------------------------------------------------------------------------------- /detectors/config.ini.template: -------------------------------------------------------------------------------- 1 | #This configuration file controls the behavior of busybody.py and tellteam.py 2 | #The two definitions can be comma-delimited lists of email addresses. 3 | #Be sure these addresses will accept mail from the tracker's email address. 4 | [main] 5 | triage_email = triage@example.com 6 | busybody_email= busybody@example.com 7 | 8 | # URI to XMLRPC server doing the actual spam check. 9 | spambayes_uri = http://localhost/not_here 10 | # These must match the {ham,spam}_cutoff setting in the SpamBayes server 11 | # config. 12 | spambayes_ham_cutoff = 0.2 13 | spambayes_spam_cutoff = 0.85 14 | 15 | spambayes_may_view_spam = User,Coordinator,Developer 16 | spambayes_may_classify = Coordinator 17 | spambayes_may_report_misclassified = User,Coordinator,Developer 18 | 19 | ciavc_server = http://localhost/not_here 20 | 21 | [irker] 22 | channels = irc://chat.freenode.net/python-dev 23 | -------------------------------------------------------------------------------- /detectors/countauditor.py: -------------------------------------------------------------------------------- 1 | 2 | def count_nosy_msg(db, cl, nodeid, newvalues): 3 | ''' Update the counts of messages and nosy users on issue edit''' 4 | if 'nosy' in newvalues: 5 | newvalues['nosy_count'] = len(set(newvalues['nosy'])) 6 | if 'messages' in newvalues: 7 | newvalues['message_count'] = len(set(newvalues['messages'])) 8 | 9 | 10 | def init(db): 11 | # Should run after the creator and auto-assignee are added 12 | db.issue.audit('create', count_nosy_msg, priority=120) 13 | db.issue.audit('set', count_nosy_msg, priority=120) 14 | -------------------------------------------------------------------------------- /detectors/hgrepo.py: -------------------------------------------------------------------------------- 1 | # Auditor for hgrepo records 2 | # Split repo#branch URLs in repo and branch 3 | 4 | def hgsplit(db, cl, nodeid, newvalues): 5 | url = newvalues.get('url','') 6 | if '#' in url: 7 | url, branch = url.split('#', 1) 8 | newvalues['url'] = url 9 | newvalues['patchbranch'] = branch 10 | 11 | def init(db): 12 | db.hgrepo.audit('create', hgsplit) 13 | -------------------------------------------------------------------------------- /detectors/irker.py: -------------------------------------------------------------------------------- 1 | # Detector for sending changes to irker 2 | # Written by Ezio Melotti 3 | 4 | import re 5 | import json 6 | import socket 7 | 8 | IRKER_HOST = 'localhost' 9 | IRKER_PORT = 6659 10 | 11 | max_content = 120 12 | 13 | TEMPLATE = ('%(green)s%(author)s%(reset)s ' 14 | '%(bluish)s#%(nodeid)s%(reset)s/%(title)s%(bold)s:%(bold)s ' 15 | '%(log)s %(url)s') 16 | 17 | 18 | def sendmsg(msg): 19 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 20 | try: 21 | sock.connect((IRKER_HOST, IRKER_PORT)) 22 | sock.sendall(msg + "\n") 23 | finally: 24 | sock.close() 25 | 26 | 27 | def notify_irker(db, cl, nodeid, oldvalues): 28 | messages = set(cl.get(nodeid, 'messages')) 29 | if oldvalues: 30 | messages -= set(oldvalues.get('messages',())) 31 | if not messages: 32 | return 33 | messages = list(messages) 34 | 35 | if oldvalues: 36 | oldstatus = oldvalues['status'] 37 | else: 38 | oldstatus = None 39 | newstatus = db.issue.get(nodeid, 'status') 40 | if oldstatus != newstatus: 41 | if oldvalues: 42 | status = db.status.get(newstatus, 'name') 43 | else: 44 | status = 'new' 45 | log = '[' + status + '] ' 46 | else: 47 | log = '' 48 | for msg in messages: 49 | log += db.msg.get(msg, 'content') 50 | if len(log) > max_content: 51 | log = log[:max_content-3] + '...' 52 | log = re.sub('\s+', ' ', log) 53 | 54 | # include irc colors 55 | params = { 56 | 'bold': '\x02', 57 | 'green': '\x0303', 58 | 'blue': '\x0302', 59 | 'bluish': '\x0310', 60 | 'yellow': '\x0307', 61 | 'brown': '\x0305', 62 | 'reset': '\x0F' 63 | } 64 | # extend with values used in the template 65 | params['author'] = db.user.get(db.getuid(), 'username') 66 | params['nodeid'] = nodeid 67 | params['title'] = db.issue.get(nodeid, 'title') 68 | params['log'] = log 69 | params['url'] = '%sissue%s' % (db.config.TRACKER_WEB, nodeid) 70 | 71 | # create the message and use the list of channels defined in 72 | # detectors/config.ini 73 | msg = json.dumps({ 74 | 'to': db.config.detectors.IRKER_CHANNELS.split(','), 75 | 'privmsg': TEMPLATE % params, 76 | }) 77 | 78 | try: 79 | sendmsg(msg) 80 | except Exception as e: 81 | # Ignore any errors in sending the irker; 82 | # if the server is down, that's just bad luck 83 | # XXX might want to do some logging here 84 | print '* Sending message to irker failed', str(e) 85 | 86 | def init(db): 87 | db.issue.react('create', notify_irker) 88 | db.issue.react('set', notify_irker) 89 | -------------------------------------------------------------------------------- /detectors/issuestates.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Sets the 'stage' field to 'resolved' when an issue is closed. 3 | * Sets the 'stage' field to 'patch review' and adds 'patch' to the 'keywords' field. 4 | """ 5 | 6 | 7 | def issuestates(db, cl, nodeid, newvalues): 8 | status_change = newvalues.get('status') 9 | status_close = status_change and status_change == db.status.lookup('closed') 10 | 11 | if status_close: 12 | if newvalues.get('stage') is None: 13 | newvalues['stage'] = db.stage.lookup('resolved') 14 | 15 | is_open = cl.get(nodeid, 'status') == db.status.lookup('open') 16 | patch_keyword = db.keyword.lookup('patch') 17 | old_keywords = cl.get(nodeid, 'keywords') 18 | new_keywords = newvalues.get('keywords', []) 19 | old_prs = cl.get(nodeid, 'pull_requests') 20 | new_prs = newvalues.get('pull_requests', []) 21 | pr_change = len(new_prs) > len(old_prs) 22 | patch_review = db.stage.lookup('patch review') 23 | needs_change = is_open and pr_change and newvalues.get('stage') != patch_review 24 | if needs_change: 25 | newvalues['stage'] = patch_review 26 | if patch_keyword not in new_keywords and patch_keyword not in old_keywords: 27 | set_new_keywords = old_keywords[:] 28 | set_new_keywords.extend(new_keywords) 29 | set_new_keywords.append(patch_keyword) 30 | newvalues['keywords'] = set_new_keywords 31 | 32 | 33 | def init(db): 34 | db.issue.audit('set', issuestates) 35 | -------------------------------------------------------------------------------- /detectors/messagesummary.py: -------------------------------------------------------------------------------- 1 | #$Id: messagesummary.py,v 1.1 2003/04/17 03:26:38 richard Exp $ 2 | 3 | from roundup.mailgw import parseContent 4 | 5 | def summarygenerator(db, cl, nodeid, newvalues): 6 | ''' If the message doesn't have a summary, make one for it. 7 | ''' 8 | if newvalues.has_key('summary') or not newvalues.has_key('content'): 9 | return 10 | 11 | summary, content = parseContent(newvalues['content'], config=db.config) 12 | newvalues['summary'] = summary 13 | 14 | 15 | def init(db): 16 | # fire before changes are made 17 | db.msg.audit('create', summarygenerator) 18 | 19 | # vim: set filetype=python ts=4 sw=4 et si 20 | -------------------------------------------------------------------------------- /detectors/no_texthtml.py: -------------------------------------------------------------------------------- 1 | 2 | def audit_html_files(db, cl, nodeid, newvalues): 3 | if newvalues.has_key('type') and newvalues['type'] in ('text/html', 'html', 'text/x-html'): 4 | newvalues['type'] = 'text/plain' 5 | 6 | 7 | def init(db): 8 | db.file.audit('set', audit_html_files) 9 | db.file.audit('create', audit_html_files) 10 | -------------------------------------------------------------------------------- /detectors/nosyreaction.py.off: -------------------------------------------------------------------------------- 1 | from roundup import roundupdb, hyperdb 2 | 3 | def updatenosy(db, cl, nodeid, newvalues): 4 | '''Update the nosy list for changes to the assignee 5 | ''' 6 | # nodeid will be None if this is a new node 7 | current_nosy = set() 8 | if nodeid is None: 9 | ok = ('new', 'yes') 10 | else: 11 | ok = ('yes',) 12 | # old node, get the current values from the node if they haven't 13 | # changed 14 | if not newvalues.has_key('nosy'): 15 | nosy = cl.get(nodeid, 'nosy') 16 | for value in nosy: 17 | current_nosy.add(value) 18 | 19 | # if the nosy list changed in this transaction, init from the new 20 | # value 21 | if newvalues.has_key('nosy'): 22 | nosy = newvalues.get('nosy', []) 23 | for value in nosy: 24 | if not db.hasnode('user', value): 25 | continue 26 | current_nosy.add(value) 27 | 28 | new_nosy = set(current_nosy) 29 | 30 | # add assignee(s) to the nosy list 31 | if newvalues.has_key('assignee') and newvalues['assignee'] is not None: 32 | propdef = cl.getprops() 33 | if isinstance(propdef['assignee'], hyperdb.Link): 34 | assignee_ids = [newvalues['assignee']] 35 | elif isinstance(propdef['assignee'], hyperdb.Multilink): 36 | assignee_ids = newvalues['assignee'] 37 | for assignee_id in assignee_ids: 38 | new_nosy.add(assignee_id) 39 | 40 | # see if there's any new messages - if so, possibly add the author and 41 | # recipient to the nosy 42 | if newvalues.has_key('messages'): 43 | if nodeid is None: 44 | ok = ('new', 'yes') 45 | messages = newvalues['messages'] 46 | else: 47 | ok = ('yes',) 48 | # figure which of the messages now on the issue weren't 49 | oldmessages = cl.get(nodeid, 'messages') 50 | messages = [] 51 | for msgid in newvalues['messages']: 52 | if msgid not in oldmessages: 53 | messages.append(msgid) 54 | 55 | # configs for nosy modifications 56 | add_author = getattr(db.config, 'ADD_AUTHOR_TO_NOSY', 'new') 57 | add_recips = getattr(db.config, 'ADD_RECIPIENTS_TO_NOSY', 'new') 58 | 59 | # now for each new message: 60 | msg = db.msg 61 | for msgid in messages: 62 | if add_author in ok: 63 | authid = msg.get(msgid, 'author') 64 | new_nosy.add(authid) 65 | 66 | # add on the recipients of the message 67 | if add_recips in ok: 68 | for recipient in msg.get(msgid, 'recipients'): 69 | new_nosy.add(recipient) 70 | 71 | # add creator of PR to the nosy list 72 | if 'pull_requests' in newvalues: 73 | if nodeid is not None: 74 | current_prs = cl.get(nodeid, 'pull_requests') 75 | else: 76 | current_prs = [] 77 | prs = newvalues['pull_requests'] 78 | # we only care about when a new PR is linked, so don't do 79 | # anything when a PR is unlinked 80 | if len(prs) > len(current_prs): 81 | # we only want to use the latest linked PR 82 | latest_pr = prs[-1] 83 | probj = db.pull_request.getnode(latest_pr) 84 | if probj: 85 | new_nosy.add(probj.creator) 86 | 87 | if current_nosy != new_nosy: 88 | # that's it, save off the new nosy list 89 | newvalues['nosy'] = list(new_nosy) 90 | 91 | def addcreator(db, cl, nodeid, newvalues): 92 | assert None == nodeid, "addcreator called for existing node" 93 | nosy = newvalues.get('nosy', []) 94 | if not db.getuid() in nosy: 95 | nosy.append(db.getuid()) 96 | newvalues['nosy'] = nosy 97 | 98 | 99 | def init(db): 100 | db.issue.audit('create', updatenosy) 101 | db.issue.audit('set', updatenosy) 102 | 103 | # Make sure creator of issue is added. Do this after 'updatenosy'. 104 | db.issue.audit('create', addcreator, priority=110) 105 | -------------------------------------------------------------------------------- /detectors/patches.py: -------------------------------------------------------------------------------- 1 | # Auditor for patch files 2 | # Patches should be declared as text/plain (also .py files), 3 | # independent of what the browser says, and 4 | # the "patch" keyword should get set automatically. 5 | 6 | import posixpath 7 | import identify_patch 8 | 9 | patchtypes = ('.diff', '.patch') 10 | sourcetypes = ('.diff', '.patch', '.py') 11 | 12 | def ispatch(file, types): 13 | return posixpath.splitext(file)[1] in types 14 | 15 | def patches_text_plain(db, cl, nodeid, newvalues): 16 | if ispatch(newvalues['name'], sourcetypes): 17 | newvalues['type'] = 'text/plain' 18 | 19 | def patches_keyword(db, cl, nodeid, newvalues): 20 | # Check whether there are any new files 21 | newfiles = set(newvalues.get('files',())) 22 | if nodeid: 23 | newfiles -= set(db.issue.get(nodeid, 'files')) 24 | # Check whether any of these is a patch 25 | newpatch = False 26 | for fileid in newfiles: 27 | if ispatch(db.file.get(fileid, 'name'), patchtypes): 28 | newpatch = True 29 | break 30 | if newpatch: 31 | # Add the patch keyword if its not already there 32 | patchid = db.keyword.lookup("patch") 33 | oldkeywords = [] 34 | if nodeid: 35 | oldkeywords = db.issue.get(nodeid, 'keywords') 36 | if patchid in oldkeywords: 37 | # This is already marked as a patch 38 | return 39 | if not newvalues.has_key('keywords'): 40 | newvalues['keywords'] = oldkeywords 41 | if patchid not in newvalues['keywords']: 42 | newvalues['keywords'].append(patchid) 43 | 44 | def patch_revision(db, cl, nodeid, oldvalues): 45 | # there shouldn't be old values 46 | assert not oldvalues 47 | if not ispatch(cl.get(nodeid, 'name'), patchtypes): 48 | return 49 | revno = identify_patch.identify(db, cl.get(nodeid, 'content')) 50 | if revno: 51 | cl.set(nodeid, revision=str(revno)) 52 | 53 | def init(db): 54 | db.file.audit('create', patches_text_plain) 55 | db.file.react('create', patch_revision) 56 | db.issue.audit('create', patches_keyword) 57 | db.issue.audit('set', patches_keyword) 58 | -------------------------------------------------------------------------------- /detectors/priorityauditor.py: -------------------------------------------------------------------------------- 1 | def init_priority(db, cl, nodeid, newvalues): 2 | """ Make sure the priority is set on new issues""" 3 | 4 | if newvalues.has_key('priority') and newvalues['priority']: 5 | return 6 | 7 | normal = db.priority.lookup('normal') 8 | newvalues['priority'] = normal 9 | 10 | def init(db): 11 | db.issue.audit('create', init_priority) 12 | -------------------------------------------------------------------------------- /detectors/pull_request.py: -------------------------------------------------------------------------------- 1 | # Auditor for GitHub URLs 2 | # Check if it is a valid GitHub Pull Request URL and extract PR number 3 | 4 | import re 5 | 6 | 7 | repo_number_re = re.compile(r'^#?(?P\d+)$') 8 | url_re = re.compile(r'(https?:\\)?github\.com/python/cpython/pull/(?P\d+)') 9 | 10 | def validate_pr_uniqueness(db, cl, nodeid, newvalues): 11 | """ 12 | Verifies if newly added PR isn't already attached to an issue. 13 | This process is a 2-level action, first a pull_request object is created, which 14 | goes through validate_pr_number to extract the PR number in case an URL is passed, 15 | only then we validate PR uniqueness within a single issue. 16 | """ 17 | newprs = set(newvalues.get('pull_requests',())) 18 | if not newprs: 19 | return 20 | oldprs = set() 21 | if nodeid: 22 | # if this is an existing issue, get the list of existing prs 23 | oldprs = set(db.issue.get(nodeid, 'pull_requests')) 24 | newprs -= oldprs 25 | try: 26 | # get the newly created PR number 27 | number = db.pull_request.get(newprs.pop(), 'number') 28 | except KeyError: 29 | return 30 | # and compare with those already attached to an issue 31 | for oldpr in oldprs: 32 | oldnumber = db.pull_request.get(oldpr, 'number') 33 | if number == oldnumber: 34 | raise ValueError("GitHub PR already added to issue") 35 | 36 | def validate_pr_number(db, cl, nodeid, newvalues): 37 | try: 38 | number = extract_number(newvalues['number']) 39 | if number: 40 | newvalues['number'] = number 41 | except KeyError: 42 | pass 43 | 44 | def extract_number(input): 45 | """ 46 | Extracts PR number from the following forms: 47 | - #number 48 | - number 49 | - full url 50 | and returns its number. 51 | """ 52 | # try matching just the number 53 | repo_number_match = repo_number_re.search(input) 54 | if repo_number_match: 55 | return repo_number_match.group('number') 56 | # fallback to parsing the entire url 57 | url_match = url_re.search(input) 58 | if url_match: 59 | return url_match.group('number') 60 | # if nothing else raise error 61 | raise ValueError("Unknown PR format, acceptable formats are: " 62 | "full github URL, #pr_number, pr_number") 63 | 64 | 65 | def init(db): 66 | db.issue.audit('create', validate_pr_uniqueness) 67 | db.issue.audit('set', validate_pr_uniqueness) 68 | db.pull_request.audit('create', validate_pr_number) 69 | db.pull_request.audit('set', validate_pr_number) 70 | -------------------------------------------------------------------------------- /detectors/reopenpending.py: -------------------------------------------------------------------------------- 1 | def reopen_pending(db, cl, nodeid, newvalues): 2 | """Re-open pending issues when the issue is updated.""" 3 | 4 | if newvalues.has_key('status'): return 5 | 6 | if nodeid is None: oldStatus = None 7 | else: oldStatus = cl.get(nodeid, 'status') 8 | if oldStatus == db.status.lookup('pending'): 9 | newvalues['status'] = db.status.lookup('open') 10 | 11 | 12 | def init(db): 13 | # fire before changes are made 14 | db.issue.audit('set', reopen_pending) 15 | -------------------------------------------------------------------------------- /detectors/rietveldreactor.py: -------------------------------------------------------------------------------- 1 | import cPickle, base64 2 | 3 | # ListProperty is initialized to the cPickle of an empty list 4 | empty_list = base64.encodestring(cPickle.dumps([])) 5 | 6 | def create_django_user(db, cl, nodeid, oldvalues): 7 | username = cl.get(nodeid, 'username') 8 | email = cl.get(nodeid, 'address') 9 | if email is None: 10 | email = '' 11 | c = db.cursor 12 | # django.contrib.auth.models.UNUSABLE_PASSWORD=='!' 13 | c.execute("insert into auth_user(id, username, email, password, first_name, last_name, " 14 | "is_staff, is_active, is_superuser, last_login, date_joined) " 15 | "values(%s, %s, %s, '!', '', '', false, true, false, now(), now())", 16 | (nodeid, username, email)) 17 | 18 | def update_django_user(db, cl, nodeid, oldvalues): 19 | user = nodeid 20 | oldname = oldvalues['username'] 21 | newname = cl.get(nodeid, 'username') 22 | if oldname != newname: 23 | c = db.cursor 24 | c.execute("update auth_user set username=%s where id=%s", (newname, user)) 25 | 26 | old = oldvalues['address'].decode('ascii') 27 | new = cl.get(nodeid, 'address').decode('ascii') 28 | if old != new: 29 | c = db.cursor 30 | c.execute('update auth_user set email=%s where id=%s', (new, user)) 31 | c.execute('update codereview_account set email=%s where id=%s', (new, user)) 32 | # find issues where user is on nosy 33 | c.execute('select nodeid,cc from issue_nosy, codereview_issue ' 34 | 'where linkid=%s and nodeid=id', (user,)) 35 | for issue, cc in c.fetchall(): 36 | cc = cPickle.loads(base64.decodestring(cc)) 37 | try: 38 | cc[cc.index(old)] = new 39 | except ValueError: 40 | cc.append(new) 41 | cc = base64.encodestring(cPickle.dumps(cc)) 42 | c.execute('update codereview_issue set cc=%s where id=%s', (cc, issue)) 43 | 44 | 45 | def update_issue_cc(db, cl, nodeid, oldvalues): 46 | if 'nosy' not in oldvalues: 47 | return 48 | c = db.cursor 49 | c.execute("select count(*) from codereview_issue where id=%s", (nodeid,)) 50 | if c.fetchone()[0] == 0: 51 | return 52 | cc = [] 53 | for user in db.issue.get(nodeid, 'nosy'): 54 | cc.append(db.user.get(user, 'address')) 55 | cc = base64.encodestring(cPickle.dumps(cc)) 56 | c.execute("update codereview_issue set cc=%s where id=%s", (cc, nodeid)) 57 | 58 | def init(db): 59 | c = db.cursor 60 | c.execute("select table_name from information_schema.tables where table_name='auth_user'") 61 | if not c.fetchall(): 62 | # Rietveld tables not present 63 | return 64 | db.user.react('create', create_django_user) 65 | db.user.react('set', update_django_user) 66 | db.issue.react('set', update_issue_cc) 67 | # XXX react to email changes, roles 68 | # XXX react to subject, closed changes on issues 69 | -------------------------------------------------------------------------------- /detectors/sendmail.py.off: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from roundup import roundupdb 4 | 5 | def determineNewMessages(cl, nodeid, oldvalues): 6 | ''' Figure a list of the messages that are being added to the given 7 | node in this transaction. 8 | ''' 9 | messages = [] 10 | if oldvalues is None: 11 | # the action was a create, so use all the messages in the create 12 | messages = cl.get(nodeid, 'messages') 13 | elif oldvalues.has_key('messages'): 14 | # the action was a set (so adding new messages to an existing issue) 15 | m = {} 16 | for msgid in oldvalues['messages']: 17 | m[msgid] = 1 18 | messages = [] 19 | # figure which of the messages now on the issue weren't there before 20 | for msgid in cl.get(nodeid, 'messages'): 21 | if not m.has_key(msgid): 22 | messages.append(msgid) 23 | return messages 24 | 25 | 26 | def is_spam(db, msgid): 27 | """Return true if message has a spambayes score above 28 | db.config.detectors['SPAMBAYES_SPAM_CUTOFF']. Also return true if 29 | msgid is None, which happens when there are no messages (i.e., a 30 | property-only change)""" 31 | if not msgid: 32 | return False 33 | cutoff_score = float(db.config.detectors['SPAMBAYES_SPAM_CUTOFF']) 34 | 35 | msg = db.getnode("msg", msgid) 36 | if msg.has_key('spambayes_score') and \ 37 | msg['spambayes_score'] > cutoff_score: 38 | return True 39 | return False 40 | 41 | 42 | def extract_pr_number(changenote): 43 | """ 44 | Extract PR number from *changenote*. 45 | 46 | Example input (reformatted for brevity): 47 | 48 | '\n----------\n' 49 | 'message_count: 2.0 -> 3.0\n' 50 | 'pull_requests: +20\n 51 | 'versions: +Python 3.1' 52 | """ 53 | match = re.search(r'pull_requests: \+(\d+)', changenote, flags=re.M|re.S) 54 | if match is None: 55 | return None 56 | return match.group(1) 57 | 58 | 59 | def sendmail(db, cl, nodeid, oldvalues): 60 | """Send mail to various recipients, when changes occur: 61 | 62 | * For all changes (property-only, or with new message), send mail 63 | to all e-mail addresses defined in 64 | db.config.detectors['BUSYBODY_EMAIL'] 65 | 66 | * For all changes (property-only, or with new message), send mail 67 | to all members of the nosy list. 68 | 69 | * For new issues, and only for new issue, send mail to 70 | db.config.detectors['TRIAGE_EMAIL'] 71 | 72 | """ 73 | 74 | sendto = [] 75 | 76 | # The busybody addresses always get mail. 77 | try: 78 | sendto += db.config.detectors['BUSYBODY_EMAIL'].split(",") 79 | except KeyError: 80 | pass 81 | 82 | # New submission? 83 | if oldvalues == None: 84 | changenote = cl.generateCreateNote(nodeid) 85 | try: 86 | # Add triage addresses 87 | sendto += db.config.detectors['TRIAGE_EMAIL'].split(",") 88 | except KeyError: 89 | pass 90 | oldfiles = [] 91 | oldmsglist = [] 92 | else: 93 | changenote = cl.generateChangeNote(nodeid, oldvalues) 94 | oldfiles = oldvalues.get('files', []) 95 | oldmsglist = oldvalues.get('messages', []) 96 | 97 | lines = changenote.splitlines() 98 | 99 | pr_number = extract_pr_number(changenote) 100 | if pr_number is not None: 101 | pr_number = db.pull_request.get(pr_number, 'number') 102 | if pr_number: 103 | lines.append( 104 | 'pull_request: https://github.com/python/cpython/pull/%s' % pr_number 105 | ) 106 | 107 | # Silence nosy_count/message_count/pull_requests 108 | changenote = '\n'.join( 109 | line for line in lines if '_count' not in line or 110 | # Don't strip it if we couldn't find PR in the database. 111 | (pr_number and'pull_requests' not in line) 112 | ) 113 | 114 | newfiles = db.issue.get(nodeid, 'files', []) 115 | if oldfiles != newfiles: 116 | added = [fid for fid in newfiles if fid not in oldfiles] 117 | removed = [fid for fid in oldfiles if fid not in newfiles] 118 | filemsg = "" 119 | 120 | for fid in added: 121 | url = db.config.TRACKER_WEB + "file%s/%s" % \ 122 | (fid, db.file.get(fid, "name")) 123 | changenote+="\nAdded file: %s" % url 124 | for fid in removed: 125 | url = db.config.TRACKER_WEB + "file%s/%s" % \ 126 | (fid, db.file.get(fid, "name")) 127 | changenote+="\nRemoved file: %s" % url 128 | 129 | # detect if any of the messages has been removed 130 | newmsglist = db.issue.get(nodeid, 'messages', []) 131 | for msgid in set(oldmsglist)-set(newmsglist): 132 | url = db.config.TRACKER_WEB + "msg%s" % msgid 133 | changenote += "\nRemoved message: %s" % url 134 | 135 | new_messages = determineNewMessages(cl, nodeid, oldvalues) 136 | 137 | # Make sure we send a nosy mail even for property-only 138 | # changes. 139 | if not new_messages: 140 | new_messages = [None] 141 | 142 | for msgid in [msgid for msgid in new_messages if not is_spam(db, msgid)]: 143 | try: 144 | if sendto: 145 | cl.send_message(nodeid, msgid, changenote, sendto) 146 | nosymessage(db, nodeid, msgid, oldvalues, changenote) 147 | except roundupdb.MessageSendError, message: 148 | raise roundupdb.DetectorError, message 149 | 150 | def nosymessage(db, nodeid, msgid, oldvalues, note, 151 | whichnosy='nosy', 152 | from_address=None, cc=[], bcc=[]): 153 | """Send a message to the members of an issue's nosy list. 154 | 155 | The message is sent only to users on the nosy list who are not 156 | already on the "recipients" list for the message. 157 | 158 | These users are then added to the message's "recipients" list. 159 | 160 | If 'msgid' is None, the message gets sent only to the nosy 161 | list, and it's called a 'System Message'. 162 | 163 | The "cc" argument indicates additional recipients to send the 164 | message to that may not be specified in the message's recipients 165 | list. 166 | 167 | The "bcc" argument also indicates additional recipients to send the 168 | message to that may not be specified in the message's recipients 169 | list. These recipients will not be included in the To: or Cc: 170 | address lists. 171 | """ 172 | if msgid: 173 | authid = db.msg.get(msgid, 'author') 174 | recipients = db.msg.get(msgid, 'recipients', []) 175 | else: 176 | # "system message" 177 | authid = None 178 | recipients = [] 179 | 180 | sendto = [] 181 | bcc_sendto = [] 182 | seen_message = {} 183 | for recipient in recipients: 184 | seen_message[recipient] = 1 185 | 186 | def add_recipient(userid, to): 187 | # make sure they have an address 188 | address = db.user.get(userid, 'address') 189 | if address: 190 | to.append(address) 191 | recipients.append(userid) 192 | 193 | def good_recipient(userid): 194 | # Make sure we don't send mail to either the anonymous 195 | # user or a user who has already seen the message. 196 | return (userid and 197 | (db.user.get(userid, 'username') != 'anonymous') and 198 | not seen_message.has_key(userid)) 199 | 200 | # possibly send the message to the author, as long as they aren't 201 | # anonymous 202 | if (good_recipient(authid) and 203 | (db.config.MESSAGES_TO_AUTHOR == 'yes' or 204 | (db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues))): 205 | add_recipient(authid, sendto) 206 | 207 | if authid: 208 | seen_message[authid] = 1 209 | 210 | # now deal with the nosy and cc people who weren't recipients. 211 | for userid in cc + db.issue.get(nodeid, whichnosy): 212 | if good_recipient(userid): 213 | add_recipient(userid, sendto) 214 | 215 | # now deal with bcc people. 216 | for userid in bcc: 217 | if good_recipient(userid): 218 | add_recipient(userid, bcc_sendto) 219 | 220 | # If we have new recipients, update the message's recipients 221 | # and send the mail. 222 | if sendto or bcc_sendto: 223 | if msgid is not None: 224 | db.msg.set(msgid, recipients=recipients) 225 | db.issue.send_message(nodeid, msgid, note, sendto, from_address, 226 | bcc_sendto) 227 | 228 | 229 | def init(db): 230 | db.issue.react('set', sendmail) 231 | db.issue.react('create', sendmail) 232 | -------------------------------------------------------------------------------- /detectors/severityauditor.py: -------------------------------------------------------------------------------- 1 | 2 | def init_severity(db, cl, nodeid, newvalues): 3 | """Make sure severity is set on new issues""" 4 | if newvalues.has_key('severity') and newvalues['severity']: 5 | return 6 | 7 | normal = db.severity.lookup('normal') 8 | newvalues['severity'] = normal 9 | 10 | def init(db): 11 | db.issue.audit('create', init_severity) 12 | -------------------------------------------------------------------------------- /detectors/spambayes.py: -------------------------------------------------------------------------------- 1 | 2 | import xmlrpclib 3 | import socket 4 | import time 5 | import math 6 | import re 7 | 8 | from roundup.exceptions import Reject 9 | 10 | REVPAT = re.compile(r'(r[0-9]+\b|rev(ision)? [0-9]+\b)') 11 | 12 | def extract_classinfo(db, klass, nodeid, newvalues): 13 | if None == nodeid: 14 | node = newvalues 15 | content = newvalues['content'] 16 | else: 17 | node = db.getnode(klass.classname, nodeid) 18 | content = klass.get(nodeid, 'content') 19 | 20 | if node.has_key('creation') or node.has_key('date'): 21 | nodets = node.get('creation', node.get('date')).timestamp() 22 | else: 23 | nodets = time.time() 24 | 25 | if node.has_key('author') or node.has_key('creator'): 26 | authorid = node.get('author', node.get('creator')) 27 | else: 28 | authorid = db.getuid() 29 | 30 | authorage = nodets - db.getnode('user', authorid)['creation'].timestamp() 31 | 32 | tokens = ["klass:%s" % klass.classname, 33 | "author:%s" % authorid, 34 | "authorage:%d" % int(math.log(authorage)), 35 | "hasrev:%s" % (REVPAT.search(content) is not None)] 36 | 37 | 38 | return (content, tokens) 39 | 40 | def check_spambayes(db, content, tokens): 41 | try: 42 | spambayes_uri = db.config.detectors['SPAMBAYES_URI'] 43 | except KeyError, e: 44 | return (False, str(e)) 45 | 46 | try: 47 | server = xmlrpclib.ServerProxy(spambayes_uri, verbose=False) 48 | except IOError, e: 49 | return (False, str(e)) 50 | 51 | 52 | try: 53 | prob = server.score({'content':content}, tokens, {}) 54 | return (True, prob) 55 | except (socket.error, xmlrpclib.Error), e: 56 | return (False, str(e)) 57 | 58 | 59 | def check_spam(db, klass, nodeid, newvalues): 60 | """Auditor to score a website submission.""" 61 | 62 | 63 | if newvalues.has_key('spambayes_score'): 64 | if not db.security.hasPermission('SB: May Classify', db.getuid()): 65 | raise ValueError, "You don't have permission to spamclassify messages" 66 | # Don't do anything if we're explicitly setting the score 67 | return 68 | 69 | if not newvalues.has_key('content'): 70 | # No need to invoke spambayes if the content of the message 71 | # is unchanged. 72 | return 73 | 74 | (content, tokens) = extract_classinfo(db, klass, nodeid, newvalues) 75 | (success, other) = check_spambayes(db, content, tokens) 76 | if success: 77 | newvalues['spambayes_score'] = other 78 | newvalues['spambayes_misclassified'] = False 79 | else: 80 | newvalues['spambayes_score'] = -1 81 | newvalues['spambayes_misclassified'] = True 82 | 83 | def init(database): 84 | """Initialize auditor.""" 85 | database.msg.audit('create', check_spam) 86 | database.msg.audit('set', check_spam) 87 | database.file.audit('create', check_spam) 88 | database.file.audit('set', check_spam) 89 | -------------------------------------------------------------------------------- /detectors/statusauditor.py: -------------------------------------------------------------------------------- 1 | def init_status(db, cl, nodeid, newvalues): 2 | """ Make sure the status is set on new issues""" 3 | 4 | if newvalues.has_key('status') and newvalues['status']: 5 | return 6 | 7 | new_id = db.status.lookup('open') 8 | newvalues['status'] = new_id 9 | 10 | 11 | def block_resolution(db, cl, nodeid, newvalues): 12 | """ If the issue has blockers, don't allow it to be resolved.""" 13 | 14 | if nodeid is None: 15 | dependencies = [] 16 | else: 17 | dependencies = cl.get(nodeid, 'dependencies') 18 | dependencies = newvalues.get('dependencies', dependencies) 19 | 20 | # Check which dependencies are still open 21 | closed = db.status.lookup('closed') 22 | dep = [nid for nid in dependencies if cl.get(nid,'status') != closed] 23 | dependencies = dep 24 | 25 | # don't do anything if there's no open blockers or the status hasn't 26 | # changed 27 | if not dependencies or not newvalues.has_key('status'): 28 | return 29 | 30 | # format the info 31 | u = db.config.TRACKER_WEB 32 | s = ', '.join(['%s'%(u,id,id) for id in dependencies]) 33 | if len(dependencies) == 1: 34 | s = 'issue %s is'%s 35 | else: 36 | s = 'issues %s are'%s 37 | 38 | # ok, see if we're trying to resolve 39 | if newvalues.get('status') and newvalues['status'] == closed: 40 | raise ValueError, "This issue can't be closed until %s closed."%s 41 | 42 | 43 | def resolve(db, cl, nodeid, newvalues): 44 | """Make sure status, resolution, and superseder values match.""" 45 | 46 | status_change = newvalues.get('status') 47 | status_close = status_change and newvalues['status'] == db.status.lookup('closed') 48 | 49 | # Make sure resolution and superseder get only set when status->close 50 | if not status_change or not status_close: 51 | if newvalues.get('resolution') or newvalues.get('superseder'): 52 | raise ValueError, "resolution and superseder must only be set when a issue is closed" 53 | 54 | # Make sure resolution is set when status->close 55 | if status_close: 56 | if not newvalues.get('resolution'): 57 | raise ValueError, "resolution must be set when a issue is closed" 58 | 59 | # Make sure superseder is set when resolution->duplicate 60 | if newvalues['resolution'] == db.resolution.lookup('duplicate'): 61 | if not newvalues.get('superseder'): 62 | raise ValueError, "please provide a superseder when closing a issue as 'duplicate'" 63 | 64 | 65 | 66 | def resolve_dependencies(db, cl, nodeid, oldvalues): 67 | """ When we resolve an issue that's a blocker, remove it from the 68 | blockers list of the issue(s) it blocks.""" 69 | 70 | newstatus = cl.get(nodeid,'status') 71 | 72 | # no change? 73 | if oldvalues.get('status', None) == newstatus: 74 | return 75 | 76 | closed_id = db.status.lookup('closed') 77 | 78 | # interesting? 79 | if newstatus != closed_id: 80 | return 81 | 82 | # yes - find all the dependend issues, if any, and remove me from 83 | # their dependency list 84 | issues = cl.find(dependencies=nodeid) 85 | for issueid in issues: 86 | dependencies = cl.get(issueid, 'dependencies') 87 | if nodeid in dependencies: 88 | dependencies.remove(nodeid) 89 | cl.set(issueid, dependencies=dependencies) 90 | 91 | 92 | def init(db): 93 | # fire before changes are made 94 | db.issue.audit('create', init_status) 95 | # db.issue.audit('create', block_resolution) 96 | db.issue.audit('set', block_resolution) 97 | # db.issue.audit('set', resolve) 98 | 99 | # adjust after changes are committed 100 | # db.issue.react('set', resolve_dependencies) 101 | -------------------------------------------------------------------------------- /detectors/textplain.py: -------------------------------------------------------------------------------- 1 | # On file creation, if the file is application/octet-stream, 2 | # yet the file content looks like text, change the type to 3 | # text/plain. 4 | def audit_application_octetstream(db, cl, nodeid, newvalues): 5 | if newvalues.has_key('type') and newvalues['type'] == 'application/octet-stream': 6 | # check whether this might be a text file 7 | try: 8 | text = newvalues['content'].decode('utf-8') 9 | except UnicodeError: 10 | return 11 | # check that there aren't funny control characters in there 12 | for c in text: 13 | if ord(c) >= 32: 14 | continue 15 | if c not in u' \f\t\r\n': 16 | return 17 | newvalues['type'] = 'text/plain' 18 | 19 | 20 | def init(db): 21 | db.file.audit('create', audit_application_octetstream) 22 | -------------------------------------------------------------------------------- /detectors/userauditor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2003 Richard Jones (richard@mechanicalcat.net) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | # 21 | #$Id: userauditor.py,v 1.3 2006/09/18 03:24:38 tobias-herp Exp $ 22 | 23 | import re 24 | import urlparse 25 | 26 | valid_username = re.compile(r'^[a-z0-9_.-]+$', re.IGNORECASE) 27 | 28 | 29 | def audit_user_fields(db, cl, nodeid, newvalues): 30 | ''' Make sure user properties are valid. 31 | 32 | - email address has no spaces in it 33 | - roles specified exist 34 | ''' 35 | if 'username' in newvalues: 36 | if not valid_username.match(newvalues['username']): 37 | raise ValueError( 38 | 'Username must consist only of the letters a-z (any case), ' 39 | 'digits 0-9 and the symbols: ._-' 40 | ) 41 | 42 | if newvalues.has_key('address') and ' ' in newvalues['address']: 43 | raise ValueError, 'Email address must not contain spaces' 44 | 45 | if newvalues.has_key('roles') and newvalues['roles']: 46 | roles = [x.lower().strip() for x in newvalues['roles'].split(',')] 47 | for rolename in roles: 48 | if not db.security.role.has_key(rolename): 49 | raise ValueError, 'Role "%s" does not exist'%rolename 50 | 51 | if None != nodeid and "admin" in roles: 52 | if not "admin" in [x.lower().strip() for x in cl.get(nodeid, 'roles').split(",")]: 53 | raise ValueError, "Only Admins may assign the Admin role!" 54 | 55 | if newvalues.get('homepage'): 56 | scheme = urlparse.urlparse(newvalues['homepage'])[0] 57 | if scheme not in ('http', 'https'): 58 | raise ValueError, "Invalid URL scheme in homepage URL" 59 | 60 | def init(db): 61 | # fire before changes are made 62 | db.user.audit('set', audit_user_fields) 63 | db.user.audit('create', audit_user_fields) 64 | 65 | # vim: set filetype=python ts=4 sw=4 et si 66 | -------------------------------------------------------------------------------- /extensions/README.txt: -------------------------------------------------------------------------------- 1 | This directory is for tracker extensions: 2 | 3 | - CGI Actions 4 | - Templating functions 5 | 6 | See the customisation doc for more information. 7 | -------------------------------------------------------------------------------- /extensions/create_patch.py: -------------------------------------------------------------------------------- 1 | import os, tempfile 2 | from roundup.cgi.actions import Action 3 | 4 | class NotChanged(ValueError): 5 | pass 6 | 7 | def download_patch(source, lastrev, patchbranch): 8 | from mercurial import hg, ui, localrepo, commands, bundlerepo 9 | UI = ui.ui() 10 | bundle = tempfile.mktemp(dir="/var/tmp") 11 | cwd = os.getcwd() 12 | os.chdir(base) 13 | try: 14 | repo0 = hg.repository(UI,base) 15 | repo0.ui.quiet=True 16 | repo0.ui.pushbuffer() 17 | commands.pull(repo0.ui, repo0, quiet=True) 18 | repo0.ui.popbuffer() # discard all pull output 19 | # find out what the head revision of the given branch is 20 | repo0.ui.pushbuffer() 21 | head = repo0.ui.popbuffer().strip() 22 | repo0.ui.pushbuffer() 23 | if commands.incoming(repo0.ui, repo0, source=source, branch=[patchbranch], bundle=bundle, force=False) != 0: 24 | raise ValueError, "Repository contains no changes" 25 | rhead = repo0.ui.popbuffer() 26 | if rhead: 27 | # output is a list of revisions, one per line. last line should be newest revision 28 | rhead = rhead.splitlines()[-1].split(':')[1] 29 | if rhead == lastrev: 30 | raise NotChanged 31 | repo=bundlerepo.bundlerepository(UI, ".", bundle) 32 | repo.ui.pushbuffer() 33 | old = 'max(ancestors(branch("%s"))-outgoing("%s"))' % (patchbranch, base) 34 | commands.diff(repo.ui, repo, rev=[old, patchbranch]) 35 | result = repo.ui.popbuffer() 36 | finally: 37 | os.chdir(cwd) 38 | if os.path.exists(bundle): 39 | os.unlink(bundle) 40 | return result, rhead 41 | 42 | class CreatePatch(Action): 43 | def handle(self): 44 | db = self.db 45 | if not self.hasPermission('Create', 'file'): 46 | raise exceptions.Unauthorised, self._( 47 | "You do not have permission to create files") 48 | if self.classname != 'issue': 49 | raise Reject, "create_patch is only useful for issues" 50 | if not self.form.has_key('@repo'): 51 | self.client.add_error_message('hgrepo missing') 52 | return 53 | repo = self.form['@repo'].value 54 | url = db.hgrepo.get(repo, 'url') 55 | if not url: 56 | self.client.add_error_message('unknown hgrepo url') 57 | return 58 | lastrev = db.hgrepo.get(repo, 'lastrev') 59 | patchbranch = db.hgrepo.get(repo, 'patchbranch') 60 | if not patchbranch: 61 | patchbranch = 'default' 62 | try: 63 | diff, head = download_patch(url, lastrev, patchbranch) 64 | except NotChanged: 65 | self.client.add_error_message('%s.diff is already available' % lastrev) 66 | return 67 | except Exception, e: 68 | self.client.add_error_message(str(e)) 69 | return 70 | fileid = db.file.create(name='%s.diff' % head, 71 | type='text/plain', 72 | content=diff) 73 | files = db.issue.get(self.nodeid, 'files') 74 | files.append(fileid) 75 | db.issue.set(self.nodeid, files=files) 76 | db.hgrepo.set(repo, lastrev=head) 77 | self.client.add_ok_message('Successfully downloaded %s.diff' % head) 78 | db.commit() 79 | 80 | def init(instance): 81 | global base 82 | base = os.path.join(instance.tracker_home, 'cpython') 83 | instance.registerAction('create_patch', CreatePatch) 84 | -------------------------------------------------------------------------------- /extensions/jnosy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides two helper functions used by the Javascript autocomplete 3 | of the nosy list: 4 | 1) a simple state machine to parse the tables of the 5 | experts index and turn them in a JSON object; 6 | 2) a function to get the list of developers as a JSON object; 7 | """ 8 | 9 | import urllib 10 | try: 11 | import json 12 | except ImportError: 13 | import simplejson as json 14 | 15 | url = 'https://raw.githubusercontent.com/python/devguide/main/experts.rst' 16 | 17 | # possible states 18 | no_table = 0 # not parsing a table 19 | table_header = 1 # parsing the header 20 | table_content = 2 # parsing the content 21 | table_end = 3 # reached the end of the table 22 | 23 | def experts_as_json(): 24 | """ 25 | Parse the tables of the experts index and turn them into a JSON object. 26 | """ 27 | data = {} 28 | table_state = no_table 29 | 30 | try: 31 | page = urllib.urlopen(url) 32 | except Exception: 33 | # if something goes wrong just return an empty JSON object 34 | return '{}' 35 | 36 | for line in page: 37 | columns = [column.strip() for column in line.split(' ', 1)] 38 | # all the tables have 2 columns (some entries might not have experts, 39 | # so we just skip them) 40 | if len(columns) != 2: 41 | continue 42 | first, second = columns 43 | # check if we found a table separator 44 | if set(first) == set(second) == set('='): 45 | table_state += 1 46 | if table_state == table_end: 47 | table_state = no_table 48 | continue 49 | if table_state == table_header: 50 | # create a dict for the category (e.g. 'Modules', 'Interest areas') 51 | category = first 52 | data[category] = {} 53 | if table_state == table_content: 54 | # add to the category dict the entries for that category 55 | # (e.g.module names) and the list of experts 56 | # if the entry is empty the names belong to the previous entry 57 | entry = first or entry 58 | names = (name.strip(' *') for name in second.split(',')) 59 | names = ','.join(name for name in names if '(inactive)' not in name) 60 | if not first: 61 | data[category][entry] += names 62 | else: 63 | data[category][entry] = names 64 | return json.dumps(data, separators=(',',':')) 65 | 66 | 67 | def committers_as_json(cls): 68 | """ 69 | Generate a JSON object that contains the username and realname of all 70 | the committers. 71 | """ 72 | users = [] 73 | for user in cls.filter(None, {'iscommitter': 1}): 74 | username = user.username.plain() 75 | realname = user.realname.plain() 76 | if not realname: 77 | continue 78 | users.append([username, realname]) 79 | return json.dumps(users, separators=(',',':')) 80 | 81 | 82 | def devs_as_json(cls): 83 | """ 84 | Generate a JSON object that contains the username and realname of all 85 | the user with the Developer role that are not committers. 86 | """ 87 | users = [] 88 | for user in cls.filter(None, {'roles': 'Developer', 'iscommitter': 0}): 89 | username = user.username.plain() 90 | realname = user.realname.plain() 91 | if not realname: 92 | continue 93 | users.append([username, realname]) 94 | return json.dumps(users, separators=(',',':')) 95 | 96 | 97 | def init(instance): 98 | instance.registerUtil('experts_as_json', experts_as_json) 99 | instance.registerUtil('committers_as_json', committers_as_json) 100 | instance.registerUtil('devs_as_json', devs_as_json) 101 | -------------------------------------------------------------------------------- /extensions/local_replace.py: -------------------------------------------------------------------------------- 1 | import re 2 | import cgi 3 | from roundup import hyperdb 4 | from roundup.cgi.templating import register_propclass, StringHTMLProperty 5 | 6 | 7 | # pre-hg migration 8 | ''' 9 | substitutions = [ 10 | # r12345, r 12345, rev12345, rev 12345, revision12345, revision 12345 11 | (re.compile(r'\b(?r(ev(ision)?)?\s*)(?P\d+)'), 12 | r'\g\g'), 14 | 15 | # Lib/somefile.py, Modules/somemodule.c, Doc/somedocfile.rst, ... 16 | (re.compile(r'(?P(?(?:Demo|Doc|Grammar|' 17 | r'Include|Lib|Mac|Misc|Modules|Parser|PC|PCbuild|Python|' 18 | 'RISCOS|Tools|Objects)/[-.a-zA-Z0-9_/]+[a-zA-Z0-9]/?)'), 19 | r'\g\g'), 21 | ] 22 | ''' 23 | 24 | 25 | def make_file_link(match): 26 | """Convert files to links to the GitHub repo.""" 27 | baseurl = 'https://github.com/python/cpython/blob/' 28 | branch = match.group('v') or 'master/' # the match includes the '/' 29 | path = match.group('path') 30 | lnum = match.group('lnum') or '' # the match includes the ':' 31 | url = baseurl + branch + path 32 | if not path.endswith('/'): 33 | # files without and with line number 34 | if not lnum: 35 | return '%s' % (url, path) 36 | else: 37 | return '%s%s' % (url, lnum[1:], path, lnum) 38 | else: 39 | # dirs 40 | return '%s%s' % (url, path, lnum) 41 | 42 | 43 | def guess_version(path): 44 | """Search for Python version hints in the file path.""" 45 | match = re.search(r'((?<=[Pp]ython)[23]\d\d?|[23]\.\d\d?)', path) 46 | if not match: 47 | return 'main' 48 | version = match.group(1) 49 | if '.' not in version: 50 | version = '.'.join((version[0], version[1:])) 51 | return version 52 | 53 | 54 | def make_traceback_link(match): 55 | """Convert the file/line in the traceback lines in a link.""" 56 | baseurl = 'https://github.com/python/cpython/blob/' 57 | path = match.group('path') # first part of the path 58 | branch = guess_version(match.group('fullpath')) # guessed branch 59 | file = match.group('file') # second part after Lib/ 60 | nfile = file.replace('\\', '/') # normalize the path separators 61 | lnum = match.group('lnum') 62 | return ('File "%s%s", line %s' % 63 | (path, baseurl, branch, nfile, lnum, file, lnum)) 64 | 65 | def make_pep_link(match): 66 | text = match.group(0) 67 | pepnum = match.group(1).zfill(4) 68 | return '%s' % (pepnum, text) 69 | 70 | 71 | # these regexs have test in tests/test_local_replace.py 72 | 73 | seps = r'\b(?(git|hg)?[a-fA-F0-9]{40})\b' % seps), 78 | r'\g'), 79 | (re.compile(r'%s(?P(git|hg)?[a-fA-F0-9]{10,12})\b' % seps), 80 | r'\g'), 81 | 82 | # r12345, r 12345, rev12345, rev. 12345, revision12345, revision 12345 83 | (re.compile(r'%s(?Pr\.?(ev\.?(ision)?)?\s*)(?P\d{4,})' % seps), 84 | r'\g\g'), 85 | 86 | # Lib/somefile.py, Lib/somefile.py:123, Modules/somemodule.c:123, ... 87 | (re.compile(r'%s(?P2\.[0-7]/|3\.\d/)?(?P(?:Demo|Doc|Grammar|' 88 | r'Include|Lib|Mac|Misc|Modules|Parser|PC|PCbuild|Python|' 89 | r'RISCOS|Tools|Programs|Objects)/' 90 | r'[-.\w/]+[a-zA-Z0-9]/?)(?P:\d{1,5})?' % seps), 91 | make_file_link), 92 | 93 | # traceback lines: File "Lib/somefile.py", line 123 in some_func 94 | # note: this regex is not 100% accurate, it might get the wrong part of 95 | # the path or link to non-existing files, but it usually works fine 96 | (re.compile(r'File "(?P(?P[-.\w/\\:]+(?[-.\w/\\]+?\.py))", ' 98 | r'line (?P\d{1,5})'), 99 | make_traceback_link), 100 | 101 | # PEP 8, PEP8, PEP 0008, ... 102 | (re.compile(r'%s\b(?\1'), 108 | ] 109 | 110 | 111 | # if the issue number is too big the db will explode -- limit it to 7 digits 112 | issue_re = re.compile(r'(?P((?1?\d{1,6}))\b', re.I) 114 | 115 | # PR number, pull request number, pullrequest number 116 | pullrequest_re = re.compile(r'(?P(\b(?\d+))\b', re.I) 118 | 119 | 120 | class PyDevStringHTMLProperty(StringHTMLProperty): 121 | def _hyper_repl(self, match): 122 | """ 123 | Override the original method and change it to still linkify URLs and 124 | emails but avoid linkification of issues and other items 125 | (except messages and files). 126 | """ 127 | if match.group('url'): 128 | return self._hyper_repl_url(match, '%s%s') 129 | elif match.group('email'): 130 | return self._hyper_repl_email(match, '%s') 131 | elif (len(match.group('id')) < 10 and 132 | match.group('class') and 133 | match.group('class').lower() in ('msg', 'file')): 134 | # linkify msgs but not issues and other things 135 | return self._hyper_repl_item(match, 136 | '%(item)s') 137 | else: 138 | # just return the matched text 139 | return match.group(0) 140 | 141 | def pydev_hyperlinked(self): 142 | """Create python-dev-specific links.""" 143 | # first do the normal linkification (without linkify the issues) 144 | message = self.plain(hyperlink=1) 145 | # then add links for revision numbers and paths 146 | for cre, replacement in substitutions: 147 | message = cre.sub(replacement, message) 148 | # finally add links for issues 149 | message = issue_re.sub(self._linkify_issue, message) 150 | message = pullrequest_re.sub(self._linkify_pull_request, message) 151 | return message 152 | 153 | def _linkify_issue(self, match): 154 | """Turn an issue (e.g. 'issue 1234' or '#1234') to an HTML link""" 155 | template = ('%(text)s') 157 | issue_id = match.group('id') 158 | text = match.group('text') 159 | cl = self._db.issue 160 | # check if the issue exists 161 | if not cl.hasnode(issue_id): 162 | return text 163 | # get the title 164 | title = cgi.escape(cl.get(issue_id, 'title').replace('"', "'")) 165 | status_id = cl.get(issue_id, 'status') 166 | # get the name of the status 167 | if status_id is not None: 168 | status = self._db.status.get(status_id, 'name') 169 | else: 170 | status = 'none' 171 | return template % dict(issue_id=issue_id, title=title, 172 | status=status, text=text) 173 | 174 | def _linkify_pull_request(self, match): 175 | """Turn a pullrequest (e.g. 'PR 123') to an HTML link""" 176 | template = ('%(text)s') 178 | pr_no = match.group('pr_no') 179 | text = match.group('text') 180 | # find title and status 181 | cl = self._db.pull_request 182 | # find all the pull_request that refer to GitHub PR pr_no, 183 | # with the most recently updated first 184 | pr_ids = cl.filter(None, dict(number=pr_no), sort=[('-', 'activity')]) 185 | title = status = info = cls = '' 186 | for pr_id in pr_ids: 187 | if not title: 188 | title = cl.get(pr_id, 'title', '') 189 | if not status: 190 | status = cl.get(pr_id, 'status', '') 191 | if title and status: 192 | # once we get both, escape and add to info 193 | status = cgi.escape(status).replace('"', "'") 194 | title = cgi.escape(title).replace('"', "'") 195 | info = ': [%s] %s' % (status, title) 196 | break 197 | if status: 198 | cls = 'class="%s" ' % ('open' if status == 'open' else 'closed') 199 | base_url = 'https://github.com/python/cpython/pull/' 200 | return template % dict(base_url=base_url, pr_no=pr_no, cls=cls, 201 | info=info, text=text) 202 | 203 | 204 | noise_changes = re.compile('(nosy_count|message_count)\: \d+\.0( -> \d+\.0)?') 205 | 206 | def clean_count(history): 207 | history = noise_changes.sub('', history).replace('
', '') 208 | return history 209 | 210 | def init(instance): 211 | register_propclass(hyperdb.String, PyDevStringHTMLProperty) 212 | instance.registerUtil('clean_count', clean_count) 213 | -------------------------------------------------------------------------------- /extensions/pull_request.py: -------------------------------------------------------------------------------- 1 | 2 | baseurl = 'https://github.com/python/cpython/pull/' 3 | 4 | def get_pr_url(pr): 5 | """Transforms pr into a working URL.""" 6 | return 'PR %s' % (baseurl, pr.number, pr.title, pr.number) 7 | 8 | 9 | def init(instance): 10 | instance.registerUtil('get_pr_url', get_pr_url) 11 | 12 | -------------------------------------------------------------------------------- /extensions/pydevutils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import random 4 | from roundup.cgi.actions import Action 5 | from roundup.cgi.exceptions import Redirect 6 | 7 | 8 | def is_history_ok(request): 9 | user = request.client.userid 10 | db = request.client.db 11 | classname = request.classname 12 | nodeid = request.nodeid 13 | # restrict display of user history to user itself only 14 | if classname == 'user': 15 | return user == nodeid or 'Coordinator' in db.user.get(user, 'roles') 16 | # currently not used 17 | return True 18 | 19 | def is_coordinator(request): 20 | user = request.client.userid 21 | db = request.client.db 22 | return 'Coordinator' in db.user.get(user, 'roles') 23 | 24 | def is_triager(request, userid): 25 | # We can't use 'request.client.userid' here because is_coordinator() 26 | # is used to determine if the current user is a coordinator. We need 27 | # 'userid' to determine if an arbitrary user is a triager. 28 | db = request.client.db 29 | query = db.user.get(userid, 'roles') 30 | # Disabled users have no roles so we need to check if 'userid' has 31 | # any roles. 32 | if query is None: 33 | return False 34 | return 'Developer' in query 35 | 36 | def clean_ok_message(ok_message): 37 | """Remove nosy_count and message_count from the ok_message.""" 38 | pattern = '\s*(?:nosy|message)_count,|,\s*(?:nosy|message)_count(?= edited)' 39 | return ''.join(re.sub(pattern, '', line) for line in ok_message) + '
' 40 | 41 | 42 | def issueid_and_action_from_class(cls): 43 | """ 44 | Return the id of the issue where the msg/file is/was linked 45 | and if the last "linking action" was 'link' or 'unlink'. 46 | """ 47 | last_action = '' 48 | for entry in cls._klass.history(cls._nodeid): 49 | if 'unlink' in entry: 50 | last_unlink = entry 51 | last_action = 'unlink' 52 | elif 'link' in entry: 53 | last_entry = entry 54 | last_action = 'link' 55 | if last_action in ('link', 'unlink'): 56 | # the msg has been unlinked and not linked back 57 | # the link looks like: ('16', , '4', 58 | # 'link', ('issue', '1', 'messages')) 59 | return last_entry[4][1], last_action 60 | return None, None 61 | 62 | 63 | def clas_as_json(request, cls): 64 | """ 65 | Generate a JSON object that has the GitHub usernames as keys and as values 66 | true if the user signed the CLA, false if not, or none if there is no user 67 | associated with the GitHub username. 68 | """ 69 | # pass the names as user?@template=clacheck&github_names=name1,name2 70 | names = request.form.getvalue('github_names') 71 | if names is None: 72 | # we got a request like user?@template=clacheck&github_names= 73 | return json.dumps({}) # return an empty JSON object 74 | 75 | names = names.split(',') 76 | user = request.client.db.user 77 | result = {} 78 | for name in names: 79 | # find all users with the given github name (case-insensitive) 80 | matches = user.stringFind(github=name) 81 | if matches: 82 | # if we have 1 (or more) user(s), see if at least one signed 83 | value = any(user.get(id, 'contrib_form') for id in matches) 84 | else: 85 | # otherwise the user has no associated bpo account 86 | value = None 87 | result[name] = value 88 | return json.dumps(result, separators=(',',':')) 89 | 90 | 91 | class RandomIssueAction(Action): 92 | def handle(self): 93 | """Redirect to a random open issue.""" 94 | issue = self.context['context'] 95 | # use issue._klass to get a list of ids, and not a list of instances 96 | issue_ids = issue._klass.filter(None, {'status': '1'}) 97 | if not issue_ids: 98 | raise Redirect(self.db.config.TRACKER_WEB) 99 | # we create our own Random instance so we don't have share the state 100 | # of the default Random instance. see issue 644 for details. 101 | rand = random.Random() 102 | url = self.db.config.TRACKER_WEB + 'issue' + rand.choice(issue_ids) 103 | raise Redirect(url) 104 | 105 | 106 | class Redirect2GitHubAction(Action): 107 | def handle(self): 108 | """Redirect to the corresponding GitHub issue.""" 109 | # This action is invoked by opening /issue?@action=redirect&bpo=ID 110 | # If ID is a valid bpo issue ID linked to its corresponding GitHub 111 | # issue, the action will automatically redirect the browser to the 112 | # GitHub issue, if not, it will show an error message. 113 | issue = self.context['context'] 114 | request = self.context['request'] 115 | bpo_id = request.form.getvalue('bpo') 116 | if not bpo_id: 117 | return 'Please provide a bpo issue id with `&bpo=ID` in the URL.' 118 | try: 119 | bpo_id = int(bpo_id) # make sure it's just a number 120 | except ValueError: 121 | return 'Please provide a valid bpo issue id.' 122 | try: 123 | gh_id = issue._klass.get(bpo_id, 'github') 124 | except IndexError: 125 | return 'There is no bpo issue with id {}.'.format(bpo_id) 126 | if not gh_id: 127 | return 'There is no GitHub id for bpo-{}.'.format(bpo_id) 128 | url = 'https://github.com/python/cpython/issues/{}'.format(gh_id) 129 | raise Redirect(url) 130 | 131 | 132 | def openid_links(request): 133 | providers = [ 134 | ('Google', 'oic_login', 'https://www.google.com/favicon.ico'), 135 | ('GitHub', 'oic_login', 'https://github.com/favicon.ico'), 136 | ('Launchpad', 'openid_login', 'https://launchpad.net/favicon.ico'), 137 | ] 138 | links = [] 139 | for name, action, icon, in providers: 140 | links.append({ 141 | 'href': request.env['PATH_INFO'] + '?@action=' + action + '&provider=' + name, 142 | 'src': icon, 143 | 'title': name, 144 | 'alt': name, 145 | }) 146 | return links 147 | 148 | 149 | def init(instance): 150 | instance.registerUtil('is_history_ok', is_history_ok) 151 | instance.registerUtil('is_coordinator', is_coordinator) 152 | instance.registerUtil('is_triager', is_triager) 153 | instance.registerUtil('clean_ok_message', clean_ok_message) 154 | instance.registerUtil('issueid_and_action_from_class', 155 | issueid_and_action_from_class) 156 | instance.registerUtil('clas_as_json', clas_as_json) 157 | instance.registerAction('random', RandomIssueAction) 158 | instance.registerAction('redirect', Redirect2GitHubAction) 159 | instance.registerUtil('openid_links', openid_links) 160 | -------------------------------------------------------------------------------- /extensions/rietveldlink.py: -------------------------------------------------------------------------------- 1 | def rietveldlink(request, issueid, fileid): 2 | patchset = request.client.db.file.get(fileid, 'patchset') 3 | if patchset and patchset != 'n/a': 4 | return '/review/%s/#ps%s' % (issueid, patchset) 5 | return "" 6 | 7 | def init(instance): 8 | instance.registerUtil('rietveldlink', rietveldlink) 9 | -------------------------------------------------------------------------------- /extensions/search_id.py: -------------------------------------------------------------------------------- 1 | import cgi 2 | from roundup.cgi.actions import Action 3 | from roundup.cgi import exceptions 4 | 5 | class SearchIDAction(Action): 6 | def handle(self): 7 | request = self.context['request'] 8 | if not request.search_text: 9 | raise exceptions.FormError("Missing search text") 10 | split = request.search_text.split() 11 | if len(split) == 1: 12 | id = split[0] 13 | if id.isdigit(): 14 | if self.db.hasnode('issue', id): 15 | raise exceptions.Redirect('issue'+id) 16 | if len(split) > 50: 17 | # Postgres crashes on long queries 18 | raise exceptions.FormError("too many search terms") 19 | 20 | class OpenSearchAction(SearchIDAction): 21 | """Action referred to in the Open Search Description. 22 | This has just a single query parameter (in addition to the action 23 | name), and fills out the rest here. 24 | """ 25 | def handle(self): 26 | # Check for IDs first 27 | SearchIDAction.handle(self) 28 | # regular search, fill out query parameters 29 | for k, v in [('@columns', 'id,activity,title,creator,assignee,status,type'), #columns_showall 30 | ('@sort', '-activity'), 31 | ('ignore', 'file:content')]: 32 | self.form.value.append(cgi.MiniFieldStorage(k, v)) 33 | 34 | 35 | def init(instance): 36 | instance.registerAction('searchid', SearchIDAction) 37 | instance.registerAction('opensearch', OpenSearchAction) 38 | -------------------------------------------------------------------------------- /extensions/spambayes.py: -------------------------------------------------------------------------------- 1 | import re, math 2 | from roundup.cgi.actions import Action 3 | from roundup.cgi.exceptions import * 4 | 5 | import xmlrpclib, socket 6 | 7 | REVPAT = re.compile(r'(r[0-9]+\b|rev(ision)? [0-9]+\b)') 8 | 9 | def extract_classinfo(db, classname, nodeid): 10 | node = db.getnode(classname, nodeid) 11 | 12 | authorage = node['creation'].timestamp() - \ 13 | db.getnode('user', node.get('author', node.get('creator')))['creation'].timestamp() 14 | 15 | authorid = node.get('author', node.get('creator')) 16 | 17 | content = db.getclass(classname).get(nodeid, 'content') 18 | 19 | tokens = ["klass:%s" % classname, 20 | "author:%s" % authorid, 21 | "authorage:%d" % int(math.log(authorage)), 22 | "hasrev:%s" % (REVPAT.search(content) is not None)] 23 | 24 | return (content, tokens) 25 | 26 | def train_spambayes(db, content, tokens, is_spam): 27 | # spambayes training is now disabled; only leave 28 | # spam classification UI 29 | return True, None 30 | spambayes_uri = db.config.detectors['SPAMBAYES_URI'] 31 | 32 | server = xmlrpclib.ServerProxy(spambayes_uri, verbose=False) 33 | try: 34 | server.train({'content':content}, tokens, {}, is_spam) 35 | return (True, None) 36 | except (socket.error, xmlrpclib.Error), e: 37 | return (False, str(e)) 38 | 39 | 40 | class SpambayesClassify(Action): 41 | permissionType = 'SB: May Classify' 42 | 43 | def handle(self): 44 | (content, tokens) = extract_classinfo(self.db, 45 | self.classname, self.nodeid) 46 | 47 | if self.form.has_key("trainspam"): 48 | is_spam = True 49 | elif self.form.has_key("trainham"): 50 | is_spam = False 51 | 52 | (status, errmsg) = train_spambayes(self.db, content, tokens, 53 | is_spam) 54 | 55 | node = self.db.getnode(self.classname, self.nodeid) 56 | props = {} 57 | 58 | if status: 59 | if node.get('spambayes_misclassified', False): 60 | props['spambayes_misclassified'] = True 61 | 62 | props['spambayes_score'] = 1.0 63 | 64 | s = " SPAM" 65 | if not is_spam: 66 | props['spambayes_score'] = 0.0 67 | s = " HAM" 68 | self.client.add_ok_message(self._('Message classified as') + s) 69 | else: 70 | self.client.add_error_message(self._('Unable to classify message, got error:') + errmsg) 71 | 72 | klass = self.db.getclass(self.classname) 73 | klass.set(self.nodeid, **props) 74 | self.db.commit() 75 | 76 | def sb_is_spam(obj): 77 | cutoff_score = float(obj._db.config.detectors['SPAMBAYES_SPAM_CUTOFF']) 78 | try: 79 | score = obj['spambayes_score'] 80 | except KeyError: 81 | return False 82 | return score >= cutoff_score 83 | 84 | def init(instance): 85 | instance.registerAction("spambayes_classify", SpambayesClassify) 86 | instance.registerUtil('sb_is_spam', sb_is_spam) 87 | 88 | -------------------------------------------------------------------------------- /extensions/test/test_local_replace.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import os.path 4 | 5 | if len(sys.argv) != 2: 6 | sys.exit('Error: You have to provide the path of Roundup in order to run ' 7 | 'the tests (e.g. /opt/tracker-roundup/lib/python2.7/site-packages/).') 8 | # add to sys.path the dir where roundup is installed (local_replace will try 9 | # to import it) 10 | sys.path.append(sys.argv.pop()) 11 | 12 | testdir = os.path.dirname(os.path.abspath(__file__)) 13 | dirs = testdir.split(os.path.sep) 14 | # add the dir where local_replace is (i.e. one level up) 15 | sys.path.append(os.path.sep.join(dirs[:-1])) 16 | # add the dir where the roundup tests are 17 | sys.path.append(os.path.sep.join(dirs[:-3] + ['roundup', 'test'])) 18 | 19 | 20 | from local_replace import PyDevStringHTMLProperty 21 | from test_templating import MockDatabase, TemplatingTestCase 22 | 23 | 24 | class MockDBItem(object): 25 | def __init__(self, type): 26 | self.type = type 27 | 28 | def hasnode(self, id): 29 | # only issues between 1000 and 2M and prs < 1000 exist in the db 30 | return ((self.type == 'issue' and 1000 <= int(id) < 2000000) or 31 | (self.type == 'pull_request' and int(id) < 1000)) 32 | 33 | def get(self, id, value, default=None): 34 | # for issues and prs, the id determines the status: 35 | # id%3 == 0: open 36 | # id%3 == 1: closed 37 | # id%3 == 2: pending/merged 38 | id = int(id) 39 | if value == 'title': 40 | return 'Mock title' 41 | if value == 'status': 42 | if self.type == 'issue': 43 | return id%3 44 | if self.type == 'pull_request': 45 | return ['open', 'closed', 'merged'][id%3] 46 | if self.type == 'status' and value == 'name': 47 | return ['open', 'closed', 'pending'][id] 48 | 49 | def filter(self, _, filterspec, sort): 50 | prid = filterspec['number'] 51 | return [prid] if self.hasnode(prid) else [] 52 | 53 | class PyDevMockDatabase(MockDatabase): 54 | def __init__(self): 55 | for type in ['issue', 'msg', 'status', 'pull_request']: 56 | setattr(self, type, MockDBItem(type)) 57 | def getclass(self, cls): 58 | return self.issue 59 | 60 | 61 | class TestPyDevStringHTMLProperty(TemplatingTestCase): 62 | def test_replacement(self): 63 | self.maxDiff = None 64 | # create the db 65 | self.client.db = self._db = PyDevMockDatabase() 66 | self.client.db.security.hasPermission = lambda *args, **kw: True 67 | # the test file contains the text on odd lines and the expected 68 | # result on even ones, with comments starting with '##' 69 | f = open(os.path.join(testdir, 'local_replace_data.txt')) 70 | for text, expected_result in zip(f, f): 71 | if text.startswith('##') and expected_result.startswith('##'): 72 | continue # skip the comments 73 | p = PyDevStringHTMLProperty(self.client, 'test', '1', 74 | None, 'test', text) 75 | # decode the str -- Unicode strings have a better diff 76 | self.assertEqual(p.pydev_hyperlinked().decode(), 77 | expected_result.decode()) 78 | 79 | # run the tests 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /extensions/timestamp.py: -------------------------------------------------------------------------------- 1 | import time, struct, base64 2 | from roundup.cgi.actions import RegisterAction 3 | from roundup.cgi.exceptions import * 4 | 5 | def timestamp(): 6 | return base64.encodestring(struct.pack("i", time.time())).strip() 7 | 8 | def unpack_timestamp(s): 9 | return struct.unpack("i",base64.decodestring(s))[0] 10 | 11 | class Timestamped: 12 | def check(self): 13 | try: 14 | created = unpack_timestamp(self.form['opaque'].value) 15 | except KeyError: 16 | raise FormError, "somebody tampered with the form" 17 | if time.time() - created < 4: 18 | raise FormError, "responding to the form too quickly" 19 | return True 20 | 21 | class TimestampedRegister(Timestamped, RegisterAction): 22 | def permission(self): 23 | self.check() 24 | RegisterAction.permission(self) 25 | 26 | def init(instance): 27 | instance.registerUtil('timestamp', timestamp) 28 | instance.registerAction('register', TimestampedRegister) 29 | -------------------------------------------------------------------------------- /extensions/timezone.py: -------------------------------------------------------------------------------- 1 | # Utility for replacing the simple input field for the timezone with 2 | # a select-field that lists the available values. 3 | 4 | import cgi 5 | 6 | try: 7 | import pytz 8 | except ImportError: 9 | pytz = None 10 | 11 | 12 | def tzfield(prop, name, default): 13 | if pytz: 14 | value = prop.plain() 15 | if '' == value: 16 | value = default 17 | else: 18 | try: 19 | value = "Etc/GMT%+d" % int(value) 20 | except ValueError: 21 | pass 22 | 23 | l = ['') 31 | return '\n'.join(l) 32 | 33 | else: 34 | return prop.field() 35 | 36 | def init(instance): 37 | instance.registerUtil('tzfield', tzfield) 38 | -------------------------------------------------------------------------------- /html/_generic.404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item Not Found 4 | 5 | 6 | Item Not Found 7 | 8 | 9 | 10 | There is no with id 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /html/_generic.calendar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /html/_generic.collision.html: -------------------------------------------------------------------------------- 1 | 2 | <span tal:replace="python:context._classname.capitalize()" 4 | i18n:name="class" /> Edit Collision - <span i18n:name="tracker" 5 | tal:replace="config/TRACKER_NAME" /> 6 | Edit Collision 9 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /html/_generic.help-empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Empty page (no search performed yet) 4 | 5 | 6 |

Please specify your search parameters!

7 | 8 | 9 | -------------------------------------------------------------------------------- /html/_generic.help-list.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | Search result for user helper 5 | 6 | 12 | 13 | 16 | 17 | 18 |
19 | 
20 |   

You are not 21 | allowed to view this page.

22 | 23 | 24 | 25 |
26 | 30 | 31 | 34 | 36 | 39 | 40 |
41 | 42 | 43 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 69 | 70 | 71 | 72 |
 x
60 | 63 | 65 | 68 |
73 |
74 |
75 |
76 | 77 |
78 |      
82 |   
83 | 
84 | 


--------------------------------------------------------------------------------
/html/_generic.help-search.html:
--------------------------------------------------------------------------------
 1 | 
 2 |   
 3 |     Frame for search input fields
 4 |   
 5 |   
 6 |     

Generic template 7 | help-search 8 | or version for class 9 | user 10 | is not yet implemented

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /html/_generic.help-submit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | Generic submit page for framed helper windows 8 | 41 | 42 | 43 | 44 | 45 |
46 |  
51 |
52 | 55 |
56 | 57 | 60 | 63 | 66 |
67 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /html/_generic.help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | <tal:x i18n:name="property" 11 | tal:content="property" i18n:translate="" /> help - <span i18n:name="tracker" 12 | tal:replace="config/TRACKER_NAME" /> 13 | 19 | 21 | 22 | 23 | 24 | 25 |

26 | logo 27 |

28 |
29 |
30 | 31 | 32 |
35 | 36 |
37 | 39 | 41 | 44 | 47 |
48 | 49 | 50 | 61 | 66 | 77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
 x
88 | 92 | 94 | 97 |
 x
105 | 106 |
107 |
108 | 109 | 111 | 112 | 113 | 124 | 129 | 140 | 141 |
142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 154 | 155 | 156 | 157 | 158 | 159 |
150 | 153 |
160 |
161 |
162 |
163 |
164 | 165 | 166 | -------------------------------------------------------------------------------- /html/_generic.index.html: -------------------------------------------------------------------------------- 1 | 2 | <span tal:replace="python:context._classname.capitalize()" 4 | i18n:name="class" /> editing - <span i18n:name="tracker" 5 | tal:replace="config/TRACKER_NAME" /> 6 | editing 9 | 10 | 11 | 12 | You are not allowed to view this page. 16 | 17 | Please login with your username and password. 21 | 22 | 23 | 24 |

25 | You may edit the contents of the 26 | 27 | class using this form. Commas, newlines and double quotes (") must be 28 | handled delicately. You may include commas and newlines by enclosing the 29 | values in double-quotes ("). Double quotes themselves must be quoted by 30 | doubling (""). 31 |

32 | 33 |

34 | Multilink properties have their multiple values colon (":") separated 35 | (... ,"one:two:three", ...) 36 |

37 | 38 |

39 | Remove entries by deleting their line. Add new entries by appending 40 | them to the table - put an X in the id column. If you wish to restore a 41 | removed item and you know its id then just put that id in the id column. 42 |

43 |
44 |
46 | 47 |
48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 63 | 64 | 65 |
 
 
66 | 67 | 68 | 69 |
70 | -------------------------------------------------------------------------------- /html/_generic.item.html: -------------------------------------------------------------------------------- 1 | 2 | <span tal:replace="python:context._classname.capitalize()" 4 | i18n:name="class" /> editing - <span i18n:name="tracker" 5 | tal:replace="config/TRACKER_NAME" /> 6 | editing 9 | 10 | 11 | 12 |

14 | You are not allowed to view this page.

15 | 16 |

18 | Please login with your username and password.

19 | 20 |
21 | 22 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 |
  40 | submit button will go here 41 |
44 | 45 |
46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 | -------------------------------------------------------------------------------- /html/_generic.keywords_expr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /html/bullet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/bullet.gif -------------------------------------------------------------------------------- /html/button-on-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/button-on-bg.png -------------------------------------------------------------------------------- /html/committer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/committer.png -------------------------------------------------------------------------------- /html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/favicon.ico -------------------------------------------------------------------------------- /html/file.index.html: -------------------------------------------------------------------------------- 1 | 2 | List of files - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" /> 4 | List of files 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
DownloadDescriptionContent TypeUploaded ByDate
17 | dld link 19 | descriptioncontent typecreator's namecreation date
29 | 30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /html/file.item.html: -------------------------------------------------------------------------------- 1 | 2 | File display - <span 3 | i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> 4 | File display 6 | 7 | 8 | 9 |

11 | You are not allowed to view this page.

12 | 13 |

15 | Please login with your username and password.

16 | 17 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 55 | 56 | 57 |
Name
Description
Content Type 33 | Please note that 34 | for security reasons, it's not permitted to set content type to text/html.
SpamBayes Score
Marked as misclassified
48 |   49 | 50 | 51 | 54 | submit button here
58 |
59 | 60 |

61 | File has been classified as spam.

62 | 63 | download 66 | 67 |

68 | Files classified as spam are not available for download by 69 | unathorized users. If you think the file has been misclassified, 70 | please login and click on the button for reclassification. 71 |

72 | 73 | 74 |
78 | 79 | 80 | 81 | 82 |
83 | 84 | 85 |
91 | 92 | 93 | 94 |

This file is linked to : 95 |

96 |
97 | 98 | 99 |

This file has been unlinked from : 100 |

101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /html/gh-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/gh-icon.png -------------------------------------------------------------------------------- /html/header-bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/header-bg2.png -------------------------------------------------------------------------------- /html/help.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | x 9 | 10 | 11 | 12 | 13 | 19 | 20 | 26 | 31 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /html/home.classlist.html: -------------------------------------------------------------------------------- 1 | 2 | List of classes - <span 3 | i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> 4 | List of classes 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
12 | classname 14 |
nametype
23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /html/home.html: -------------------------------------------------------------------------------- 1 | 7 | 11 | -------------------------------------------------------------------------------- /html/issue.index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <span tal:omit-tag="true" i18n:translate="" >List of issues</span> 4 | <span tal:condition="request/dispname" 5 | tal:replace="python:' - %s '%request.dispname" 6 | /> - <span tal:replace="config/TRACKER_NAME" /> 7 | 8 | 9 | List of issues 10 | 12 | 13 | 14 | 15 |

17 | You are not allowed to view this page.

18 | 19 |

21 | Please login with your username and password.

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 54 | 55 | 56 | 57 | 59 | 60 | 64 | 66 | 68 | 70 | 74 | 78 | 82 | 84 | 86 | 88 | 90 | 92 | 94 | 96 | 98 | 100 | 102 | 104 | 106 | 107 | 108 | 109 |
SeverityIDGHCreationActivityActorTitleComponentsVersionsStageStatusResolutionCreatorAssigned ToKeywordsDepends OnTypeMsgsNosy
50 | 51 | 52 | 53 |
   61 | GH ID 63 |     71 | title 73 | 75 | has patch 77 | 79 | has PR 81 |             
110 | 111 | 112 |
133 | 134 | 135 | 136 | Download as CSV 138 | 139 |
141 | 142 | 143 | 144 | 145 | 148 | 158 | 159 | 162 | 163 | 164 | 165 | 166 | 169 | 179 | 180 | 183 | 184 | 185 | 190 |
146 | Sort on: 147 | 149 | 157 | Descending: 161 |
167 | Group on: 168 | 170 | 178 | Descending: 182 |
186 | 187 | 189 |
191 |
192 | 193 |
194 | 195 | 196 | -------------------------------------------------------------------------------- /html/issue.stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <span tal:omit-tag="true" i18n:translate="" >Issues stats</span> 4 | <span tal:condition="request/dispname" 5 | tal:replace="python:' - %s '%request.dispname" 6 | /> - <span tal:replace="config/TRACKER_NAME" /> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 90 | 91 | 92 | 93 | Issues stats 94 | 96 | 97 | 98 | 99 |

These charts are updated weekly. JavaScript must be enabled to see the charts.

100 | 101 |

Total number of open issues and open issues with patches:

102 |
103 | 104 |

Delta of open issues compared with the previous week. If the delta is positive, 105 | the total number of open issues increased; if negative, the total number decreased.

106 |
107 | 108 |

Number of issues that have been opened and closed during each week. 109 | The difference between these two values is shown in the previous graph.

110 |
111 | 112 |

The number of closed issues, and the number of issues regardless of their status:

113 |
114 | 115 |
116 |
117 | -------------------------------------------------------------------------------- /html/keyword.index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <span tal:omit-tag="true" i18n:translate="" >List of keywords</span> 4 | <span tal:condition="request/dispname" 5 | tal:replace="python:' - %s '%request.dispname" 6 | /> - <span tal:replace="config/TRACKER_NAME" /> 7 | 8 | 9 | List of keywords 10 | 12 | 13 | 14 | 15 |

17 | You are not allowed to view this page.

18 | 19 |

21 | Please login with your username and password.

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
KeywordDescription
32 | keyword name 34 |  
New Keyword
44 | 45 |
46 |
-------------------------------------------------------------------------------- /html/keyword.item.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | <tal:if condition="context/id" i18n:translate="" 6 | >Keyword <span tal:replace="context/id" i18n:name="id" 7 | />: <span tal:replace="context/name" i18n:name="title" 8 | /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" 9 | /></tal:if> 10 | <tal:if condition="not:context/id" i18n:translate="" 11 | >New Keyword - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" 12 | /></tal:if> 13 | 14 | 15 | 16 | 17 | 19 | New Keyword 21 | New Keyword Editing 23 | Keyword 26 | Keyword Editing 29 | 30 | 31 | 32 | 33 |

35 | You are not allowed to view this page.

36 | 37 |

39 | Please login with your username and password.

40 | 41 |
42 | 43 |
47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 69 | 72 | 73 | 74 |
Keyword:title
Description:description
64 |   65 | 66 | 68 | 70 | 71 |
75 |
76 |
77 | 78 |
79 | -------------------------------------------------------------------------------- /html/msg.index.html: -------------------------------------------------------------------------------- 1 | 2 | List of messages - <span tal:replace="config/TRACKER_NAME" 4 | i18n:name="tracker"/> 5 | Message listing 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Messages
authordate
content
24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /html/msg.item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <tal:block condition="context/id" i18n:translate="" 4 | >Message <span tal:replace="context/id" i18n:name="id" 5 | /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" 6 | /></tal:block> 7 | <tal:block condition="not:context/id" i18n:translate="" 8 | >New Message - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" 9 | /></tal:block> 10 | 11 | 12 | New Message 14 | New Message Editing 16 | Message 19 | Message Editing 22 | 23 | 24 | 25 |

27 | You are not allowed to view this page.

28 | 29 |

31 | Please login with your username and password.

32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Author
Recipients
Date
SpamBayes Score
Marked as misclassified
Message-id
In-reply-to
72 | 73 |

74 | Message has been classified as spam

75 | 76 | 77 | 78 | 79 | 80 | 91 | 92 | 93 | 94 | 95 | 99 | 106 | 107 |
Content 82 |
85 | 86 | 87 | 88 | 89 |
90 |
97 |
101 | Message has been classified as spam and is therefore not 102 | available to unathorized users. If you think this is 103 | incorrect, please login and report the message as being 104 | misclassified. 105 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 120 | 124 | 125 |
Files
File nameUploaded
117 | dld link 119 | 121 | creator's name, 122 | creation date 123 |
126 | 127 | 128 |
134 | 135 | 136 | 137 |

This message is linked to : 138 |

139 |
140 | 141 | 142 |

This message has been unlinked from : 143 |

144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 | 152 | 153 |
154 | -------------------------------------------------------------------------------- /html/nav-off-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/nav-off-bg.png -------------------------------------------------------------------------------- /html/nav-on-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/nav-on-bg.png -------------------------------------------------------------------------------- /html/openid-16x16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/openid-16x16.gif -------------------------------------------------------------------------------- /html/osd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python Bugs 4 | Python Bug Tracker Search 5 | python bugtracker 6 | tracker-discuss@python.org 7 | 9 | 11 | Python Bug Tracker Search 12 | 13 | en-us 14 | 15 | -------------------------------------------------------------------------------- /html/patch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/patch-icon.png -------------------------------------------------------------------------------- /html/pull_request.item.html: -------------------------------------------------------------------------------- 1 | 2 | Pull Request - <span 3 | i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> 4 | Pull Request 6 | 7 | 8 | 9 |

11 | You are not allowed to view this page.

12 | 13 |

15 | Please login with your username and password.

16 | 17 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 |
URL
Status
Title
37 |   38 | 39 | 42 | submit button here
46 |
47 | 48 |

49 | File has been classified as spam.

50 | 51 |
57 | 58 | 59 | 60 |

This PR is linked to : 61 |

62 | 63 | 64 | 65 |

This PR has been unlinked from : 66 |

67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /html/python-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/python-logo-small.png -------------------------------------------------------------------------------- /html/python-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/python-logo.gif -------------------------------------------------------------------------------- /html/query.edit.html: -------------------------------------------------------------------------------- 1 | 2 | "Your Queries" Editing - <span tal:replace="config/TRACKER_NAME" 4 | i18n:name="tracker" /> 5 | "Your Queries" Editing 7 | 8 | 9 | 10 | You are not allowed to edit queries. 12 | 13 | 42 | 43 |
45 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 73 | 74 | 75 | 76 | 84 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 111 | 112 | 117 | 119 | 120 | 121 | 122 | 123 | 128 | 129 |
QueryInclude in "Your Queries"EditPrivate to you? 
query 62 | 67 | 72 | edit 77 | 83 | 86 | 88 |
query 98 | 99 | 100 | Query is retired. You can select "remove" and press "Submit Changes" to 101 | remove it from "Your Queries" in the sidebar. 102 |
query 113 | 114 | 115 | edit 116 | [not yours to edit]
124 | 125 | 126 | 127 |
130 |
131 |
132 | 133 | 134 | 135 | New Query Name: 136 | 137 |
138 | 139 |
140 | -------------------------------------------------------------------------------- /html/query.item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /html/triager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/bpo-tracker-cpython/1a94f0977ca025d2baf45ef712ef87f394a59b25/html/triager.png -------------------------------------------------------------------------------- /html/user.clacheck.html: -------------------------------------------------------------------------------- 1 | 3 | {"user1":true,"user2":false,"user3":none} 4 | 5 | -------------------------------------------------------------------------------- /html/user.committers.html: -------------------------------------------------------------------------------- 1 | 3 | [["username1","Real Name1"],["username2", "Real Name2"],...] 4 | 5 | -------------------------------------------------------------------------------- /html/user.devs.html: -------------------------------------------------------------------------------- 1 | 3 | [["username1","Real Name1"],["username2", "Real Name2"],...] 4 | 5 | -------------------------------------------------------------------------------- /html/user.experts.html: -------------------------------------------------------------------------------- 1 | 2 | {"Platform":{"platname":"name1,name2",...}, 3 | "Module":{"modname":"name1,name2",...}, 4 | ...} 5 | 6 | -------------------------------------------------------------------------------- /html/user.forgotten.html: -------------------------------------------------------------------------------- 1 | 2 | Password reset request - <span 3 | i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> 4 | Password reset request 6 | 7 | 8 | 9 | 10 |

You have two options if you have forgotten your password. 11 | If you know the email address you registered with, enter it below.

12 | 13 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 |
Email Address:
  23 | 24 | 25 | 27 |
30 | 31 |

Or, if you know your username, then enter it below.

32 | 33 |

If you have previously created or modified issue 34 | reports in the sourceforge issue tracker, you have an account here with 35 | the same username as your sourceforge username.

36 | 37 | 38 | 39 | 41 |
Username:
42 |
43 | 44 |

A confirmation email will be sent to you - 45 | please follow the instructions within it to complete the reset process.

46 | 47 |
48 | 49 | 50 | g 51 |
52 | -------------------------------------------------------------------------------- /html/user.help-search.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | Search input for user helper 7 | 14 | 15 | 16 | 17 | 22 |
23 |     
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 63 | 64 | 65 | 66 | 67 | 75 | 76 | 77 |
Name
Phone
role 59 | 62 |
  68 | 69 | 70 | 71 | 72 | 73 | 74 |
78 | 79 |
80 |
81 | 
84 |   
85 | 
86 | 


--------------------------------------------------------------------------------
/html/user.help.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 7 |   
 8 |       
 9 |       
11 |       
12 |       <tal:x i18n:translate=""><tal:x i18n:name="property"
13 |        tal:content="property" i18n:translate="" /> help - <span i18n:name="tracker"
14 | 	       tal:replace="config/TRACKER_NAME" /></tal:x>
15 |       
23 |       
26 |       
27 |   
28 | 
29 |   
30 |   
31 |   
34 |   
37 |   
38 | 
39 | 
40 |   <body>
41 | <p i18n:translate="">
42 | Your browser is not capable of using frames; you should be redirected immediately,
43 | or visit <a href="#" tal:attributes="href string:?${qs}&template=help-noframes"
44 | i18n:name="link">this link</a>.
45 | </p>
46 | </body>
47 | 
48 | 
49 | 
50 | 


--------------------------------------------------------------------------------
/html/user.index.html:
--------------------------------------------------------------------------------
  1 | 
  2 | User listing - <span
  3 |  i18n:name="tracker" tal:replace="config/TRACKER_NAME" />
  4 | User listing
  6 | 
  7 | 
  8 | You are not allowed to view this page.
 11 | 
 12 | Please login with your username and password.
 15 | 
 16 | 
18 | 19 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 36 | 37 | 42 | 43 | 44 | 45 | 46 |
Search for users
Username 26 | 29 | Realname 32 | 35 | GitHub 38 | 41 |
47 | 48 |
49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 95 | 96 | 97 |
UsernameGitHubReal nameOrganisation
62 | username * 64 |    
72 | 73 | 74 | 81 | 85 | 92 | 93 |
94 |
98 | 99 | 100 | 101 | 102 |
103 | -------------------------------------------------------------------------------- /html/user.item.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | <tal:if condition="context/id" i18n:translate="" 6 | >User <span tal:replace="context/id" i18n:name="id" 7 | />: <span tal:replace="context/username" i18n:name="title" 8 | /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" 9 | /></tal:if> 10 | <tal:if condition="not:context/id" i18n:translate="" 11 | >New User - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" 12 | /></tal:if> 13 | 14 | 15 | 16 | 17 | 18 | 20 | New User 22 | New User Editing 24 | User 27 | User Editing 30 | 31 | 32 | 33 | 34 |

36 | You are not allowed to view this page.

37 | 38 |

40 | Please login with your username and password.

41 | 42 |
43 | 44 |
51 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 127 | 128 | 129 | 130 | 131 | 133 | 134 | 135 | 136 | 147 | 148 | 149 | 150 | 151 | 157 | 158 | 159 | 160 | 166 | 169 | 170 |
Name
Login Name
Login Password
Confirm Password
81 | 82 | 83 | 84 | 85 | 86 | 87 | (to give the user more than one role, 88 | enter a comma,separated,list) 89 |
GitHub Name
Organisation
Timezone 108 |
Home page
Contributor Form Received 119 | 120 | on: 121 | 122 | 123 | 124 | 125 | 126 |
Is Committer 132 |
E-mail address 138 | calvin@the-z.org 142 | 143 | 144 | 145 |   146 |
152 | 156 |
161 |   162 | 163 | 165 | 167 | 168 |
171 |
172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |
Note: highlighted fields are required.
181 |
182 | 183 |
184 | 185 | 186 | 187 | 188 | 193 | 194 |
OpenID providers
189 | 190 | 191 | 192 |
195 | 196 | 197 | 198 | 199 | 200 | 201 | 211 | 212 | 213 |
Associated OpenID Connect providers
202 | 203 | 204 | 205 |
206 | 207 | 208 | 209 |
210 |
214 |
215 | 216 |
217 | 218 | 219 | 220 | 221 | 222 |
223 | 224 | 225 | 226 |
227 | -------------------------------------------------------------------------------- /html/user.openid.html: -------------------------------------------------------------------------------- 1 | 2 | Registering with <span i18n:name="tracker" 4 | tal:replace="db/config/TRACKER_NAME" /> using OpenID 5 | Registering with 8 | 9 | 10 |

If you already have a tracker account and want to associate this 11 | OpenID with it, you should login to your account, and then claim 12 | the OpenID on Your Details page.

13 | 14 | 15 |
18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 65 |
Name
Login Name
OpenID 34 |
Rolesroles 40 | 41 |
Organisationorganisation
E-mail address
Alternate E-mail addresses
One address per line
alternate_addresses
  59 | 60 | 61 | 62 | 63 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
Note: highlighted fields are required.
76 |
77 | 78 | 79 | 80 |
81 | -------------------------------------------------------------------------------- /html/user.register.html: -------------------------------------------------------------------------------- 1 | 2 | Registering with <span i18n:name="tracker" 4 | tal:replace="db/config/TRACKER_NAME" /> 5 | Registering with 8 | 9 | 10 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | 69 |
Namerealname
Login Name 23 | username 24 | Login name must consist only of the letters a-z (any case), digits 0-9 and the symbols: ._- 25 |
Login Passwordpassword
Confirm Passwordpassword
Rolesroles 40 | 41 |
GitHub Namegithub
Organisationorganisation
E-mail addressaddress
Alternate E-mail addresses
One address per line
alternate_addresses
  63 | 64 | 65 | 66 | 67 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Note: highlighted fields are required.
80 |
81 | 82 | 83 | 84 |
85 | -------------------------------------------------------------------------------- /html/user.rego_progress.html: -------------------------------------------------------------------------------- 1 | 2 | Registration in progress - <span i18n:name="tracker" 4 | tal:replace="config/TRACKER_NAME" /> 5 | Registration in progress... 7 | 8 | 9 |

You will shortly receive an email to 10 | to confirm your registration. To complete the registration process, 11 | visit the link indicated in the email. 12 |

13 | 14 |

Note: if after a few minutes you still haven't received 15 | any email, make sure to check the spam folder.

16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /html/user_utils.js: -------------------------------------------------------------------------------- 1 | // User Editing Utilities 2 | 3 | /** 4 | * for new users: 5 | * Depending on the input field which calls it, takes the value 6 | * and dispatches it to certain other input fields: 7 | * 8 | * address 9 | * +-> username 10 | * | `-> realname 11 | * `-> organisation 12 | */ 13 | function split_name(that) { 14 | var raw = that.value 15 | var val = trim(raw) 16 | if (val == '') { 17 | return 18 | } 19 | var username='' 20 | var realname='' 21 | var address='' 22 | switch (that.name) { 23 | case 'address': 24 | address=val 25 | break 26 | case 'username': 27 | username=val 28 | break 29 | case 'realname': 30 | realname=val 31 | break 32 | default: 33 | alert('Ooops - unknown name field '+that.name+'!') 34 | return 35 | } 36 | var the_form = that.form; 37 | 38 | function field_empty(name) { 39 | return the_form[name].value == '' 40 | } 41 | 42 | // no break statements - on purpose! 43 | switch (that.name) { 44 | case 'address': 45 | var split1 = address.split('@') 46 | if (field_empty('username')) { 47 | username = split1[0] 48 | the_form.username.value = username 49 | } 50 | if (field_empty('organisation')) { 51 | the_form.organisation.value = default_organisation(split1[1]) 52 | } 53 | case 'username': 54 | if (field_empty('realname')) { 55 | realname = Cap(username.split('.').join(' ')) 56 | the_form.realname.value = realname 57 | } 58 | case 'realname': 59 | if (field_empty('username')) { 60 | username = Cap(realname.replace(' ', '.')) 61 | the_form.username.value = username 62 | } 63 | if (the_form.firstname && the_form.lastname) { 64 | var split2 = realname.split(' ') 65 | var firstname='', lastname='' 66 | firstname = split2[0] 67 | lastname = split2.slice(1).join(' ') 68 | if (field_empty('firstname')) { 69 | the_form.firstname.value = firstname 70 | } 71 | if (field_empty('lastname')) { 72 | the_form.lastname.value = lastname 73 | } 74 | } 75 | } 76 | } 77 | 78 | function SubCap(str) { 79 | switch (str) { 80 | case 'de': case 'do': case 'da': 81 | case 'du': case 'von': 82 | return str; 83 | } 84 | if (str.toLowerCase().slice(0,2) == 'mc') { 85 | return 'Mc'+str.slice(2,3).toUpperCase()+str.slice(3).toLowerCase() 86 | } 87 | return str.slice(0,1).toUpperCase()+str.slice(1).toLowerCase() 88 | } 89 | 90 | function Cap(str) { 91 | var liz = str.split(' ') 92 | for (var i=0; i 0: 33 | continue 34 | parts = f.split('/') 35 | for i in range(len(parts)): 36 | prefix = '/'.join(parts[:i]) 37 | if i: 38 | prefix += '/' 39 | suffix = '/'.join(parts[i:]) 40 | to_add.append((prefix, suffix)) 41 | cursor.executemany("insert into fileprefix(prefix, suffix) " 42 | "values(%s, %s)", to_add) 43 | 44 | def fill_revs(db, lookfor=None): 45 | """Initialize/update svnbranch table. If lookfor is given, 46 | return its branch, or None if that cannot be determined.""" 47 | result = None 48 | c = db.cursor 49 | c.execute('select max(rev) from svnbranch') 50 | start = c.fetchone()[0] 51 | if not start: 52 | start = 1 53 | else: 54 | start = start+1 55 | if lookfor and lookfor < start: 56 | # revision is not in database 57 | return None 58 | p = subprocess.Popen(['svn', 'log', '-r%s:HEAD' % start, '--xml', '-v', 59 | 'http://svn.python.org/projects'], 60 | stdout = subprocess.PIPE, 61 | stderr = open('/dev/null', 'w')) 62 | data = p.stdout.read() 63 | if p.wait() != 0: 64 | # svn log failed 65 | return None 66 | xml = ElementTree.fromstring(data) 67 | files = set() 68 | for entry in xml.findall('logentry'): 69 | rev = int(entry.get('revision')) 70 | paths = [p.text for p in entry.findall('paths/path')] 71 | if not paths: 72 | continue 73 | path = paths[0] 74 | if (not path.startswith('/python') or 75 | # The count may be smaller on the revision that created /python 76 | # or the branch 77 | path.count('/')<3): 78 | continue 79 | branch = path[len('/python'):] 80 | if branch.startswith('/trunk'): 81 | branch = '/trunk' 82 | else: 83 | d1, d2 = branch.split('/', 3)[1:3] 84 | branch = '/'+d1+'/'+d2 85 | ppath = '/python'+branch 86 | for p in paths: 87 | if not p.startswith(ppath): 88 | # inconsistent commit 89 | break 90 | else: 91 | if branch in ('/trunk', '/branches/py3k'): 92 | # Add all files that ever existed on the main trunks 93 | for p in paths: 94 | files.add(p[len(ppath)+1:]) 95 | c.execute('insert into svnbranch(rev, branch) values(%s, %s)', 96 | (rev, branch)) 97 | if lookfor == rev: 98 | result = branch 99 | addfiles(c, files) 100 | db.commit() 101 | return result 102 | 103 | # this runs as a cron job every 30min 104 | if __name__=='__main__': 105 | # manual setup: 106 | # create table svnbranch(rev integer primary key, branch text); 107 | # create table fileprefix(prefix text, suffix text); 108 | # create index fileprefix_suffix on fileprefix(suffix); 109 | # then run this once in the instance directory 110 | sys.path.append('/home/roundup/lib/python2.5/site-packages') 111 | import roundup.instance 112 | tracker = roundup.instance.open('.') 113 | db = tracker.open('admin') 114 | #db.cursor.execute('delete from svnbranch') 115 | fill_revs(db) 116 | -------------------------------------------------------------------------------- /rietveld.wsgi: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append('/srv/roundup/trackers/cpython/rietveld') 3 | os.environ['DJANGO_SETTINGS_MODULE']='settings' 4 | import django.core.handlers.wsgi 5 | import gae2django 6 | gae2django.install(server_software='Django') 7 | _application = django.core.handlers.wsgi.WSGIHandler() 8 | def application(environ, start_response): 9 | # Django's {%url%} template won't prefix script_name, 10 | # so clear it here and put everything in path_info 11 | environ['PATH_INFO'] = environ['SCRIPT_NAME']+environ['PATH_INFO'] 12 | environ['SCRIPT_NAME'] = '' 13 | 14 | #start_response('200 Ok', [('Content-type', 'text/plain')]) 15 | #return ["\n".join([':'.join((str(k),str(v))) for k,v in environ.items()])] 16 | 17 | return _application(environ, start_response) 18 | 19 | -------------------------------------------------------------------------------- /scripts/addoic: -------------------------------------------------------------------------------- 1 | # -*- python -*- 2 | # Add OpenID Connect registration 3 | # Usage: addoic provider client_id client_secret 4 | import sys 5 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.6/site-packages') 6 | import roundup.instance 7 | 8 | tracker = roundup.instance.open('.') 9 | db = tracker.open('admin') 10 | 11 | reg = db.oic_registration.create(issuer=sys.argv[1], client_id=sys.argv[2], client_secret=sys.argv[3]) 12 | db.commit() 13 | -------------------------------------------------------------------------------- /scripts/addpatchsets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys, os, urllib2, logging, datetime 3 | basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | sys.path.append(basedir+"/rietveld") 5 | os.environ["DJANGO_SETTINGS_MODULE"]="settings" 6 | import gae2django 7 | gae2django.install(server_software='Django') 8 | 9 | verbose = False 10 | if len(sys.argv)>=2 and sys.argv[1]=='-v': 11 | verbose=True 12 | del sys.argv[1] 13 | else: 14 | logging.disable(logging.ERROR) 15 | 16 | from codereview.models import (Repository, Branch, Patch, 17 | PatchSet, Issue, Content) 18 | from django.contrib.auth.models import User 19 | from codereview import patching, utils 20 | from roundup_helper.models import File, RoundupIssue 21 | from django.db import connection, transaction 22 | from google.appengine.ext import db as gae_db 23 | 24 | transaction.enter_transaction_management() 25 | transaction.managed() 26 | python = Repository.gql("WHERE name = 'Python'").get() 27 | 28 | _branches={} 29 | def get_branch(branchname): 30 | try: 31 | return _branches[branchname] 32 | except KeyError: 33 | branch = Branch.gql("WHERE url = '%s%s/'" % (python.url, branchname[1:])).get() 34 | _branches[branchname] = branch 35 | return branch 36 | 37 | def try_match(filename, chunks, rev): 38 | try: 39 | r = urllib2.urlopen("http://hg.python.org/cpython/raw-file/"+rev+"/"+filename) 40 | except urllib2.HTTPError: 41 | # not found/not accessible 42 | # assume that the file did not exist in the base revision 43 | # and consider it empty. If the problem was different (i.e. there should have 44 | # been a non-empty base, patching will fail. 45 | base = "" 46 | else: 47 | base = utils.unify_linebreaks(r.read()) 48 | lines = base.splitlines(True) 49 | for (start, end), newrange, oldlines, newlines in chunks: 50 | if lines[start:end] != oldlines: 51 | if verbose: 52 | print "No match", filename, start, end 53 | return None 54 | return base 55 | 56 | def hg_splitpatch(data): 57 | patches = [] 58 | filename = None 59 | for line in data.splitlines(True): 60 | if line.startswith('diff '): 61 | if filename: 62 | chunks = patching.ParsePatchToChunks(diff) 63 | if chunks: 64 | patches.append((filename, ''.join(diff), chunks)) 65 | diff = [] 66 | filename = line.split()[-1] 67 | if filename.startswith("b/"): 68 | # git style 69 | filename = filename[2:] 70 | continue 71 | if filename: 72 | diff.append(line) 73 | # add last patch 74 | if filename: 75 | chunks = patching.ParsePatchToChunks(diff) 76 | if chunks: 77 | patches.append((filename, ''.join(diff), chunks)) 78 | return patches 79 | 80 | def find_bases(data): 81 | if not data.startswith("diff "): 82 | # this should only be called if there is actually is a diff in the file 83 | head, tail = data.split("\ndiff ", 1) 84 | data = "diff "+tail 85 | # default to default branch if no revision is found 86 | rev = 'default' 87 | if data.startswith("diff -r "): 88 | first, second, rev = data.split()[:3] 89 | c = connection.cursor() 90 | pieces = hg_splitpatch(data) 91 | if not pieces: 92 | if verbose: 93 | print "Splitting failed" 94 | return None, None 95 | bases = [] 96 | for filename, data, chunks in pieces: 97 | res = try_match(filename, chunks, rev) 98 | if res is None: 99 | return None, None 100 | bases.append((filename, data, chunks, res)) 101 | if verbose: 102 | print "Found match", rev 103 | return 'default', bases 104 | 105 | c = connection.cursor() 106 | c.execute("select id from _status where _name='closed'") 107 | closed = c.fetchone()[0] 108 | 109 | if len(sys.argv) == 2: 110 | query = File.objects.filter(id = sys.argv[1]) 111 | else: 112 | query = (File.objects.filter(_patchset__isnull=True, 113 | _creation__gt=datetime.datetime.utcnow()-datetime.timedelta(seconds=30*60)). 114 | order_by('id')) 115 | for f in query: 116 | c.execute("select nodeid from issue_files where linkid=%s", (f.id,)) 117 | nodeid = c.fetchone() 118 | if not nodeid: 119 | if verbose: 120 | print "File",f.id,"is detached" 121 | continue 122 | nodeid = nodeid[0] 123 | roundup = RoundupIssue.objects.get(id=nodeid) 124 | #if roundup._status == closed: 125 | # if verbose: 126 | # print "issue",nodeid,"is closed" 127 | # continue 128 | filename = os.path.join(basedir, "db", "files", "file", 129 | str(f.id/1000), "file"+str(f.id)) 130 | if not os.path.exists(filename): 131 | print filename,"not found" 132 | continue 133 | data = open(filename).read() 134 | if not data.startswith('diff ') and data.find('\ndiff ')==-1: 135 | if verbose: 136 | print filename, "is not a patch" 137 | continue 138 | issue = Issue.objects.filter(id=nodeid) 139 | if not issue: 140 | c.execute("select _address from _user,issue_nosy where nodeid=%s and id=linkid", 141 | (nodeid,)) 142 | cc = [r[0] for r in c.fetchall()] 143 | c.execute("select _title, _creator from _issue where id=%s", (nodeid,)) 144 | title, creator = c.fetchone() 145 | issue = Issue(id=nodeid, subject=title, owner_id=creator, cc=cc) 146 | issue.put() 147 | else: 148 | issue = issue[0] 149 | if verbose: 150 | print "Doing", f.id 151 | data = utils.unify_linebreaks(data) 152 | branch, bases = find_bases(data) 153 | if not branch: 154 | if f.id < 15000: 155 | f._patchset = "n/a" 156 | f.save() 157 | transaction.commit() 158 | continue 159 | 160 | blob = gae_db.Blob(data) 161 | patchset = PatchSet(issue=issue, data=blob, parent=issue, 162 | created=f._creation, modified=f._creation) 163 | patchset.put() 164 | issue.patchset=patchset 165 | issue.put() 166 | f._branch = branch 167 | f._patchset = str(patchset.id) 168 | f.save() 169 | for filename, data, chunks, base in bases: 170 | patch = Patch(patchset=patchset, text=utils.to_dbtext(data), 171 | filename=filename, parent=patchset) 172 | patch.put() 173 | content = Content(text=utils.to_dbtext(base), parent=patch) 174 | content.put() 175 | patch.content = content 176 | patch.put() 177 | transaction.commit() 178 | transaction.commit() 179 | transaction.leave_transaction_management() 180 | -------------------------------------------------------------------------------- /scripts/adjust_user: -------------------------------------------------------------------------------- 1 | # This script changes all references to one user to point to a 2 | # different. Useful if the user has several accounts which he 3 | # doesn't need anymore. 4 | 5 | import sys 6 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.4/site-packages') 7 | import roundup.instance 8 | from roundup.hyperdb import Link, Multilink 9 | 10 | if len(sys.argv) != 3: 11 | print "Usage: adjust_user old new" 12 | raise SystemExit 13 | old, new = sys.argv[1:] 14 | 15 | tracker = roundup.instance.open('.') 16 | db = tracker.open('admin') 17 | 18 | old = db.user.lookup(old) 19 | new = db.user.lookup(new) 20 | 21 | references = [] # class, prop 22 | for klass in db.getclasses(): 23 | klass = db.getclass(klass) 24 | klass.disableJournalling() 25 | for name, typ in klass.getprops().items(): 26 | if isinstance(typ, (Link, Multilink)) and typ.classname=='user': 27 | references.append((klass, name)) 28 | for klass, name in references: 29 | for id in klass.find(**{name:old}): 30 | v = klass.get(id, name) 31 | if isinstance(v, list): 32 | # Multilink 33 | for i in range(len(v)): 34 | if v[i] == old: 35 | v[i] = new 36 | # col:(add, remove) 37 | multilink_changes = {name:([new],[old])} 38 | else: 39 | # Link 40 | v = new 41 | multilink_changes = {} 42 | db.setnode(klass.classname, id, {name: v}, multilink_changes) 43 | db.user.enableJournalling() 44 | db.user.retire(old) 45 | db.commit() 46 | -------------------------------------------------------------------------------- /scripts/close-pending: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Make sure you are using the python that has the roundup runtime used by the 3 | # tracker. Requires Python 2.3 or later. 4 | # 5 | # Based on roundup-summary script to create a tracker summary by 6 | # Richard Jones and Paul Dubois 7 | # 8 | # Changed into close-pending script by Sean Reifschneider, 2009. 9 | 10 | import sys, math 11 | 12 | # kludge 13 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.4/site-packages') 14 | # 15 | import roundup 16 | import roundup.date, roundup.instance 17 | import optparse , datetime 18 | import cStringIO, MimeWriter, smtplib 19 | from roundup.mailer import SMTPConnection 20 | 21 | ### CONFIGURATION 22 | # Roundup date range, pending issues that don't have activity in this 23 | # date range will be set to "closed" status. 24 | expireDates = ';-2w' 25 | 26 | ##### END CONFIGURATION 27 | 28 | usage = """%prog trackerHome 29 | [--expire-dates 'date1;date2'] 30 | # for maintainers 31 | [--DEBUG] 32 | dates is a roundup date range such as: 33 | '-1w;' -- newer than a week 34 | 35 | Be sure to protect commas and semicolons from the shell with quotes! 36 | Execute %prog --help for detailed help 37 | """ 38 | #### Options 39 | parser = optparse.OptionParser(usage=usage) 40 | parser.add_option('-e','--expire-dates', dest='expire_dates', 41 | default=expireDates, metavar="'expire_dates;'", 42 | help="""Specification for range of dates, such as: \ 43 | ';-1w' -- Older than 1 week; 44 | ';-1y' -- Older than 1 year""") 45 | 46 | #### Get the command line args: 47 | (options, args) = parser.parse_args() 48 | 49 | if len(args) != 1: 50 | parser.error("""Incorrect number of arguments; 51 | you must supply a tracker home.""") 52 | instanceHome = args[0] 53 | 54 | instance = roundup.instance.open(instanceHome) 55 | db = instance.open('admin') 56 | 57 | pendingID = db.status.lookup('pending') 58 | expireIdList = db.issue.filter(None, 59 | { 'activity' : options.expire_dates, 'status' : pendingID }) 60 | 61 | messageBody = ( 62 | 'This issue is being automatically closed due to inactivity while it is in ' 63 | 'the pending state. If you are able to provide further information to help ' 64 | 'move this along, please update the ticket (which will re-open it).') 65 | 66 | closedID = db.status.lookup('closed') 67 | for issueID in expireIdList: 68 | #print 'Closing issue "%s" with activity "%s"' % ( issueID, 69 | # db.issue.get(issueID, 'activity')) 70 | 71 | messages = db.issue.get(issueID, 'messages') 72 | msgID = db.msg.create(author = db.getuid(), 73 | summary = 'Automatically closing pending issue', 74 | content = messageBody) 75 | messages.append(msgID) 76 | 77 | db.issue.set(issueID, status = closedID, messages = messages) 78 | 79 | db.commit() 80 | sys.exit(0) 81 | -------------------------------------------------------------------------------- /scripts/fillrevs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Fill branch information for all file objects where it's missing 3 | import identify_patch 4 | import roundup.instance 5 | tracker = roundup.instance.open('.') 6 | db = tracker.open('admin') 7 | 8 | for fileid in db.file.list(): 9 | fname = db.file.get(fileid, 'name') 10 | if fname.endswith('.patch') or fname.endswith('.diff'): 11 | if db.file.get(fileid, 'revision'): 12 | continue 13 | data = db.file.get(fileid, 'content') 14 | revid, branch = identify_patch.identify(db, data) 15 | if revid: 16 | db.file.set_inner(fileid, revision=str(revid)) 17 | if branch: 18 | db.file.set_inner(fileid, branch=branch) 19 | db.commit() 20 | -------------------------------------------------------------------------------- /scripts/initrietveld: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Create Rietveld users and issues 3 | import rietveldreactor 4 | import roundup.instance 5 | tracker = roundup.instance.open('.') 6 | db = tracker.open('admin') 7 | 8 | # Need to widen username, as some roundup names are too long 9 | widen_username = """ 10 | begin; 11 | alter table auth_user add username2 varchar(50); 12 | update auth_user set username2=username; 13 | alter table auth_user drop username; 14 | alter table auth_user add username varchar(50); 15 | update auth_user set username=username2; 16 | commit; 17 | """ 18 | 19 | c = db.cursor 20 | for userid in db.user.getnodeids(): 21 | c.execute("select count(*) from auth_user where id=%s", (userid,)) 22 | if c.fetchone()[0] == 1: 23 | continue 24 | rietveldreactor.create_django_user(db, db.user, userid, None) 25 | db.commit() 26 | -------------------------------------------------------------------------------- /scripts/issuestats.py: -------------------------------------------------------------------------------- 1 | # Search for the weekly summary reports from bugs.python.org in the 2 | # python-dev archives and plot the result. 3 | # 4 | # $ issuestats.py collect 5 | # 6 | # Collects statistics from the mailing list and saves to 7 | # issue.stats.json 8 | # 9 | # $ issuestats.py plot 10 | # 11 | # Written by Ezio Melotti. 12 | # Based on the work of Petri Lehtinen (https://gist.github.com/akheron/2723809). 13 | # 14 | # Requires Python 3. 15 | # 16 | # This script is used to generate a JSON file with historical data that can 17 | # be copied in the html/ dir of the bugs.python.org Roundup instance and used 18 | # by the html/issue.stats.html page. The roundup-summary script can update the 19 | # JSON file weekly. 20 | # 21 | 22 | 23 | import os 24 | import re 25 | import sys 26 | import json 27 | import gzip 28 | import errno 29 | import argparse 30 | import datetime 31 | import tempfile 32 | import webbrowser 33 | import urllib.parse 34 | import urllib.request 35 | 36 | from collections import defaultdict 37 | 38 | 39 | MONTH_NAMES = [datetime.date(2012, n, 1).strftime('%B') for n in range(1, 13)] 40 | ARCHIVE_URL = 'http://mail.python.org/pipermail/python-dev/%s' 41 | 42 | STARTYEAR = 2011 43 | STARTMONTH = 1 # February 44 | 45 | NOW = datetime.date.today() 46 | ENDYEAR = NOW.year 47 | ENDMONTH = NOW.month 48 | 49 | STATISTICS_FILENAME = 'issue.stats.json' 50 | 51 | activity_re = re.compile('ACTIVITY SUMMARY \((\d{4}-\d\d-\d\d) - ' 52 | '(\d{4}-\d\d-\d\d)\)') 53 | count_re = re.compile('\s+(open|closed|total)\s+(\d+)\s+\(([^)]+)\)') 54 | patches_re = re.compile('Open issues with patches: (\d+)') 55 | 56 | 57 | def find_statistics(source): 58 | print(source) 59 | monthly_data = {} 60 | with gzip.open(source) as file: 61 | parsing = False 62 | for line in file: 63 | line = line.decode('utf-8') 64 | if not parsing: 65 | m = activity_re.match(line) 66 | if not m: 67 | continue 68 | start_end = m.groups() 69 | if start_end in monthly_data: 70 | continue 71 | monthly_data[start_end] = weekly_data = {} 72 | parsing = True 73 | continue 74 | m = count_re.match(line) 75 | if parsing and m: 76 | type, count, delta = m.groups() 77 | weekly_data[type] = int(count) 78 | weekly_data[type + '_delta'] = int(delta) 79 | m = patches_re.match(line) 80 | if parsing and m: 81 | weekly_data['patches'] = int(m.group(1)) 82 | parsing = False 83 | print(' ', len(monthly_data), 'reports found') 84 | return monthly_data 85 | 86 | 87 | def collect_data(): 88 | try: 89 | os.mkdir('cache') 90 | except OSError as exc: 91 | if exc.errno != errno.EEXIST: 92 | raise 93 | 94 | statistics = {} 95 | 96 | for year in range(STARTYEAR, ENDYEAR + 1): 97 | # Assume STARTYEAR != ENDYEAR 98 | if year == STARTYEAR: 99 | month_range = range(STARTMONTH, 12) 100 | elif year == ENDYEAR: 101 | month_range = range(0, ENDMONTH) 102 | else: 103 | month_range = range(12) 104 | 105 | for month in month_range: 106 | prefix = '%04d-%s' % (year, MONTH_NAMES[month]) 107 | 108 | archive = prefix + '.txt.gz' 109 | archive_path = os.path.join('cache', archive) 110 | 111 | if not os.path.exists(archive_path): 112 | print('Downloading %s' % archive) 113 | url = ARCHIVE_URL % urllib.parse.quote(archive) 114 | urllib.request.urlretrieve(url, archive_path) 115 | 116 | 117 | print('Processing %s' % prefix) 118 | statistics.update(find_statistics(archive_path)) 119 | 120 | 121 | statistics2 = defaultdict(list) 122 | for key, val in sorted(statistics.items()): 123 | statistics2['timespan'].append(key) 124 | for k2, v2 in val.items(): 125 | statistics2[k2].append(v2) 126 | 127 | with open(STATISTICS_FILENAME, 'w') as fobj: 128 | json.dump(statistics2, fobj) 129 | 130 | print('Now run "plot".') 131 | 132 | 133 | HTML = """ 134 | 135 | 136 | 137 | 138 | 139 | 175 | 176 | 177 | 178 | 179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | 189 | 190 | """ 191 | 192 | def plot_statistics(): 193 | try: 194 | with open(STATISTICS_FILENAME) as j: 195 | json = j.read() 196 | except FileNotFoundError: 197 | sys.exit('You need to run "collect" first.') 198 | with tempfile.NamedTemporaryFile('w', delete=False) as tf: 199 | tf.write(HTML % json) 200 | webbrowser.open(tf.name) 201 | 202 | 203 | 204 | def main(): 205 | parser = argparse.ArgumentParser() 206 | parser.add_argument('command', choices=['collect', 'plot']) 207 | 208 | args = parser.parse_args() 209 | 210 | if args.command == 'collect': 211 | collect_data() 212 | elif args.command == 'plot': 213 | plot_statistics() 214 | 215 | 216 | if __name__ == '__main__': 217 | main() 218 | -------------------------------------------------------------------------------- /scripts/mass_reassign: -------------------------------------------------------------------------------- 1 | # This sample script reassigns all "open" "Documentation" 2 | # issues assigned to "georg.brandl" to "docs@python" 3 | import sys 4 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.4/site-packages') 5 | import roundup.instance 6 | 7 | tracker = roundup.instance.open('.') 8 | db = tracker.open('admin') 9 | 10 | open = db.status.lookup('open') 11 | olduser = db.user.lookup('georg.brandl') 12 | newuser = db.user.lookup('docs@python') 13 | component = db.component.lookup("Documentation") 14 | 15 | edit = db.issue.filter(None, {'status':open, 16 | 'components':component, 17 | 'assignee':olduser}) 18 | print len(edit), edit; raise SystemExit 19 | 20 | for issue in edit: 21 | # Use set_inner, so that auditors and reactors don't fire 22 | db.issue.set_inner(issue, assignee=newuser) 23 | 24 | db.commit() 25 | -------------------------------------------------------------------------------- /scripts/remove_py3k: -------------------------------------------------------------------------------- 1 | # This sample script changes all issues with the 2 | # py3k keyword to using the "Python 3.0" version instead. 3 | import sys 4 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.4/site-packages') 5 | import roundup.instance 6 | 7 | tracker = roundup.instance.open('.') 8 | db = tracker.open('admin') 9 | 10 | py3k = db.keyword.lookup('py3k') 11 | py30 = db.version.lookup('Python 3.0') 12 | 13 | using_py3k = db.issue.find(keywords={py3k:1}) 14 | 15 | for issue in using_py3k: 16 | keywords = db.issue.get(issue, 'keywords') 17 | keywords.remove(py3k) 18 | versions = db.issue.get(issue, 'versions') 19 | versions.append(py30) 20 | 21 | # Use set_inner, so that auditors and reactors don't fire 22 | db.issue.set_inner(issue, keywords=keywords, versions=versions) 23 | 24 | db.commit() 25 | -------------------------------------------------------------------------------- /scripts/set_counts: -------------------------------------------------------------------------------- 1 | -- cannot use roundup API, since that would set _activity on all issues 2 | update _issue set _message_count = (select count(*) from issue_messages where nodeid=id); 3 | update _issue set _nosy_count = (select count(*) from issue_nosy where nodeid=id); 4 | -------------------------------------------------------------------------------- /scripts/set_text_plain: -------------------------------------------------------------------------------- 1 | # This sample script changes all files with the 2 | # extensions .diff/.patch/.py to the text/plain type 3 | import sys, posixpath 4 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.4/site-packages') 5 | import roundup.instance 6 | 7 | tracker = roundup.instance.open('.') 8 | db = tracker.open('admin') 9 | 10 | tochange = [] 11 | for fileid in db.file.getnodeids(): 12 | name = db.file.get(fileid, 'name') 13 | if posixpath.splitext(name)[1] in ('.diff','.patch','.py'): 14 | if db.file.get(fileid, 'type') != 'text/plain': 15 | db.file.set_inner(fileid, type='text/plain') 16 | 17 | db.commit() 18 | -------------------------------------------------------------------------------- /scripts/suggest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # WSGI server to implement search suggestions 3 | import ConfigParser, psycopg2, os, urllib 4 | 5 | try: 6 | import json 7 | except ImportError: 8 | import simplejson as json 9 | 10 | # copied from indexer_common 11 | STOPWORDS = set([ 12 | "A", "AND", "ARE", "AS", "AT", "BE", "BUT", "BY", 13 | "FOR", "IF", "IN", "INTO", "IS", "IT", 14 | "NO", "NOT", "OF", "ON", "OR", "SUCH", 15 | "THAT", "THE", "THEIR", "THEN", "THERE", "THESE", 16 | "THEY", "THIS", "TO", "WAS", "WILL", "WITH" 17 | ]) 18 | 19 | cfg = ConfigParser.ConfigParser({'password':'', 'port':''}) 20 | cfg.read(os.path.dirname(__file__)+"/../config.ini") 21 | pgname = cfg.get('rdbms', 'name') 22 | pguser = cfg.get('rdbms', 'user') 23 | pgpwd = cfg.get('rdbms', 'password') 24 | pghost = cfg.get('rdbms', 'host') 25 | pgport = cfg.get('rdbms', 'port') 26 | 27 | optparams = {} 28 | if pghost: optparams['host'] = pghost 29 | if pgport: optparams['port'] = pgport 30 | 31 | conn = psycopg2.connect(database=pgname, 32 | user=pguser, 33 | password=pgpwd, 34 | **optparams) 35 | c = conn.cursor() 36 | 37 | def escape_sql_like(s): 38 | return s.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') 39 | 40 | def suggest(query): 41 | words = query.upper().split() 42 | fullwords = [w for w in words[:-1] 43 | if w not in STOPWORDS] 44 | partialword = words[-1] 45 | if fullwords: 46 | sql = 'select distinct(_textid) from __words where _word=%s' 47 | intersect = '\nINTERSECT\n'.join([sql]*len(fullwords)) 48 | constraint = ' and _textid in ('+intersect+')' 49 | else: 50 | constraint = '' 51 | sql = ('select _word from __words where _word like %s '+constraint+ 52 | 'group by _word order by count(*) desc limit 10') 53 | c.execute(sql, (escape_sql_like(partialword)+'_%',)+tuple(fullwords)) 54 | words = [w[0].lower() for w in c.fetchall()] 55 | conn.rollback() 56 | return json.dumps([query, words]) 57 | 58 | def application(environ, start_response): 59 | # don't care about the URL here - just consider 60 | # the q query parameter 61 | query = urllib.unquote(environ['QUERY_STRING']) 62 | if not query: 63 | start_response('404 Not Found',[('Content-Type', 'text/html')]) 64 | return ['Not found', 65 | 'The q query parameter is missing'] 66 | result = suggest(query) 67 | start_response('200 OK', 68 | [('Content-type', 'application/x-suggestions+json'), 69 | ('Content-length', str(len(result)))]) 70 | return [result] 71 | 72 | if __name__=='__main__': 73 | from wsgiref.simple_server import make_server 74 | httpd = make_server('', 8086, application) 75 | print "Serving on port 8086..." 76 | httpd.serve_forever() 77 | -------------------------------------------------------------------------------- /scripts/update_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Update rietveld tables when the email address changes 3 | # this updates auth_user.email, codereview_account.email 4 | # and codereview_issue.cc, based on the nosy list s 5 | # it does not update codereview_issue.reviewers, since it would be 6 | # too expensive to search all issues, and since the reviewers list 7 | # is something that has to be filled out separately in Rietveld. 8 | # It also does not update codereview_message, since these have already 9 | # been sent with the email addresses recorded in the database. 10 | # 11 | # This script is now part of rietveldreactor 12 | 13 | import sys, os, base64, cPickle 14 | sys.path.insert(1,'/home/roundup/roundup/lib/python2.5/site-packages') 15 | import roundup.instance 16 | 17 | verbose = sys.argv[1:] == ['-v'] 18 | 19 | homedir = os.path.join(os.path.dirname(__file__), "..") 20 | tracker = roundup.instance.open(homedir) 21 | db = tracker.open('admin') 22 | c = db.cursor 23 | 24 | c.execute('select a.id, b.email, a._address from _user a, auth_user b ' 25 | 'where a.id=b.id and a._address != b.email') 26 | for user, old, new in c.fetchall(): 27 | old = old.decode('ascii') 28 | new = new.decode('ascii') 29 | if verbose: 30 | print "Update user", user, 'from', old, 'to ', new 31 | c.execute('update auth_user set email=%s where id=%s', (new, user)) 32 | c.execute('update codereview_account set email=%s where id=%s', (new, user)) 33 | # find issues where user is on nosy 34 | c.execute('select nodeid,cc from issue_nosy, codereview_issue ' 35 | 'where linkid=%s and nodeid=id', (user,)) 36 | for nodeid, cc in c.fetchall(): 37 | cc = cPickle.loads(base64.decodestring(cc)) 38 | if verbose: 39 | print " Update issue", nodeid, cc 40 | try: 41 | cc[cc.index(old)] = new 42 | except ValueError: 43 | cc.append(new) 44 | cc = base64.encodestring(cPickle.dumps(cc)) 45 | c.execute('update codereview_issue set cc=%s where id=%s', (cc, nodeid)) 46 | 47 | db.conn.commit() 48 | 49 | 50 | -------------------------------------------------------------------------------- /scripts/updatecc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys, os, urllib2, logging, datetime 3 | basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | sys.path.append(basedir+"/rietveld") 5 | os.environ["DJANGO_SETTINGS_MODULE"]="settings" 6 | import gae2django 7 | gae2django.install(server_software='Django') 8 | 9 | from codereview.models import (Repository, Branch, Patch, 10 | PatchSet, Issue, Content) 11 | from django.db import connection, transaction 12 | c = connection.cursor() 13 | 14 | for issue in Issue.objects.all(): 15 | c.execute("select _address from _user,issue_nosy where nodeid=%s and id=linkid", 16 | (issue.id,)) 17 | issue.cc = [r[0] for r in c.fetchall()] 18 | issue.save() 19 | --------------------------------------------------------------------------------