| 1 | #!/usr/bin/python |
|---|
| 2 | # -*- coding: utf-8 -*- |
|---|
| 3 | import gtk |
|---|
| 4 | import gobject |
|---|
| 5 | import urllib |
|---|
| 6 | from bug import * |
|---|
| 7 | from mechanize import Browser |
|---|
| 8 | from BeautifulSoup import BeautifulSoup |
|---|
| 9 | # You have to change this if you have another storage |
|---|
| 10 | # from auth_file import user |
|---|
| 11 | |
|---|
| 12 | #baseurl = "https://staging.launchpad.net/" |
|---|
| 13 | #baseurl = "https://launchpad.net/" |
|---|
| 14 | |
|---|
| 15 | class protocol: |
|---|
| 16 | |
|---|
| 17 | #constructor, common to all protocol file |
|---|
| 18 | def __init__(self,manager) : |
|---|
| 19 | #the user object contains login and password |
|---|
| 20 | self.user = manager |
|---|
| 21 | |
|---|
| 22 | def url(self): |
|---|
| 23 | zeurl = self.user.url |
|---|
| 24 | if zeurl.endswith("/") : |
|---|
| 25 | return zeurl |
|---|
| 26 | else : |
|---|
| 27 | return "%s/" %zeurl |
|---|
| 28 | |
|---|
| 29 | def login(self) : |
|---|
| 30 | return self.user.login |
|---|
| 31 | |
|---|
| 32 | def password(self) : |
|---|
| 33 | return self.user.password |
|---|
| 34 | |
|---|
| 35 | # LAUNCHPAD protocol using the web : suboptimal |
|---|
| 36 | |
|---|
| 37 | # Available informations for authentification are : |
|---|
| 38 | # - user.login() -> string that contains the login |
|---|
| 39 | # - user.password() -> string that contains the password |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | ### Launchpad only functions #### |
|---|
| 43 | |
|---|
| 44 | def htmlify(self, string) : |
|---|
| 45 | s1 = string.strip().replace(" "," ") |
|---|
| 46 | s2 = s1.strip().replace(">",">") |
|---|
| 47 | s3 = s2.strip().replace("<","<") |
|---|
| 48 | return s3 |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | ################INFORMATION ABOUT THE BTS ################################ |
|---|
| 52 | |
|---|
| 53 | # Send back the name of the protocol as a simple string |
|---|
| 54 | |
|---|
| 55 | @staticmethod |
|---|
| 56 | def Name(): |
|---|
| 57 | return "launchpadstaging_web" |
|---|
| 58 | |
|---|
| 59 | # send the name of the bts this protocol will talk too. |
|---|
| 60 | # this is intended for future multi-protocol support |
|---|
| 61 | # and will enable bookmarks sharing between differents protocols |
|---|
| 62 | # that speaks to the same bts |
|---|
| 63 | def btsName(self): |
|---|
| 64 | return "launchpad" |
|---|
| 65 | |
|---|
| 66 | ################BUG RETRIEVING IN THE BTS ################################# |
|---|
| 67 | |
|---|
| 68 | # Functions related to bugs in your BTS |
|---|
| 69 | # Functions return an object "bug" (see bug.py) |
|---|
| 70 | # |
|---|
| 71 | # You have to implement the following functions : |
|---|
| 72 | # - retrieveBug(int) |
|---|
| 73 | # - getBugUrl(int) |
|---|
| 74 | |
|---|
| 75 | # Retrieve bug take an int, the bug number, and construct a bug object |
|---|
| 76 | # this bug object is then returned |
|---|
| 77 | def retrieveBug(self,nbr): |
|---|
| 78 | zebug = bug(nbr) |
|---|
| 79 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 80 | # this is what you want to modify to support your own protocol |
|---|
| 81 | zeurl="%sbugs/%s" %(self.url(),str(nbr)) |
|---|
| 82 | f= urllib.urlopen(zeurl) |
|---|
| 83 | # ici on a le corps principal |
|---|
| 84 | # faudrait faire un extract pour soulager la mémoire |
|---|
| 85 | soup = BeautifulSoup(f).findAll(id="maincontent")[0] |
|---|
| 86 | title = soup.findAll('h1')[0].contents[0] |
|---|
| 87 | # no bug of this number here |
|---|
| 88 | if title == "Page not found" : |
|---|
| 89 | # a negative number means that the bug doesn't exist |
|---|
| 90 | zebug.setNbr(-1) |
|---|
| 91 | else : |
|---|
| 92 | tbody = soup.findAll('tbody')[0].findAll('td') |
|---|
| 93 | product = tbody[0].a.contents[0] |
|---|
| 94 | status = tbody[1].contents[0] |
|---|
| 95 | importance = tbody[2].contents[0] |
|---|
| 96 | string_tmp = tbody[3].a |
|---|
| 97 | # Here we look if the bug is assigned to someone or not |
|---|
| 98 | if string_tmp != None : |
|---|
| 99 | assigned = string_tmp.contents[2].strip() |
|---|
| 100 | else : |
|---|
| 101 | #Assigned to nobody |
|---|
| 102 | assigned = "Nobody yet" |
|---|
| 103 | #need to prettify content |
|---|
| 104 | content='' |
|---|
| 105 | p = soup.findAll('div')[3].findAll('div')[1].findAll('p', recursive=0) |
|---|
| 106 | #p = soup.findAll('div', recursive=0)[1].div.findAll('p', recursive=0) |
|---|
| 107 | for i in p: |
|---|
| 108 | #print i |
|---|
| 109 | #print "--------" |
|---|
| 110 | for j in i.contents: |
|---|
| 111 | #essai pour avoir les urls |
|---|
| 112 | #print j |
|---|
| 113 | jj = j.string |
|---|
| 114 | #jj=j |
|---|
| 115 | #print jj |
|---|
| 116 | # we get rid of <br /> tags |
|---|
| 117 | if jj != None and jj != "<br />" : |
|---|
| 118 | # here we remove manually all   |
|---|
| 119 | newj = self.htmlify(jj) |
|---|
| 120 | #newj = jj |
|---|
| 121 | #newj = jj.strip().replace(" "," ") |
|---|
| 122 | content = "%s %s"%(content,newj) |
|---|
| 123 | else : |
|---|
| 124 | content = "%s\n"%(content) |
|---|
| 125 | content = "%s\n\n"%content |
|---|
| 126 | |
|---|
| 127 | zebug.setTitle(title) |
|---|
| 128 | zebug.setStatus(status) |
|---|
| 129 | zebug.setPackage(product) |
|---|
| 130 | zebug.setImportance(importance) |
|---|
| 131 | zebug.setDescription(content) |
|---|
| 132 | zebug.setAssignee(assigned) |
|---|
| 133 | zebug.setCommentable(self.__isCommentable(nbr)) |
|---|
| 134 | |
|---|
| 135 | array_com = soup.findAll('div', recursive=0)[1].findAll('div','boardComment') |
|---|
| 136 | j = 0 |
|---|
| 137 | com_counter = 0 |
|---|
| 138 | for i in array_com : |
|---|
| 139 | details = i.contents[1].findAll('a') |
|---|
| 140 | title = details[1].contents[0].strip() |
|---|
| 141 | poster = details[1].contents[0] |
|---|
| 142 | body = i.contents[3].findAll('p') |
|---|
| 143 | com_counter += 1 |
|---|
| 144 | content="" |
|---|
| 145 | for z in body : |
|---|
| 146 | phrase = '' |
|---|
| 147 | for ligne in z.contents : |
|---|
| 148 | if ligne.string != None : |
|---|
| 149 | phrase += ligne.string.strip() |
|---|
| 150 | content += self.htmlify(phrase) |
|---|
| 151 | content += "\n\n" |
|---|
| 152 | newcom = comment(com_counter,content,poster,title,"00/00/00") |
|---|
| 153 | zebug.addComment(newcom) |
|---|
| 154 | |
|---|
| 155 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 156 | return zebug |
|---|
| 157 | |
|---|
| 158 | # getBugUrl takes a int, the bug number, and return the URL of |
|---|
| 159 | # the bug in order to open it in a browser |
|---|
| 160 | def getBugUrl(self,nbr) : |
|---|
| 161 | bugnbr = str(nbr) |
|---|
| 162 | return "%sbugs/%s" %(self.url(),bugnbr) |
|---|
| 163 | |
|---|
| 164 | |
|---|
| 165 | |
|---|
| 166 | ################SEARCH IN THE BTS ################################# |
|---|
| 167 | |
|---|
| 168 | # The functions Search will perform a search |
|---|
| 169 | # in the BTS |
|---|
| 170 | # All results matching the search are added to a |
|---|
| 171 | # gtk.ListStore with attributes in the following orders : |
|---|
| 172 | # gobject.TYPE_INT : number of the bug |
|---|
| 173 | # gobject.TYPE_STRING : package |
|---|
| 174 | # gobject.TYPE_STRING : title of the bug |
|---|
| 175 | # gobject.TYPE_STRING : importance |
|---|
| 176 | # gobject.TYPE_STRING : status |
|---|
| 177 | # |
|---|
| 178 | # You have to implement the following search methods for your BTS : |
|---|
| 179 | # - genericSearch(string) |
|---|
| 180 | # - packageSearch(string,string) |
|---|
| 181 | # - advancedSearch |
|---|
| 182 | # - packageExist(string) (return a boolean, not a listStore) |
|---|
| 183 | |
|---|
| 184 | |
|---|
| 185 | #private function to create a new result gtk.ListStore |
|---|
| 186 | def __newResult(self) : |
|---|
| 187 | return gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) |
|---|
| 188 | |
|---|
| 189 | # content is an array of bug numbers |
|---|
| 190 | def __populateResult(self, results, content, typeofsearch, arg_package): |
|---|
| 191 | parse = content.body.findAll('tbody') |
|---|
| 192 | # if no results |
|---|
| 193 | if len(parse) == 0 : |
|---|
| 194 | return results |
|---|
| 195 | else : |
|---|
| 196 | table = parse[1].findAll('tr') |
|---|
| 197 | for tr in table : |
|---|
| 198 | row = tr.findAll('td') |
|---|
| 199 | nbr = int(row[1].contents[0]) |
|---|
| 200 | title = row[2].a.contents[0] |
|---|
| 201 | if typeofsearch == 2 : |
|---|
| 202 | package = row[3].contents[0] |
|---|
| 203 | importance = row[4].contents[0] |
|---|
| 204 | status = row[5].contents[0] |
|---|
| 205 | elif typeofsearch == 3 : |
|---|
| 206 | package = arg_package |
|---|
| 207 | importance = row[3].contents[0] |
|---|
| 208 | status = row[4].contents[0] |
|---|
| 209 | else : |
|---|
| 210 | package = '' |
|---|
| 211 | importance = '' |
|---|
| 212 | status = '' |
|---|
| 213 | results.insert_before(None, [nbr, package, title, importance, status ]) |
|---|
| 214 | return results |
|---|
| 215 | |
|---|
| 216 | |
|---|
| 217 | # genericSearch on a given string search_str. |
|---|
| 218 | def genericSearch(self, search_str) : |
|---|
| 219 | #creating an empty result first |
|---|
| 220 | results= self.__newResult() |
|---|
| 221 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 222 | # this is what you want to modify to support your own protocol |
|---|
| 223 | #inserting dummies bugs |
|---|
| 224 | plus = search_str.replace(" ","+") |
|---|
| 225 | zeurl="%sdistros/ubuntu/+bugs?field.searchtext=%s" %(self.url(),plus) |
|---|
| 226 | f= urllib.urlopen(zeurl) |
|---|
| 227 | content=BeautifulSoup(f) |
|---|
| 228 | return self.__populateResult(results, content, 2, None) |
|---|
| 229 | |
|---|
| 230 | |
|---|
| 231 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 232 | #returning the results |
|---|
| 233 | #return results |
|---|
| 234 | |
|---|
| 235 | # search on a given string search_str but only in bugs of |
|---|
| 236 | # a given package |
|---|
| 237 | def packageSearch(self, package, search_str): |
|---|
| 238 | #creating an empty result first |
|---|
| 239 | results= self.__newResult() |
|---|
| 240 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 241 | # this is what you want to modify to support your own protocol |
|---|
| 242 | #inserting dummies bugs |
|---|
| 243 | # your package search must support a Null search_str |
|---|
| 244 | if search_str != None : |
|---|
| 245 | plus = search_str.replace(" ","+") |
|---|
| 246 | zeurl="%sdistros/ubuntu/+source/%s/+bugs?field.searchtext=%s" %(self.url(),package,plus) |
|---|
| 247 | else : |
|---|
| 248 | zeurl="%sdistros/ubuntu/+source/%s/+bugs" %(self.url(),package) |
|---|
| 249 | f= urllib.urlopen(zeurl) |
|---|
| 250 | content=BeautifulSoup(f) |
|---|
| 251 | return self.__populateResult(results, content, 3, package) |
|---|
| 252 | |
|---|
| 253 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 254 | #returning the results |
|---|
| 255 | #return results |
|---|
| 256 | |
|---|
| 257 | |
|---|
| 258 | # Return true is the package exist in the BTS, false if not |
|---|
| 259 | def packageExist(self, package): |
|---|
| 260 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 261 | # this is what you want to modify to support your own protocol |
|---|
| 262 | zeurl="%sproducts/%s" %(self.url(),package) |
|---|
| 263 | f= urllib.urlopen(zeurl) |
|---|
| 264 | #ugly launchpad hack ! |
|---|
| 265 | title=BeautifulSoup(f).html.head.title.contents[0] |
|---|
| 266 | return title != "Error: Page not found" |
|---|
| 267 | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|---|
| 268 | |
|---|
| 269 | #Return the different available status in the bts as a list |
|---|
| 270 | # this is used to build the advanced search interface |
|---|
| 271 | def availableStatus(self): |
|---|
| 272 | return ['Unconfirmed', 'Needs info', 'Rejected', 'Confirmed', 'In Progress', 'Fix Committed', 'Fix Released'] |
|---|
| 273 | |
|---|
| 274 | #Return the different available importante in the bts as a list |
|---|
| 275 | # this is used to build the advanced search interface |
|---|
| 276 | def availableImportance(self): |
|---|
| 277 | return ['Untriaged', 'Wishlist', 'Low', 'Medium', 'High', 'Critical'] |
|---|
| 278 | |
|---|
| 279 | ################MODIFYING A BUG ################################# |
|---|
| 280 | |
|---|
| 281 | # The functions that will allow us to modify a bug |
|---|
| 282 | # |
|---|
| 283 | # You have to implement the following search modify for your BTS : |
|---|
| 284 | # - postComment(string,string,string) |
|---|
| 285 | |
|---|
| 286 | #private function that log the user into Launchpad |
|---|
| 287 | # return a mechanize Browser object |
|---|
| 288 | def __login(self): |
|---|
| 289 | urllog="%s+login" %self.url() |
|---|
| 290 | br = Browser() |
|---|
| 291 | br.set_handle_robots(False) |
|---|
| 292 | br.open(urllog) |
|---|
| 293 | br.select_form(name="login") |
|---|
| 294 | br["loginpage_email"]=self.login() |
|---|
| 295 | br["loginpage_password"]=self.password() |
|---|
| 296 | response = br.submit() |
|---|
| 297 | return br |
|---|
| 298 | |
|---|
| 299 | def __isform_comment(self,htmlform) : |
|---|
| 300 | if htmlform.action.find("+addcomment") != -1 : |
|---|
| 301 | return True |
|---|
| 302 | else : |
|---|
| 303 | return False |
|---|
| 304 | |
|---|
| 305 | def __isCommentable(self,bugnbr) : |
|---|
| 306 | urlcom="%sbugs/%s" %(self.url(),bugnbr) |
|---|
| 307 | br = self.__login() |
|---|
| 308 | br.open(urlcom) |
|---|
| 309 | try : |
|---|
| 310 | br.select_form(predicate=self.__isform_comment) |
|---|
| 311 | return True |
|---|
| 312 | except : |
|---|
| 313 | return False |
|---|
| 314 | |
|---|
| 315 | def postComment(self,bugnbr,title,comment) : |
|---|
| 316 | urlcom="%sbugs/%s" %(self.url(),bugnbr) |
|---|
| 317 | br = self.__login() |
|---|
| 318 | br.open(urlcom) |
|---|
| 319 | br.select_form(predicate=self.__isform_comment) |
|---|
| 320 | # TODO : set the title |
|---|
| 321 | br["field.comment"]= comment |
|---|
| 322 | br.submit() |
|---|
| 323 | |
|---|
| 324 | |
|---|
| 325 | |
|---|
| 326 | |
|---|
| 327 | |
|---|
| 328 | |
|---|