Merge lp:~aaronp/software-center/modify-reviews into lp:software-center

Proposed by Aaron Peachey
Status: Merged
Merged at revision: 1887
Proposed branch: lp:~aaronp/software-center/modify-reviews
Merge into: lp:software-center
Diff against target: 947 lines (+542/-65)
8 files modified
setup.py (+2/-0)
softwarecenter/backend/reviews.py (+87/-0)
softwarecenter/backend/rnrclient_pristine.py (+18/-0)
softwarecenter/paths.py (+2/-0)
softwarecenter/ui/gtk/appdetailsview.py (+11/-0)
softwarecenter/ui/gtk/appdetailsview_gtk.py (+10/-1)
softwarecenter/ui/gtk/widgets/reviews.py (+109/-13)
utils/submit_review.py (+303/-51)
To merge this branch: bzr merge lp:~aaronp/software-center/modify-reviews
Reviewer Review Type Date Requested Status
software-store-developers Pending
Review via email: mp+63693@code.launchpad.net

Description of the change

implements edit and delete review functionality

production rnr server currently does not support the edit functionality (achuni advised ~3 weeks before next release) but the changes to USC are designed to fail gracefully on errors from the server

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added symlink 'data/delete_review.py'
2=== target is u'../utils/delete_review.py'
3=== added symlink 'data/modify_review.py'
4=== target is u'../utils/modify_review.py'
5=== modified file 'setup.py'
6--- setup.py 2011-05-31 21:12:53 +0000
7+++ setup.py 2011-06-07 12:26:16 +0000
8@@ -79,6 +79,8 @@
9 "utils/submit_review.py",
10 "utils/report_review.py",
11 "utils/submit_usefulness.py",
12+ "utils/delete_review.py",
13+ "utils/modify_review.py",
14 "utils/get_reviews_helper.py",
15 "utils/get_review_stats_helper.py",
16 "utils/get_useful_votes_helper.py",
17
18=== modified file 'softwarecenter/backend/reviews.py'
19--- softwarecenter/backend/reviews.py 2011-06-01 08:04:32 +0000
20+++ softwarecenter/backend/reviews.py 2011-06-07 12:26:16 +0000
21@@ -47,6 +47,8 @@
22 SUBMIT_REVIEW_APP,
23 REPORT_REVIEW_APP,
24 SUBMIT_USEFULNESS_APP,
25+ DELETE_REVIEW_APP,
26+ MODIFY_REVIEW_APP,
27 GET_REVIEWS_HELPER,
28 GET_REVIEW_STATS_HELPER,
29 GET_USEFUL_VOTES_HELPER,
30@@ -184,6 +186,8 @@
31 self.usefulness_favorable = 0
32 # this will be set if tryint to submit usefulness for this review failed
33 self.usefulness_submit_error = False
34+ self.delete_error = False
35+ self.modify_error = False
36 def __repr__(self):
37 return "[Review id=%s review_text='%s' reviewer_username='%s']" % (
38 self.id, self.review_text, self.reviewer_username)
39@@ -328,6 +332,29 @@
40 (pid, stdin, stdout, stderr) = glib.spawn_async(
41 cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True)
42 glib.child_watch_add(pid, self._on_submit_usefulness_finished, (review_id, is_useful, stdout, callback))
43+
44+ def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):
45+ cmd = [os.path.join(datadir, DELETE_REVIEW_APP),
46+ "--review-id", "%s" % review_id,
47+ "--parent-xid", "%s" % parent_xid,
48+ "--datadir", datadir,
49+ ]
50+ (pid, stdin, stdout, stderr) = glib.spawn_async(
51+ cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True)
52+ glib.child_watch_add(pid, self._on_delete_review_finished, (review_id, callback))
53+
54+ def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
55+ """ this spawns the UI for writing a new review and
56+ adds it automatically to the reviews DB """
57+ cmd = [os.path.join(datadir, MODIFY_REVIEW_APP),
58+ "--parent-xid", "%s" % parent_xid,
59+ "--iconname", iconname,
60+ "--datadir", "%s" % datadir,
61+ "--review-id", "%s" % review_id,
62+ ]
63+ (pid, stdin, stdout, stderr) = glib.spawn_async(
64+ cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True)
65+ glib.child_watch_add(pid, self._on_modify_review_finished, (review_id, stdout, callback))
66
67 # internal callbacks/helpers
68 def _on_submit_review_finished(self, pid, status, (app, stdout_fd, callback)):
69@@ -369,6 +396,64 @@
70 callback(app, self._reviews[app])
71 break
72
73+ def _on_delete_review_finished(self, pid, status, (review_id, callback)):
74+ """ called when delete_review finished"""
75+ exitcode = os.WEXITSTATUS(status)
76+ if exitcode == 0:
77+ LOG.debug("delete id %s " % review_id)
78+ for (app, reviews) in self._reviews.iteritems():
79+ for review in reviews:
80+ if str(review.id) == str(review_id):
81+ # remove the one we don't want to see anymore
82+ self._reviews[app].remove(review)
83+ callback(app, self._reviews[app])
84+ break
85+ else:
86+ LOG.debug("delete review id=%s failed with exitcode %s" % (
87+ review_id, exitcode))
88+ for (app, reviews) in self._reviews.iteritems():
89+ for review in reviews:
90+ if str(review.id) == str(review_id):
91+ review.delete_error = exitcode
92+ callback(app, self._reviews[app])
93+ break
94+
95+ def _on_modify_review_finished(self, pid, status, (review_id, stdout_fd, callback)):
96+ """called when modify_review finished"""
97+ exitcode = os.WEXITSTATUS(status)
98+ LOG.debug("_on_modify_review_finished")
99+ stdout = ""
100+ while True:
101+ s = os.read(stdout_fd, 1024)
102+ if not s: break
103+ stdout += s
104+ LOG.debug("stdout from modify_review: '%s'" % stdout)
105+ if exitcode == 0:
106+ try:
107+ review_json = simplejson.loads(stdout)
108+ except simplejson.decoder.JSONDecodeError:
109+ LOG.error("failed to parse '%s'" % stdout)
110+ return
111+ mod_review = ReviewDetails.from_dict(review_json)
112+
113+ for (app, reviews) in self._reviews.iteritems():
114+ for review in reviews:
115+ if str(review.id) == str(review_id):
116+ # remove the one we don't want to see anymore
117+ self._reviews[app].remove(review)
118+ self._reviews[app].insert(0, Review.from_piston_mini_client(mod_review))
119+ callback(app, self._reviews[app])
120+ break
121+ else:
122+ LOG.debug("modify review id=%s failed with exitcode %s" % (
123+ review_id, exitcode))
124+ for (app, reviews) in self._reviews.iteritems():
125+ for review in reviews:
126+ if str(review.id) == str(review_id):
127+ review.modify_error = exitcode
128+ callback(app, self._reviews[app])
129+ break
130+
131 def _on_submit_usefulness_finished(self, pid, status, (review_id, is_useful, stdout_fd, callback)):
132 """ called when report_usefulness finished """
133 exitcode = os.WEXITSTATUS(status)
134@@ -421,6 +506,7 @@
135 self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
136 self._reviews = {}
137
138+
139 def _update_rnrclient_offline_state(self):
140 # this needs the lp:~mvo/piston-mini-client/offline-mode branch
141 self.rnrclient._offline_mode = not network_state_is_connected()
142@@ -506,6 +592,7 @@
143 # stats
144 def refresh_review_stats(self, callback):
145 """ public api, refresh the available statistics """
146+
147 try:
148 mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE)
149 days_delta = int((time.time() - mtime) // (24*60*60))
150
151=== modified file 'softwarecenter/backend/rnrclient_pristine.py'
152--- softwarecenter/backend/rnrclient_pristine.py 2011-04-11 14:09:54 +0000
153+++ softwarecenter/backend/rnrclient_pristine.py 2011-06-07 12:26:16 +0000
154@@ -167,3 +167,21 @@
155
156 return self._get('usefulness/', args=data,
157 scheme=PUBLIC_API_SCHEME)
158+
159+ @validate('review_id', int)
160+ @returns_json
161+ def delete_review(self, review_id):
162+ """Delete a review"""
163+ return self._post('/reviews/delete/%s/' % review_id, data={},
164+ scheme=AUTHENTICATED_API_SCHEME)
165+
166+ @validate('review_id', int)
167+ @validate('rating', int)
168+ @validate_pattern('summary', r'[^\n]+')
169+ @validate_pattern('review_text', r'[^\n]+')
170+ @returns(ReviewDetails)
171+ def modify_review(self, review_id, rating, summary, review_text):
172+ """Modify an existing review"""
173+ data = {'rating':rating, 'summary':summary, 'review_text':review_text}
174+ return self._put('/reviews/modify/%s/' % review_id, data=data,
175+ scheme=AUTHENTICATED_API_SCHEME)
176
177=== modified file 'softwarecenter/paths.py'
178--- softwarecenter/paths.py 2011-05-31 07:26:24 +0000
179+++ softwarecenter/paths.py 2011-06-07 12:26:16 +0000
180@@ -58,6 +58,8 @@
181 SUBMIT_REVIEW_APP = "submit_review.py"
182 REPORT_REVIEW_APP = "report_review.py"
183 SUBMIT_USEFULNESS_APP = "submit_usefulness.py"
184+MODIFY_REVIEW_APP = "modify_review.py"
185+DELETE_REVIEW_APP = "delete_review.py"
186 GET_REVIEWS_HELPER = "get_reviews_helper.py"
187 GET_REVIEW_STATS_HELPER = "get_review_stats_helper.py"
188 GET_USEFUL_VOTES_HELPER = "get_useful_votes_helper.py"
189
190=== modified file 'softwarecenter/ui/gtk/appdetailsview.py'
191--- softwarecenter/ui/gtk/appdetailsview.py 2011-06-05 22:50:04 +0000
192+++ softwarecenter/ui/gtk/appdetailsview.py 2011-06-07 12:26:16 +0000
193@@ -131,6 +131,17 @@
194 review_id, is_useful, parent_xid, self.datadir,
195 self._reviews_ready_callback)
196
197+ def _review_modify(self, review_id):
198+ parent_xid = get_parent_xid(self)
199+ self.review_loader.spawn_modify_review_ui(
200+ parent_xid, self.appdetails.icon, self.datadir, review_id,
201+ self._reviews_ready_callback)
202+
203+ def _review_delete(self, review_id):
204+ parent_xid = get_parent_xid(self)
205+ self.review_loader.spawn_delete_review_ui(
206+ review_id, parent_xid, self.datadir, self._reviews_ready_callback)
207+
208 # public interface
209 def reload(self):
210 """ reload the package cache, this goes straight to the backend """
211
212=== modified file 'softwarecenter/ui/gtk/appdetailsview_gtk.py'
213--- softwarecenter/ui/gtk/appdetailsview_gtk.py 2011-06-05 22:50:04 +0000
214+++ softwarecenter/ui/gtk/appdetailsview_gtk.py 2011-06-07 12:26:16 +0000
215@@ -850,6 +850,7 @@
216 if my_votes:
217 self.reviews.update_useful_votes(my_votes)
218
219+ self.reviews.clear()
220 for review in reviews_data:
221 self.reviews.add_review(review)
222 self.reviews.configure_reviews_ui()
223@@ -1150,6 +1151,8 @@
224 self.reviews.connect("new-review", self._on_review_new)
225 self.reviews.connect("report-abuse", self._on_review_report_abuse)
226 self.reviews.connect("submit-usefulness", self._on_review_submit_usefulness)
227+ self.reviews.connect("modify-review", self._on_review_modify)
228+ self.reviews.connect("delete-review", self._on_review_delete)
229 self.reviews.connect("more-reviews-clicked", self._on_more_reviews_clicked)
230 self.reviews.connect("different-review-language-clicked", self._on_reviews_in_different_language_clicked)
231 vb.pack_start(self.reviews, False)
232@@ -1197,7 +1200,13 @@
233 return False
234
235 def _on_review_new(self, button):
236- self._review_write_new()
237+ self._review_write_new()
238+
239+ def _on_review_modify(self, button, review_id):
240+ self._review_modify(review_id)
241+
242+ def _on_review_delete(self, button, review_id):
243+ self._review_delete(review_id)
244
245 def _on_review_report_abuse(self, button, review_id):
246 self._review_report_abuse(str(review_id))
247
248=== modified file 'softwarecenter/ui/gtk/widgets/reviews.py'
249--- softwarecenter/ui/gtk/widgets/reviews.py 2011-06-05 22:50:04 +0000
250+++ softwarecenter/ui/gtk/widgets/reviews.py 2011-06-07 12:26:16 +0000
251@@ -714,6 +714,12 @@
252 'submit-usefulness':(gobject.SIGNAL_RUN_FIRST,
253 gobject.TYPE_NONE,
254 (gobject.TYPE_PYOBJECT, bool)),
255+ 'modify-review':(gobject.SIGNAL_RUN_FIRST,
256+ gobject.TYPE_NONE,
257+ (gobject.TYPE_PYOBJECT,)),
258+ 'delete-review':(gobject.SIGNAL_RUN_FIRST,
259+ gobject.TYPE_NONE,
260+ (gobject.TYPE_PYOBJECT,)),
261 'more-reviews-clicked':(gobject.SIGNAL_RUN_FIRST,
262 gobject.TYPE_NONE,
263 () ),
264@@ -957,14 +963,24 @@
265 self.yes_like = None
266 self.no_like = None
267 self.status_box = gtk.HBox()
268+ self.delete_status_box = gtk.HBox()
269+ self.delete_error_img = gtk.Image()
270+ self.delete_error_img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_SMALL_TOOLBAR)
271 self.submit_error_img = gtk.Image()
272 self.submit_error_img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_SMALL_TOOLBAR)
273 self.submit_status_spinner = gtk.Spinner()
274 self.submit_status_spinner.set_size_request(12,12)
275+ self.delete_status_spinner = gtk.Spinner()
276+ self.delete_status_spinner.set_size_request(12,12)
277 self.acknowledge_error = mkit.VLinkButton(_("<small>OK</small>"))
278 self.acknowledge_error.set_underline(True)
279 self.acknowledge_error.set_subdued(True)
280+ self.delete_acknowledge_error = mkit.VLinkButton(_("<small>OK</small>"))
281+ self.delete_acknowledge_error.set_underline(True)
282+ self.delete_acknowledge_error.set_subdued(True)
283 self.usefulness_error = False
284+ self.delete_error = False
285+ self.modify_error = False
286
287 self.pack_start(self.header, False)
288 self.pack_start(self.body, False)
289@@ -1011,6 +1027,11 @@
290 if reviews:
291 reviews.emit("report-abuse", self.id)
292
293+ def _on_modify_clicked(self, button):
294+ reviews = self.get_ancestor(UIReviewsList)
295+ if reviews:
296+ reviews.emit("modify-review", self.id)
297+
298 def _on_useful_clicked(self, btn, is_useful):
299 reviews = self.get_ancestor(UIReviewsList)
300 if reviews:
301@@ -1066,6 +1087,56 @@
302 # example raw_date str format: 2011-01-28 19:15:21
303 return datetime.datetime.strptime(raw_date_str, '%Y-%m-%d %H:%M:%S')
304
305+ def _delete_ui_update(self, type, current_user_reviewer=False, action=None):
306+ self._hide_delete_elements()
307+ if type == 'renew':
308+ self._build_delete_flag_ui(current_user_reviewer)
309+ return
310+ if type == 'progress':
311+ self.delete_status_spinner.start()
312+ self.delete_status_spinner.show()
313+ self.delete_status_label = gtk.Label("<small><b>%s</b></small>" % _(u"Deleting now\u2026"))
314+ self.delete_status_box.pack_start(self.delete_status_spinner, False)
315+ self.delete_status_label.set_use_markup(True)
316+ self.delete_status_label.set_padding(2,0)
317+ self.delete_status_box.pack_start(self.delete_status_label,False)
318+ self.delete_status_label.show()
319+ if type == 'error':
320+ self.delete_error_img.show()
321+ self.delete_status_label = gtk.Label("<small><b>%s</b></small>" % _("Error %s review" % action))
322+ self.delete_status_box.pack_start(self.delete_error_img, False)
323+ self.delete_status_label.set_use_markup(True)
324+ self.delete_status_label.set_padding(2,0)
325+ self.delete_status_box.pack_start(self.delete_status_label,False)
326+ self.delete_status_label.show()
327+ self.delete_acknowledge_error.show()
328+ self.delete_status_box.pack_start(self.delete_acknowledge_error,False)
329+ self.delete_acknowledge_error.connect('clicked', self._on_delete_error_acknowledged, current_user_reviewer)
330+ self.delete_status_box.show()
331+ self.footer.pack_end(self.delete_status_box, False)
332+ return
333+
334+ def _on_delete_clicked(self, btn):
335+ reviews = self.get_ancestor(UIReviewsList)
336+ if reviews:
337+ self._delete_ui_update('progress')
338+ reviews.emit("delete-review", self.id)
339+
340+ def _on_delete_error_acknowledged(self, button, current_user_reviewer):
341+ self.delete_error = False
342+ self._delete_ui_update('renew', current_user_reviewer)
343+
344+ def _hide_delete_elements(self):
345+ """ hide all delete elements """
346+ for attr in ["complain", "edit", "delete", "delete_status_spinner",
347+ "delete_error_img", "delete_status_box", "delete_status_label",
348+ "delete_acknowledge_error", "flagbox"
349+ ]:
350+ o = getattr(self, attr, None)
351+ if o:
352+ o.hide()
353+ return
354+
355 def _build(self, review_data, app_version, logged_in_person, useful_votes):
356
357 # all the attributes of review_data may need markup escape,
358@@ -1081,6 +1152,8 @@
359 self.useful_total = useful_total = review_data.usefulness_total
360 useful_favorable = review_data.usefulness_favorable
361 useful_submit_error = review_data.usefulness_submit_error
362+ delete_error = review_data.delete_error
363+ modify_error = review_data.modify_error
364
365 dark_color = self.style.dark[gtk.STATE_NORMAL]
366 m = self._whom_when_markup(self.person, displayname, cur_t, dark_color)
367@@ -1133,19 +1206,12 @@
368
369 self._build_usefulness_ui(current_user_reviewer, useful_total,
370 useful_favorable, useful_votes, useful_submit_error)
371-
372- # Translators: This link is for flagging a review as inappropriate.
373- # To minimize repetition, if at all possible, keep it to a single word.
374- # If your language has an obvious verb, it won't need a question mark.
375- self.complain = mkit.VLinkButton('<small>%s</small>' % _('Inappropriate?'))
376- self.complain.set_subdued(True)
377- self.complain.set_underline(True)
378- self.footer.pack_end(self.complain, False)
379- self.complain.connect('clicked', self._on_report_abuse_clicked)
380- # FIXME: dynamically update this on network changes
381- self.complain.set_sensitive(network_state_is_connected())
382- self.body.connect('size-allocate', self._on_allocate, stars,
383- summary, text, who_when, version_lbl, self.complain)
384+
385+ self.flagbox = gtk.HBox()
386+ self._build_delete_flag_ui(current_user_reviewer, delete_error, modify_error)
387+ self.footer.pack_end(self.flagbox,False)
388+ self.body.connect('size-allocate', self._on_allocate, stars, summary, text, who_when, version_lbl, self.flagbox)
389+
390 return
391
392 def _build_usefulness_ui(self, current_user_reviewer, useful_total,
393@@ -1264,6 +1330,36 @@
394
395 return gtk.Label('<small>%s</small>' % s)
396
397+ def _build_delete_flag_ui(self, current_user_reviewer, delete_error=False, modify_error=False):
398+ if delete_error:
399+ self._delete_ui_update('error', current_user_reviewer, 'deleting')
400+ elif modify_error:
401+ self._delete_ui_update('error', current_user_reviewer, 'modifying')
402+ else:
403+ if current_user_reviewer:
404+ self.edit = mkit.VLinkButton('<small>%s</small>' %_('Edit'))
405+ self.delete = mkit.VLinkButton('<small>%s</small>' %_('Delete'))
406+ self.edit.set_underline(True)
407+ self.delete.set_underline(True)
408+ self.edit.set_subdued(True)
409+ self.delete.set_subdued(True)
410+ self.flagbox.pack_start(self.edit, False)
411+ self.flagbox.pack_start(self.delete, False)
412+ self.edit.connect('clicked', self._on_modify_clicked)
413+ self.delete.connect('clicked', self._on_delete_clicked)
414+ else:
415+ # Translators: This link is for flagging a review as inappropriate.
416+ # To minimize repetition, if at all possible, keep it to a single word.
417+ # If your language has an obvious verb, it won't need a question mark.
418+ self.complain = mkit.VLinkButton('<small>%s</small>' % _('Inappropriate?'))
419+ self.complain.set_subdued(True)
420+ self.complain.set_underline(True)
421+ self.complain.set_sensitive(network_state_is_connected())
422+ self.flagbox.pack_start(self.complain, False)
423+ self.complain.connect('clicked', self._on_report_abuse_clicked)
424+ self.flagbox.show_all()
425+ return
426+
427 def _whom_when_markup(self, person, displayname, cur_t, dark_color):
428 nice_date = get_nice_date_string(cur_t)
429 #dt = datetime.datetime.utcnow() - cur_t
430
431=== added symlink 'utils/delete_review.py'
432=== target is u'submit_review.py'
433=== added symlink 'utils/modify_review.py'
434=== target is u'submit_review.py'
435=== modified file 'utils/submit_review.py'
436--- utils/submit_review.py 2011-06-06 08:49:12 +0000
437+++ utils/submit_review.py 2011-06-07 12:26:16 +0000
438@@ -69,6 +69,7 @@
439 distro = get_distro()
440 SERVER_ROOT=distro.REVIEWS_SERVER
441
442+
443 # server status URL
444 SERVER_STATUS_URL = SERVER_ROOT+"/server-status/"
445
446@@ -117,6 +118,12 @@
447 def submit_usefulness(self, review_id, is_useful):
448 self.emit("transmit-start", review_id)
449 self.worker_thread.pending_usefulness.put((int(review_id), is_useful))
450+ def modify_review(self, review_id, review):
451+ self.emit("transmit-start", review_id)
452+ self.worker_thread.pending_modify.put((int(review_id), review))
453+ def delete_review(self, review_id):
454+ self.emit("transmit-start", review_id)
455+ self.worker_thread.pending_delete.put(int(review_id))
456 def server_status(self):
457 self.worker_thread.pending_server_status()
458 def shutdown(self):
459@@ -140,6 +147,8 @@
460 self.pending_reviews = Queue()
461 self.pending_reports = Queue()
462 self.pending_usefulness = Queue()
463+ self.pending_modify = Queue()
464+ self.pending_delete = Queue()
465 self.pending_server_status = Queue()
466 self._shutdown = False
467 # FIXME: instead of a binary value we need the state associated
468@@ -176,11 +185,15 @@
469 self._submit_reviews_if_pending()
470 self._submit_reports_if_pending()
471 self._submit_usefulness_if_pending()
472+ self._submit_modify_if_pending()
473+ self._submit_delete_if_pending()
474 time.sleep(0.2)
475 if (self._shutdown and
476 self.pending_reviews.empty() and
477 self.pending_usefulness.empty() and
478- self.pending_reports.empty()):
479+ self.pending_reports.empty() and
480+ self.pending_modify.empty() and
481+ self.pending_delete.empty()):
482 return
483
484 # usefulness
485@@ -207,6 +220,59 @@
486 self._write_exception_html_log_if_needed(e)
487 self._transmit_state = TRANSMIT_STATE_ERROR
488 self.pending_usefulness.task_done()
489+
490+ #modify
491+ def queue_modify(self, modification):
492+ """ queue a new review modification request for sending to LP """
493+ logging.debug("queue_modify %s %s" % modification)
494+ self.pending_modify.put(modification)
495+
496+ def _submit_modify_if_pending(self):
497+ """ the actual modify function """
498+ while not self.pending_modify.empty():
499+ logging.debug("_modify_review")
500+ self._transmit_state = TRANSMIT_STATE_INPROGRESS
501+ (review_id, review) = self.pending_modify.get()
502+ summary = review['summary']
503+ review_text = review['review_text']
504+ rating = review['rating']
505+ try:
506+ res = self.rnrclient.modify_review(review_id=review_id,
507+ summary=summary,
508+ review_text=review_text,
509+ rating=rating)
510+ self._transmit_state = TRANSMIT_STATE_DONE
511+ sys.stdout.write(simplejson.dumps(vars(res)))
512+ except Exception as e:
513+ logging.exception("modify_review")
514+ err_str = self._get_error_messages(e)
515+ self._write_exception_html_log_if_needed(e)
516+ self._transmit_state = TRANSMIT_STATE_ERROR
517+ self._transmit_error_str = err_str
518+ self.pending_modify.task_done()
519+
520+ #delete
521+ def queue_delete(self, deletion):
522+ """ queue a new deletion request for sending to LP """
523+ logging.debug("queue_delete review id: %s" % deletion)
524+ self.pending_delete.put(deletion)
525+
526+ def _submit_delete_if_pending(self):
527+ """ the actual deletion """
528+ while not self.pending_delete.empty():
529+ logging.debug("POST delete")
530+ self._transmit_state = TRANSMIT_STATE_INPROGRESS
531+ review_id = self.pending_delete.get()
532+ try:
533+ res = self.rnrclient.delete_review(review_id=review_id)
534+ self._transmit_state = TRANSMIT_STATE_DONE
535+ sys.stdout.write(simplejson.dumps(res))
536+ except Exception as e:
537+ logging.exception("delete_review failed")
538+ self._write_exception_html_log_if_needed(e)
539+ self._transmit_error_str = _("Failed to delete review")
540+ self._transmit_state = TRANSMIT_STATE_ERROR
541+ self.pending_delete.task_done()
542
543 # reports
544 def queue_report(self, report):
545@@ -350,6 +416,9 @@
546 #submit success image
547 self.submit_success_img = gtk.Image()
548 self.submit_success_img.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_SMALL_TOOLBAR)
549+ #submit warn image
550+ self.submit_warn_img = gtk.Image()
551+ self.submit_warn_img.set_from_stock(gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_SMALL_TOOLBAR)
552 #label size to prevent image or spinner from resizing
553 self.label_transmit_status.set_size_request(-1, gtk.icon_size_lookup(gtk.ICON_SIZE_SMALL_TOOLBAR)[1])
554
555@@ -472,6 +541,8 @@
556 """method to separate the updating of status icon/spinner and message in the submit review window,
557 takes a type (progress, fail, success) as a string and a message string then updates status area accordingly"""
558 self._clear_status_imagery()
559+ self.label_transmit_status.set_text("")
560+
561 if type == "progress":
562 self.status_hbox.pack_start(self.submit_spinner, False)
563 self.status_hbox.reorder_child(self.submit_spinner, 0)
564@@ -490,6 +561,11 @@
565 self.status_hbox.reorder_child(self.submit_success_img, 0)
566 self.submit_success_img.show()
567 self.label_transmit_status.set_text(message)
568+ elif type == "warning":
569+ self.status_hbox.pack_start(self.submit_warn_img, False)
570+ self.status_hbox.reorder_child(self.submit_warn_img, 0)
571+ self.submit_warn_img.show()
572+ self.label_transmit_status.set_text(message)
573
574 def _clear_status_imagery(self):
575 self.detail_expander.hide()
576@@ -514,13 +590,18 @@
577 except TypeError:
578 pass
579
580+ try:
581+ self.status_hbox.query_child_packing(self.submit_warn_img)
582+ self.status_hbox.remove(self.submit_warn_img)
583+ except TypeError:
584+ pass
585+
586 return
587-
588+
589
590 class SubmitReviewsApp(BaseApp):
591 """ review a given application or package """
592
593-
594 STAR_SIZE = (32, 32)
595 APP_ICON_SIZE = 48
596 #character limits for text boxes and hurdles for indicator changes
597@@ -532,8 +613,9 @@
598 ERROR_COLOUR = "FF0000"
599 SUBMIT_MESSAGE = _("Submitting Review")
600 FAILURE_MESSAGE = _("Failed to submit review")
601+ SUCCESS_MESSAGE = _("Review submitted")
602
603- def __init__(self, app, version, iconname, origin, parent_xid, datadir):
604+ def __init__(self, app, version, iconname, origin, parent_xid, datadir, action="submit", review_id=0):
605 BaseApp.__init__(self, datadir, "submit_review.ui")
606 self.datadir = datadir
607 # legal fineprint, do not change without consulting a lawyer
608@@ -546,20 +628,6 @@
609 self.submit_window.connect("destroy", self.on_button_cancel_clicked)
610 self._add_spellcheck_to_textview(self.textview_review)
611
612- # gwibber stuff
613- self.gwibber_combo = gtk.combo_box_new_text()
614- #cells = self.gwibber_combo.get_cells()
615- #cells[0].set_property("ellipsize", pango.ELLIPSIZE_END)
616- self.gwibber_hbox.pack_start(self.gwibber_combo, True)
617- if "SOFTWARE_CENTER_GWIBBER_MOCK_USERS" in os.environ:
618- self.gwibber_helper = GwibberHelperMock()
619- else:
620- self.gwibber_helper = GwibberHelper()
621-
622- #get a dict with a saved gwibber_send (boolean) and gwibber account_id for persistent state
623- self.gwibber_prefs = self._get_gwibber_prefs()
624-
625- # interactive star rating
626 self.star_rating = StarRatingSelector(0, star_size=self.STAR_SIZE)
627 self.star_caption = StarCaption()
628
629@@ -573,33 +641,85 @@
630 self.rating_hbox.reorder_child(self.star_caption, 1)
631
632 self.review_buffer = self.textview_review.get_buffer()
633-
634+
635 self.detail_expander.hide()
636+
637+ self.retrieve_api = RatingsAndReviewsAPI()
638
639+
640 # data
641 self.app = app
642 self.version = version
643 self.origin = origin
644 self.iconname = iconname
645+ self.action = action
646+ self.review_id = int(review_id)
647
648- # title
649- self.submit_window.set_title(_("Review %s") % self.app.name)
650+ # parent xid
651+ if parent_xid:
652+ win = gtk.gdk.window_foreign_new(int(parent_xid))
653+ if win:
654+ self.submit_window.realize()
655+ self.submit_window.window.set_transient_for(win)
656
657+ self.submit_window.set_position(gtk.WIN_POS_MOUSE)
658+
659 self.review_summary_entry.connect('changed', self._on_mandatory_text_entry_changed)
660 self.star_rating.connect('changed', self._on_mandatory_fields_changed)
661 self.review_buffer.connect('changed', self._on_text_entry_changed)
662
663 # gwibber stuff
664+ self.gwibber_combo = gtk.combo_box_new_text()
665+ #cells = self.gwibber_combo.get_cells()
666+ #cells[0].set_property("ellipsize", pango.ELLIPSIZE_END)
667+ self.gwibber_hbox.pack_start(self.gwibber_combo, True)
668+ if "SOFTWARE_CENTER_GWIBBER_MOCK_USERS" in os.environ:
669+ self.gwibber_helper = GwibberHelperMock()
670+ else:
671+ self.gwibber_helper = GwibberHelper()
672+
673+ #get a dict with a saved gwibber_send (boolean) and gwibber account_id for persistent state
674+ self.gwibber_prefs = self._get_gwibber_prefs()
675+
676+ # gwibber stuff
677 self._setup_gwibber_gui()
678
679- # parent xid
680- if parent_xid:
681- win = gtk.gdk.window_foreign_new(int(parent_xid))
682- if win:
683- self.submit_window.realize()
684- self.submit_window.window.set_transient_for(win)
685+ #now setup rest of app based on whether submit or modify
686+ if self.action == "submit":
687+ self._init_submit()
688+ elif self.action == "modify":
689+ self._init_modify()
690
691- self.submit_window.set_position(gtk.WIN_POS_MOUSE)
692+ def _init_submit(self):
693+ self.submit_window.set_title(_("Review %s" % self.app.name))
694+
695+ def _init_modify(self):
696+ self._populate_review()
697+ self.submit_window.set_title(_("Modify Your %s Review" % self.app.name))
698+ self.button_post.set_label(_("Modify"))
699+ self.SUBMIT_MESSAGE = _("Updating your review")
700+ self.FAILURE_MESSAGE = _("Failed to edit review")
701+ self.SUCCESS_MESSAGE = _("Review updated")
702+ self._enable_or_disable_post_button()
703+
704+ def _populate_review(self):
705+ try:
706+ review_data = self.retrieve_api.get_review(review_id=self.review_id)
707+ app = Application(pkgname=review_data.package_name)
708+ self.app = app
709+ self.review_summary_entry.set_text(review_data.summary)
710+ self.star_rating.set_rating(review_data.rating)
711+ self.review_buffer.set_text(review_data.review_text)
712+ #save original review field data, for comparison purposes when user makes changes to fields
713+ self.orig_summary_text = review_data.summary
714+ self.orig_star_rating = review_data.rating
715+ self.orig_review_text = review_data.review_text
716+ self.version = review_data.version
717+ self.origin = review_data.origin
718+ return
719+ except piston_mini_client.APIError:
720+ logging.warn('Unable to retrieve review id %s for editing. Exiting' % self.review_id)
721+ self.quit(2)
722
723 def _setup_details(self, widget, app, iconname, version, display_name):
724 # icon shazam
725@@ -654,8 +774,41 @@
726 review_chars and review_chars <= self.REVIEW_CHAR_LIMITS[0] and
727 self.star_rating.get_rating()):
728 self.button_post.set_sensitive(True)
729+ self._change_status("clear", "")
730 else:
731 self.button_post.set_sensitive(False)
732+ self._change_status("clear", "")
733+
734+ #set post button insensitive, if review being modified is the same as what is currently in the UI fields
735+ #checks if 'original' review attributes exist to avoid exceptions when this method has been called prior to review being retrieved
736+ if self.action == 'modify' and hasattr(self, "orig_star_rating"):
737+ if self._modify_review_is_the_same():
738+ self.button_post.set_sensitive(False)
739+ self._change_status("warning", _("Can't submit unmodified"))
740+ else:
741+ self._change_status("clear", "")
742+
743+ def _modify_review_is_the_same(self):
744+ '''checks if review fields are the same as the review being modified and returns true if so'''
745+
746+ #perform an initial check on character counts to return False if any don't match, avoids doing unnecessary string comparisons
747+ if (self.review_summary_entry.get_text_length() != len(self.orig_summary_text) or
748+ self.review_buffer.get_char_count() != len(self.orig_review_text)):
749+ return False
750+ #compare rating
751+ if self.star_rating.get_rating() != self.orig_star_rating:
752+ return False
753+ #compare summary text
754+ if self.review_summary_entry.get_text() != self.orig_summary_text:
755+ return False
756+ #compare review text
757+ if self.review_buffer.get_text(self.review_buffer.get_start_iter(),
758+ self.review_buffer.get_end_iter()) != self.orig_review_text:
759+ return False
760+ return True
761+
762+
763+
764
765 def _check_summary_character_count(self):
766 summary_chars = self.review_summary_entry.get_text_length()
767@@ -737,7 +890,14 @@
768 review.rating = self.star_rating.get_rating()
769 review.package_version = self.version
770 review.origin = self.origin
771- self.api.submit_review(review)
772+
773+ if self.action == "submit":
774+ self.api.submit_review(review)
775+ elif self.action == "modify":
776+ changes = {'review_text':review.text,
777+ 'summary':review.summary,
778+ 'rating':review.rating}
779+ self.api.modify_review(self.review_id, changes)
780
781 def login_successful(self, display_name):
782 self.main_notebook.set_current_page(1)
783@@ -887,7 +1047,7 @@
784
785 def _success_status(self):
786 """Updates status area to show success for 2 seconds then allows window to proceed"""
787- self._change_status("success", _("Review submitted."))
788+ self._change_status("success", self.SUCCESS_MESSAGE)
789 while gtk.events_pending():
790 gtk.main_iteration(False)
791 time.sleep(2)
792@@ -995,7 +1155,6 @@
793 self.textview_report.get_buffer().connect(
794 "changed", self._enable_or_disable_report_button)
795
796-
797 # data
798 self.review_id = review_id
799
800@@ -1097,27 +1256,67 @@
801 self.api.submit_usefulness(self.review_id, self.is_useful)
802
803 def on_transmit_failure(self, api, trans, error):
804- print "exiting - error: %s" % error
805- self.api.shutdown()
806- self.quit(2)
807-
808- # override parents run to only trigger login (and subsequent
809- # events) but no UI, if this is commented out, there is some
810- # stub ui that can be useful for testing
811- def run(self):
812- self.login()
813-
814- # override UI update methods from BaseApp to prevent them
815- # causing errors if called when UI is hidden
816- def _clear_status_imagery(self):
817- pass
818-
819- def _change_status(self, type, message):
820- pass
821-
822+ logging.warn("exiting - error: %s" % error)
823+ self.api.shutdown()
824+ self.quit(2)
825+
826+ # override parents run to only trigger login (and subsequent
827+ # events) but no UI, if this is commented out, there is some
828+ # stub ui that can be useful for testing
829+ def run(self):
830+ self.login()
831+
832+ # override UI update methods from BaseApp to prevent them
833+ # causing errors if called when UI is hidden
834+ def _clear_status_imagery(self):
835+ pass
836+
837+ def _change_status(self, type, message):
838+ pass
839+
840+class DeleteReviewApp(BaseApp):
841+ SUBMIT_MESSAGE = _(u"Deleting review\u2026")
842+ FAILURE_MESSAGE = _("Failed to delete review")
843+
844+ def __init__(self, review_id, parent_xid, datadir):
845+ # uses same UI as submit usefulness because
846+ # (a) it isn't shown and (b) it's similar in usage
847+ BaseApp.__init__(self, datadir, "submit_usefulness.ui")
848+ # data
849+ self.review_id = review_id
850+ # no UI except for error conditions
851+ self.parent_xid = parent_xid
852+
853+ # override behavior of baseapp here as we don't actually
854+ # have a UI by default
855+ def _get_parent_xid_for_login_window(self):
856+ return self.parent_xid
857+
858+ def login_successful(self, display_name):
859+ logging.debug("delete review")
860+ self.main_notebook.set_current_page(1)
861+ self.api.delete_review(self.review_id)
862+
863+ def on_transmit_failure(self, api, trans, error):
864+ logging.warn("exiting - error: %s" % error)
865+ self.api.shutdown()
866+ self.quit(2)
867+
868+ # override parents run to only trigger login (and subsequent
869+ # events) but no UI, if this is commented out, there is some
870+ # stub ui that can be useful for testing
871+ def run(self):
872+ self.login()
873+
874+ # override UI update methods from BaseApp to prevent them
875+ # causing errors if called when UI is hidden
876+ def _clear_status_imagery(self):
877+ pass
878+
879+ def _change_status(self, type, message):
880+ pass
881
882 if __name__ == "__main__":
883-
884 try:
885 locale.setlocale(locale.LC_ALL, "")
886 except:
887@@ -1219,6 +1418,59 @@
888 parent_xid=options.parent_xid,
889 is_useful=int(options.is_useful))
890 usefulness_app.run()
891-
892+
893+ if "delete_review" in sys.argv[0]:
894+ #check options
895+ parser.add_option("", "--review-id")
896+ parser.add_option("", "--parent-xid")
897+ parser.add_option("", "--debug",
898+ action="store_true", default=False)
899+ (options, args) = parser.parse_args()
900+
901+ if not (options.review_id):
902+ parser.error(_("Missing review-id argument"))
903+
904+ if options.debug:
905+ logging.basicConfig(level=logging.DEBUG)
906+
907+ logging.debug("delete review mode")
908+
909+ delete_app = DeleteReviewApp(datadir=options.datadir,
910+ review_id=options.review_id,
911+ parent_xid=options.parent_xid)
912+ delete_app.run()
913+
914+ if "modify_review" in sys.argv[0]:
915+ # check options
916+ parser.add_option("", "--review-id")
917+ parser.add_option("-i", "--iconname")
918+ parser.add_option("", "--parent-xid")
919+ parser.add_option("", "--debug",
920+ action="store_true", default=False)
921+ (options, args) = parser.parse_args()
922+
923+ if not (options.review_id):
924+ parser.error(_("Missing review-id argument"))
925+
926+ if options.debug:
927+ logging.basicConfig(level=logging.DEBUG)
928+
929+ # personality
930+ logging.debug("modify_review mode")
931+
932+ # initialize and run
933+ modify_app = SubmitReviewsApp(datadir=options.datadir,
934+ app=None,
935+ parent_xid=options.parent_xid,
936+ iconname=options.iconname,
937+ origin=None,
938+ version=None,
939+ action="modify",
940+ review_id=options.review_id
941+ )
942+
943+ modify_app.run()
944+
945+
946 # main
947 gtk.main()

Subscribers

People subscribed via source and target branches