1 # vim:set encoding=utf-8:
2 ###############################################################################
3 # Copyright:
4 # © 2009 Sandro Tosi <morph@debian.org>
5 #
6 # License:
7 # Public Domain
8 ###############################################################################
10 # @see
12 # TODO: handle duplicate, usually resolution=duplicate
13 # TODO: enable XMLRPC interface, but upstream needs version >= 1.4.8
15 # *** Doc ***
16 #
17 # bugs url: http://server/path/issueNNNN
18 # base url: http://server/path
19 #
20 # We are lucky Roundup allows 2 native querying methods:
21 #
22 # * CSV export (the one currently used)
23 # * XML RPC
24 #
25 # Roundup allows for a lot of customization, in particular for status
26 # and resolution values. In order to get those values, surf to
27 #
28 # status: <base url>/status
29 # resolution: <base url>/resolution
30 #
31 # the same thing done by getStatus() and getResolution() method below.
32 #
33 # Anyhow, we need to visit those pages because we have to populate
34 # 'closing' and 'wontfix' config values.
35 #
36 # Examples urls:
37 #
38 # * issue url : http://bugs.python.org/issue764437
39 # * base uri : http://bugs.python.org
40 # * status url : http://bugs.python.org/status
41 # * resolution url: http://bugs.python.org/resolution
42 #
43 # How to write configuration entry:
44 #
45 # * uri must NOT have tailing /
46 # * write the 'closing' and 'wontfix' item following the above
47 # suggestions
49 import urllib, urlparse, cgi, re
50 from __init__ import *
52 class RoundupData:
53 def __init__(self, uri, id):
54 # to uri is the real 'uri' with '/issueNNNNN' added, we need "real uri"
55 uri_re = re.compile(r'(.*)/issue(.*)')
56 uri = uri_re.match(uri).group(1)
58 self.id = id or failwith(uri, "Roundup: no id")
60 self.status = self.getStatus(uri, id) or failwith(uri, "Roundup", exn=NoStatusExn)
61 self.resolution = self.getResolution(uri, id)
63 # NOT SUPPORTED YET
64 #if self.status == 'Duplicate':
65 # raise DupeExn(uri)
67 def getStatus(self, uri, id):
68 """get the issue status in two steps: get the numerical status from the issue page, then match that id against the <id, status name> list in $uri/status page"""
69 # we now obtain the issue status, as a numerical value
70 status_tpl = [('@action', 'export_csv'), ('@filter', 'id'), ('id', id), ('@columns', 'status')]
71 status_url = '/%s?%s' % ('issue', urllib.urlencode(status_tpl))
72 status_content = urllib.urlopen(uri + status_url).read().split('\r\n')
73 # then retrive <id, status name> list from $uri/status
74 statuslist_tpl = [('@action', 'export_csv'), ('@columns', 'id,name')]
75 statuslist_url = '/%s?%s' % ('status', urllib.urlencode(statuslist_tpl))
76 statuslist_map = urllib.urlopen(uri + statuslist_url).read().split('\r\n')
77 statuslist_dict = {}
78 # let's make a dict out of this list: keys are numerical id, values are status names
79 # we strip first ('id,name') and last ('\n') values from the map
80 for statusitem in statuslist_map[1:len(statuslist_map)-1]:
81 s = statusitem.split(',')
82 statuslist_dict[s[0]] = s[1]
83 # if it has 3 items, we have data (we hope)
84 if len(status_content) == 3:
85 return statuslist_dict[status_content[1]]
86 else:
87 return None
90 def getResolution(self, uri, id):
91 """get the issue resolution value in two steps: get the numerical resolution from the issue page, then match that id against the <id, resolution name> list in $uri/resolution page"""
92 # we now obtain the issue resolution, as a numerical value
93 resolv_tpl = [('@action', 'export_csv'), ('@filter', 'id'), ('id', id), ('@columns', 'resolution')]
94 resolv_url = '/%s?%s' % ('issue', urllib.urlencode(resolv_tpl))
95 resolv_content = urllib.urlopen(uri + resolv_url).read().split('\r\n')
96 # then retrive <id, status name> list from $uri/resolution - IF IT EXISTS!!
97 resolvlist_tpl = [('@action', 'export_csv'), ('@columns', 'id,name')]
98 resolvlist_url = '/%s?%s' % ('resolution', urllib.urlencode(resolvlist_tpl))
99 resolvlist_map = urllib.urlopen(uri + resolvlist_url).read().split('\r\n')
100 resolvlist_dict = {}
101 # let's make a dict out of this list: keys are numerical id, values are resolution names
102 # we strip first ('id,name') and last ('\n') values from the map
103 for resolvitem in resolvlist_map[1:len(resolvlist_map)-1]:
104 r = resolvitem.split(',')
105 resolvlist_dict[r[0]] = r[1]
106 # if it has 3 items, we have data (we hope)
107 if len(resolv_content) == 3 and resolv_content[1] != 'None':
108 return resolvlist_dict[resolv_content[1]]
109 else:
110 return None
112 pass
114 class RemoteRoundup(RemoteBts):
115 def __init__(self, cnf):
116 # save to an object attribute
117 self.cnf = cnf
119 bugre = r"^%(uri)s/issue([0-9]+)$"
120 urifmt = "%(uri)s/issue%(id)s"
121 RemoteBts.__init__(self, cnf, bugre, urifmt, RoundupData)
123 def _getClosingStatus(self):
124 """get the config values for closing state"""
125 if 'closing' in self.cnf:
126 return self.cnf['closing']
127 else:
128 return [None]
130 def _getWontfixStatus(self):
131 """get the config value for wontfix state"""
132 if 'wontfix' in self.cnf:
133 return self.cnf['wontfix']
134 else:
135 return [None]
137 def isClosing(self, status, resolution):
138 return status in self._getClosingStatus()
140 def isWontfix(self, status, resolution):
141 return (status in [self._getWontfixStatus()]) \
142 or (resolution in [self._getWontfixStatus()])
145 RemoteBts.register('roundup', RemoteRoundup)
