~mvo/software-center/trivial-renaming

« back to all changes in this revision

Viewing changes to softwarecenter/db/reviews.py

merged review code

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
 
 
3
# Copyright (C) 2009 Canonical
 
4
#
 
5
# Authors:
 
6
#  Michael Vogt
 
7
#
 
8
# This program is free software; you can redistribute it and/or modify it under
 
9
# the terms of the GNU General Public License as published by the Free Software
 
10
# Foundation; version 3.
 
11
#
 
12
# This program is distributed in the hope that it will be useful, but WITHOUT
 
13
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
14
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
15
# details.
 
16
#
 
17
# You should have received a copy of the GNU General Public License along with
 
18
# this program; if not, write to the Free Software Foundation, Inc.,
 
19
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
20
 
 
21
import cPickle
 
22
import gio
 
23
import gzip
 
24
import glib
 
25
import locale
 
26
import os
 
27
import json
 
28
import random
 
29
import StringIO
 
30
import time
 
31
import urllib
 
32
import weakref
 
33
import xml.dom.minidom
 
34
 
 
35
import softwarecenter.distro
 
36
 
 
37
from softwarecenter.db.database import Application
 
38
from softwarecenter.utils import *
 
39
from softwarecenter.paths import *
 
40
 
 
41
class ReviewStats(object):
 
42
    def __init__(self, app):
 
43
        self.app = app
 
44
        self.avg_rating = None
 
45
        self.nr_reviews = 0
 
46
    def __repr__(self):
 
47
        return "[ReviewStats '%s' rating='%s' nr_reviews='%s']" % (self.app, self.avg_rating, self.nr_reviews)
 
48
 
 
49
class Review(object):
 
50
    """A individual review object """
 
51
    def __init__(self, app):
 
52
        # a softwarecenter.db.database.Application object
 
53
        self.app = app
 
54
        # the review items that the object fills in
 
55
        self.id = None
 
56
        self.language = None
 
57
        self.summary = ""
 
58
        self.text = ""
 
59
        self.package_version = None
 
60
        self.date = None
 
61
        self.rating = None
 
62
        self.person = None
 
63
    def __repr__(self):
 
64
        return "[Review id=%s text='%s' person='%s']" % (self.id, self.text, self.person)
 
65
    def to_xml(self):
 
66
        return """<review app_name="%s" package_name="%s" id="%s" language="%s" 
 
67
data="%s" rating="%s" reviewer_name="%s">
 
68
<summary>%s</summary><text>%s</text></review>""" % (
 
69
            self.app.appname, self.app.pkgname,
 
70
            self.id, self.language, self.date, self.rating, 
 
71
            self.person, self.summary, self.text)
 
72
 
 
73
class ReviewLoader(object):
 
74
    """A loader that returns a review object list"""
 
75
 
 
76
    # cache the ReviewStats
 
77
    REVIEW_STATS_CACHE = {}
 
78
    REVIEW_STATS_CACHE_FILE = SOFTWARE_CENTER_CACHE_DIR+"/review-stats.p"
 
79
 
 
80
    def __init__(self, distro=None):
 
81
        self.distro = distro
 
82
        if not self.distro:
 
83
            self.distro = softwarecenter.distro.get_distro()
 
84
        if os.path.exists(self.REVIEW_STATS_CACHE_FILE):
 
85
            try:
 
86
                self.REVIEW_STATS_CACHE = cPickle.load(open(self.REVIEW_STATS_CACHE_FILE))
 
87
            except:
 
88
                logging.exception("review stats cache load failure")
 
89
                os.rename(self.REVIEW_STATS_CACHE_FILE, self.REVIEW_STATS_CACHE_FILE+".fail")
 
90
 
 
91
    def get_reviews(self, application, callback):
 
92
        """run callback f(app, review_list) 
 
93
           with list of review objects for the given
 
94
           db.database.Application object
 
95
        """
 
96
        return []
 
97
 
 
98
    def get_review_stats(self, application):
 
99
        """return a ReviewStats (number of reviews, rating)
 
100
           for a given application. this *must* be super-fast
 
101
           as it is called a lot during tree view display
 
102
        """
 
103
        # check cache
 
104
        if application in self.REVIEW_STATS_CACHE:
 
105
            return self.REVIEW_STATS_CACHE[application]
 
106
        return None
 
107
 
 
108
    def refresh_review_stats(self, callback):
 
109
        """ get the review statists and call callback when its there """
 
110
        pass
 
111
 
 
112
    def save_review_stats_cache_file(self):
 
113
        """ save review stats cache file in xdg cache dir """
 
114
        cachedir = SOFTWARE_CENTER_CACHE_DIR
 
115
        if not os.path.exists(cachedir):
 
116
            os.makedirs(cachedir)
 
117
        cPickle.dump(self.REVIEW_STATS_CACHE,
 
118
                      open(self.REVIEW_STATS_CACHE_FILE, "w"))
 
119
 
 
120
class ReviewLoaderXMLAsync(ReviewLoader):
 
121
    """ get xml (or gzip compressed xml) """
 
122
 
 
123
    def _gio_review_input_callback(self, source, result):
 
124
        app = source.get_data("app")
 
125
        callback = source.get_data("callback")
 
126
        try:
 
127
            xml_str = source.read_finish(result)
 
128
        except glib.GError, e:
 
129
            # ignore read errors, most likely transient
 
130
            return callback(app, [])
 
131
        # check for gzip header
 
132
        if xml_str.startswith("\37\213"):
 
133
            gz=gzip.GzipFile(fileobj=StringIO.StringIO(xml_str))
 
134
            xml_str = gz.read()
 
135
        dom = xml.dom.minidom.parseString(xml_str)
 
136
        reviews = []
 
137
        for review_xml in dom.getElementsByTagName("review"):
 
138
            appname = review_xml.getAttribute("app_name")
 
139
            pkgname = review_xml.getAttribute("package_name")
 
140
            app = Application(appname, pkgname)
 
141
            review = Review(app)
 
142
            review.id = review_xml.getAttribute("id")
 
143
            review.date = review_xml.getAttribute("date")
 
144
            review.rating = review_xml.getAttribute("rating")
 
145
            review.person = review_xml.getAttribute("reviewer_name")
 
146
            review.language = review_xml.getAttribute("language")
 
147
            summary_elements = review_xml.getElementsByTagName("summary")
 
148
            if summary_elements and summary_elements[0].childNodes:
 
149
                review.summary = summary_elements[0].childNodes[0].data
 
150
            review_elements = review_xml.getElementsByTagName("text")
 
151
            if review_elements and review_elements[0].childNodes:
 
152
                review.text = review_elements[0].childNodes[0].data
 
153
            reviews.append(review)
 
154
        # run callback
 
155
        callback(app, reviews)
 
156
 
 
157
    def _gio_review_read_callback(self, source, result):
 
158
        app = source.get_data("app")
 
159
        callback = source.get_data("callback")
 
160
        try:
 
161
            stream=source.read_finish(result)
 
162
        except glib.GError, e:
 
163
            print e, source, result
 
164
            # 404 means no review
 
165
            if e.code == 404:
 
166
                return callback(app, [])
 
167
            # raise other errors
 
168
            raise
 
169
        stream.set_data("app", app)
 
170
        stream.set_data("callback", callback)
 
171
        # FIXME: static size here as first argument sucks, but it seems
 
172
        #        like there is a bug in the python bindings, I can not pass
 
173
        #        -1 or anything like this
 
174
        stream.read_async(128*1024, self._gio_review_input_callback)
 
175
 
 
176
    def get_reviews(self, app, callback):
 
177
        """ get a specific review and call callback when its available"""
 
178
        url = self.distro.REVIEWS_URL % app.pkgname
 
179
        if app.appname:
 
180
            url += "/%s" % app.appname
 
181
        logging.debug("looking for review at '%s'" % url)
 
182
        f=gio.File(url)
 
183
        f.read_async(self._gio_review_read_callback)
 
184
        f.set_data("app", app)
 
185
        f.set_data("callback", callback)
 
186
 
 
187
    # review stats code
 
188
    def _gio_review_stats_input_callback(self, source, result):
 
189
        callback = source.get_data("callback")
 
190
        try:
 
191
            xml_str = source.read_finish(result)
 
192
        except glib.GError, e:
 
193
            # ignore read errors, most likely transient
 
194
            return
 
195
        # check for gzip header
 
196
        if xml_str.startswith("\37\213"):
 
197
            gz=gzip.GzipFile(fileobj=StringIO.StringIO(xml_str))
 
198
            xml_str = gz.read()
 
199
        dom = xml.dom.minidom.parseString(xml_str)
 
200
        review_stats = {}
 
201
        # FIXME: look at root element like:
 
202
        #  "<review-statistics origin="ubuntu" distroseries="lucid" language="en">"
 
203
        # to verify we got the data we expected
 
204
        for review_stats_xml in dom.getElementsByTagName("review"):
 
205
            appname = review_stats_xml.getAttribute("app_name")
 
206
            pkgname = review_stats_xml.getAttribute("package_name")
 
207
            app = Application(appname, pkgname)
 
208
            stats = ReviewStats(app)
 
209
            stats.nr_reviews = int(review_stats_xml.getAttribute("count"))
 
210
            stats.avg_rating = float(review_stats_xml.getAttribute("average"))
 
211
            review_stats[app] = stats
 
212
        # update review_stats dict
 
213
        self.REVIEW_STATS_CACHE = review_stats
 
214
        self.save_review_stats_cache_file()
 
215
        # run callback
 
216
        callback()
 
217
 
 
218
    def _gio_review_stats_read_callback(self, source, result):
 
219
        callback = source.get_data("callback")
 
220
        try:
 
221
            stream=source.read_finish(result)
 
222
        except glib.GError, e:
 
223
            print e, source, result
 
224
            raise
 
225
        stream.set_data("callback", callback)
 
226
        # FIXME: static size here as first argument sucks, but it seems
 
227
        #        like there is a bug in the python bindings, I can not pass
 
228
        #        -1 or anything like this
 
229
        stream.read_async(128*1024, self._gio_review_stats_input_callback)
 
230
 
 
231
    def refresh_review_stats(self, callback):
 
232
        """ get the review statists and call callback when its there """
 
233
        url = self.distro.REVIEW_STATS_URL
 
234
        f=gio.File(url)
 
235
        f.set_data("callback", callback)
 
236
        f.read_async(self._gio_review_stats_read_callback)
 
237
 
 
238
class ReviewLoaderIpsum(ReviewLoader):
 
239
    """ a test review loader that does not do any network io
 
240
        and returns random lorem ipsum review texts
 
241
    """
 
242
    #This text is under public domain
 
243
    #Lorem ipsum
 
244
    #Cicero
 
245
    LOREM=u"""lorem ipsum "dolor" äöü sit amet consetetur sadipscing elitr sed diam nonumy
 
246
eirmod tempor invidunt ut labore et dolore magna aliquyam erat sed diam
 
247
voluptua at vero eos et accusam et justo duo dolores et ea rebum stet clita
 
248
kasd gubergren no sea takimata sanctus est lorem ipsum dolor sit amet lorem
 
249
ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod
 
250
tempor invidunt ut labore et dolore magna aliquyam erat sed diam voluptua at
 
251
vero eos et accusam et justo duo dolores et ea rebum stet clita kasd
 
252
gubergren no sea takimata sanctus est lorem ipsum dolor sit amet lorem ipsum
 
253
dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor
 
254
invidunt ut labore et dolore magna aliquyam erat sed diam voluptua at vero
 
255
eos et accusam et justo duo dolores et ea rebum stet clita kasd gubergren no
 
256
sea takimata sanctus est lorem ipsum dolor sit amet
 
257
 
 
258
duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
 
259
molestie consequat vel illum dolore eu feugiat nulla facilisis at vero eros
 
260
et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril
 
261
delenit augue duis dolore te feugait nulla facilisi lorem ipsum dolor sit
 
262
amet consectetuer adipiscing elit sed diam nonummy nibh euismod tincidunt ut
 
263
laoreet dolore magna aliquam erat volutpat
 
264
 
 
265
ut wisi enim ad minim veniam quis nostrud exerci tation ullamcorper suscipit
 
266
lobortis nisl ut aliquip ex ea commodo consequat duis autem vel eum iriure
 
267
dolor in hendrerit in vulputate velit esse molestie consequat vel illum
 
268
dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio
 
269
dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te
 
270
feugait nulla facilisi
 
271
 
 
272
nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet
 
273
doming id quod mazim placerat facer possim assum lorem ipsum dolor sit amet
 
274
consectetuer adipiscing elit sed diam nonummy nibh euismod tincidunt ut
 
275
laoreet dolore magna aliquam erat volutpat ut wisi enim ad minim veniam quis
 
276
nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea
 
277
commodo consequat
 
278
 
 
279
duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
 
280
molestie consequat vel illum dolore eu feugiat nulla facilisis
 
281
 
 
282
at vero eos et accusam et justo duo dolores et ea rebum stet clita kasd
 
283
gubergren no sea takimata sanctus est lorem ipsum dolor sit amet lorem ipsum
 
284
dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor
 
285
invidunt ut labore et dolore magna aliquyam erat sed diam voluptua at vero
 
286
eos et accusam et justo duo dolores et ea rebum stet clita kasd gubergren no
 
287
sea takimata sanctus est lorem ipsum dolor sit amet lorem ipsum dolor sit
 
288
amet consetetur sadipscing elitr at accusam aliquyam diam diam dolore
 
289
dolores duo eirmod eos erat et nonumy sed tempor et et invidunt justo labore
 
290
stet clita ea et gubergren kasd magna no rebum sanctus sea sed takimata ut
 
291
vero voluptua est lorem ipsum dolor sit amet lorem ipsum dolor sit amet
 
292
consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore
 
293
et dolore magna aliquyam erat
 
294
 
 
295
consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore
 
296
et dolore magna aliquyam erat sed diam voluptua at vero eos et accusam et
 
297
justo duo dolores et ea rebum stet clita kasd gubergren no sea takimata
 
298
sanctus est lorem ipsum dolor sit amet lorem ipsum dolor sit amet consetetur
 
299
sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore et dolore
 
300
magna aliquyam erat sed diam voluptua at vero eos et accusam et justo duo
 
301
dolores et ea rebum stet clita kasd gubergren no sea takimata sanctus est
 
302
lorem ipsum dolor sit amet lorem ipsum dolor sit amet consetetur sadipscing
 
303
elitr sed diam nonumy eirmod tempor invidunt ut labore et dolore magna
 
304
aliquyam erat sed diam voluptua at vero eos et accusam et justo duo dolores
 
305
et ea rebum stet clita kasd gubergren no sea takimata sanctus est lorem
 
306
ipsum dolor sit amet"""
 
307
    USERS = ["Joe Doll", "John Foo", "Cat Lala", "Foo Grumpf", "Bar Tender", "Baz Lightyear"]
 
308
    SUMMARIES = ["Cool", "Medium", "Bad", "Too difficult"]
 
309
    def _random_person(self):
 
310
        return random.choice(self.USERS)
 
311
    def _random_text(self):
 
312
        return random.choice(self.LOREM.split("\n\n"))
 
313
    def _random_summary(self):
 
314
        return random.choice(self.SUMMARIES)
 
315
    def get_reviews(self, application, callback):
 
316
        reviews = []
 
317
        for i in range(0,random.randint(0,6)):
 
318
            review = Review(application)
 
319
            review.id = random.randint(1,500)
 
320
            review.rating = random.randint(1,5)
 
321
            review.summary = self._random_summary()
 
322
            review.date = time.ctime(time.time())
 
323
            review.person = self._random_person()
 
324
            review.text = self._random_text().replace("\n","")
 
325
            reviews.append(review)
 
326
        callback(application, reviews)
 
327
    def refresh_review_stats(self, callback):
 
328
        review_stats = []
 
329
        callback(review_stats)
 
330
 
 
331
review_loader = None
 
332
def get_review_loader():
 
333
    """ 
 
334
    factory that returns a reviews loader singelton
 
335
    """
 
336
    global review_loader
 
337
    if not review_loader:
 
338
        if "SOFTWARE_CENTER_IPSUM_REVIEWS" in os.environ:
 
339
            review_loader = ReviewLoaderIpsum()
 
340
        else:
 
341
            review_loader = ReviewLoaderXMLAsync()
 
342
    return review_loader
 
343
 
 
344
if __name__ == "__main__":
 
345
    def callback(app, reviews):
 
346
        print "app callback:"
 
347
        print app, reviews
 
348
    def stats_callback(stats):
 
349
        print "stats:"
 
350
        print stats
 
351
    from softwarecenter.db.database import Application
 
352
    app = Application("7zip",None)
 
353
    #loader = ReviewLoaderIpsum()
 
354
    #print loader.get_reviews(app, callback)
 
355
    #print loader.get_review_stats(app)
 
356
    app = Application("totem","totem")
 
357
    loader = ReviewLoaderXMLAsync()
 
358
    loader.get_review_stats(stats_callback)
 
359
    loader.get_reviews(app, callback)
 
360
    import gtk
 
361
    gtk.main()