iteration 1 - Use gobject for events in feed, summaryitem and feedlist
[straw.git] / src / lib / feeds.py
bloba483c3025f6951f8980ef871da738fc5ddd6ac0d
1 import locale
2 import gobject
3 import FeedDataRouter
4 import Config
5 from error import log
7 class FeedList(gobject.GObject):
9 __gsignals__ = {
10 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
11 'updated' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
12 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, gobject.TYPE_INT,)),
13 'imported' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
14 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
15 gobject.TYPE_BOOLEAN,)),
16 'deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
19 def __init__(self, init_seq = []):
20 gobject.GObject.__init__(self)
21 self.feedlist = []
22 #self.initialize_slots(Event.FeedsChangedSignal,
23 # Event.FeedDeletedSignal,
24 # Event.FeedCreatedSignal,
25 # Event.AllItemsReadSignal,
26 # Event.ItemReadSignal,
27 # Event.ItemsAddedSignal,
28 # Event.FeedPolledSignal,
29 # Event.FeedStatusChangedSignal,
30 # Event.FeedErrorStatusChangedSignal,
31 # Event.FeedsImportedSignal,
32 # Event.FeedDetailChangedSignal)
33 self._loading = False
35 def __iter__(self):
36 return iter(self.feedlist)
38 def load_data(self):
39 def _load(feedlist, parent):
40 for df in feedlist:
41 if isinstance(df, list):
42 #fc = straw.FeedCategory()
43 #fc.undump(df[0])
44 #self.append(parent, fc)
45 _load(df[1:], parent)
46 else:
47 f = Feed.create_empty_feed()
48 f.undump(df)
49 self.append(parent, f)
51 self._loading = True
52 feedlist = Config.get_instance().feeds
53 if feedlist:
54 _load(feedlist, None)
55 self._loading = False
56 self.emit('changed')
58 def _forward_signal(self, signal):
59 self.emit_signal(signal)
61 def connect_signals(self, ob):
62 ob.connect('info-updated', self.feed_detail_changed)
64 # these signals are forwarded so that listeners who just want to
65 # listen for a specific event regardless of what feed it came from can
66 # just connect to this feedlist instead of connecting to the
67 # individual feeds.
68 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
69 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
70 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
71 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
72 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
73 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
75 def __setitem__(self, key, value):
76 self.feedlist.__setitem__(key, value)
77 self.connect_signals(value)
78 self.save_feeds_and_notify(True)
80 def extend(self, parent, values, from_sub=False):
81 list.extend(self.feedlist, values)
82 for f in values:
83 f.parent = parent
84 self.connect_signals(f)
85 self.save_feeds()
86 self.emit('imported', values, parent, from_sub)
88 def append(self, parent, value):
89 self.feedlist.append(value)
90 value.parent = parent
91 self.connect_signals(value)
92 self.save_feeds()
93 self.emit('updated', value, parent, -1)
95 def insert(self, index, parent, value):
96 self.feedlist.insert(index, value)
97 value.parent = parent
98 self.connect_signals(value)
99 self.save_feeds()
100 self.emit('updated', value, parent, index)
102 def index(self, feed):
103 return self.feedlist.index(feed)
105 def reorder(self, move, delta):
106 k = self.feedlist[:]
107 move = list(move)
108 move.sort()
109 if delta > 0:
110 move.reverse()
111 if move[0] == 0 and delta < 0 or move[-1] == (len(self.feedlist) - 1) and delta > 0:
112 return
113 for m in move:
114 k[m + delta], k[m] = k[m], k[m + delta]
115 for i in range(len(k)):
116 list.__setitem__(self.feedlist, i, k[i])
117 self.save_feeds()
118 self.emit('changed')
120 def __delitem__(self, value):
121 feed = self.feedlist[value]
122 list.__delitem__(self.feedlist, value)
123 feed.delete_all_items()
124 self.save_feeds()
125 self.emit('deleted', feed)
127 def save_feeds(self):
128 if not self._loading:
129 config = Config.get_instance()
130 config.feeds = [f.dump() for f in self.feedlist]
131 return
133 def feed_detail_changed(self, feed):
134 self.save_feeds()
135 self.emit('changed') # XXXX send the feed as well?
137 def _sort_dsu(self, seq):
138 aux_list = [(x.title, x) for x in seq]
139 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
140 return [x[1] for x in aux_list]
142 def sort(self, indices = None):
143 if not indices or len(indices) == 1:
144 self[:] = self._sort_dsu(self)
145 else:
146 items = self._sort_dsu(indices)
147 for i,x in enumerate(items):
148 list.__setitem__(self, indices[i], items[i])
149 self.save_feeds()
150 self.emit('changed')
152 def __hash__(self):
153 h = 0
154 for item in self:
155 h ^= hash(item)
156 return h
158 def get_feed_with_id(self, id):
159 for f in self.flatten_list():
160 if f.id == id:
161 return f
162 return None
164 def flatten_list(self, ob=None):
165 if ob is None:
166 ob = self.feedlist
167 l = []
168 for o in ob:
169 if isinstance(o, list):
170 l = l + self.flatten_list(o)
171 else:
172 l.append(o)
173 return l
175 feedlist_instance = None
177 def get_instance():
178 global feedlist_instance
179 if feedlist_instance is None:
180 feedlist_instance = FeedList()
181 return feedlist_instance
183 class Feed(gobject.GObject):
184 "A Feed object stores information set by user about a RSS feed."
186 DEFAULT = -1
187 STATUS_IDLE = 0
188 STATUS_POLLING = 1
190 __slots__ = ('_title', '_location', '_username', '_password', '_parsed',
191 '__save_fields', '_items', '_slots',
192 '_id', '_channel_description',
193 '_channel_title', '_channel_link', '_channel_copyright',
194 'channel_lbd', 'channel_editor', 'channel_webmaster',
195 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
196 '_items_stored', '_poll_freq', '_last_poll', '_n_items_unread')
198 __save_fields = (('_title', ""), ('_location', ""), ('_username', ""),
199 ('_password', ""), ('_id', ""),
200 ('_channel_description', ""), ('_channel_title', ""),
201 ('_channel_link', ""), ('_channel_copyright', ""),
202 ('channel_creator', ""), ('_error', None),
203 ('_items_stored', DEFAULT),
204 ('_poll_freq', DEFAULT),
205 ('_last_poll', 0),
206 ('_n_items_unread', 0))
208 __gsignals__ = {
209 'info-updated' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
210 'error-occurred' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
211 'process-status-changed' : (gobject.SIGNAL_RUN_LAST,
212 gobject.TYPE_NONE, ()),
213 'poll-done' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
214 'items-updated' :(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
215 (gobject.TYPE_PYOBJECT,)),
216 'items-read' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
217 (gobject.TYPE_PYOBJECT,)),
218 'items-deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
219 (gobject.TYPE_PYOBJECT,)),
220 'item-order-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,())
224 # use one of the factory functions below instead of this directly
225 def __init__(self, title="", location="", username="", password=""):
226 gobject.GObject.__init__(self)
227 self._title = title
228 self._channel_description = ""
229 self._channel_title = ""
230 self._channel_link = ""
231 self._channel_copyright = ""
232 self.channel_lbd = None
233 self.channel_editor = ""
234 self.channel_webmaster = ""
235 self.channel_creator = ""
236 self._location = location
237 self._username = username
238 self._password = password
239 self._parsed = None
240 self._error = None
241 self._process_status = self.STATUS_IDLE
242 self.router = FeedDataRouter.FeedDataRouter(self)
243 self._parent = None
244 self._items_stored = Feed.DEFAULT
245 self._poll_freq = Feed.DEFAULT
246 self._last_poll = 0
247 self._n_items_unread = 0
248 self._items = None
249 self._items_loaded = False
250 return
252 def __str__(self):
253 return "Feed '%s' from %s" % (self._title, self._location)
255 @property
256 def id(self):
257 return self._id
259 @apply
260 def parsed():
261 doc = "A ParsedSummary object generated from the summary file"
262 def fget(self):
263 return self._parsed
264 def fset(self, parsed):
265 self._parsed = parsed
266 return property(**locals())
268 @apply
269 def title():
270 doc = "The title of this Feed (as defined by user)"
271 def fget(self):
272 text = ''
273 if self._title:
274 text = self._title
275 return text
276 def fset(self, title):
277 if self._title != title:
278 self._title = title
279 self.emit('info-updated')
280 return property(**locals())
282 @apply
283 def access_info():
284 doc = "A tuple of location, username, password"
285 def fget(self):
286 return (self._location, self._username, self._password)
287 def fset(self, (location,username,password)):
288 self._location = location
289 self._username = username
290 self._password = password
291 self.emit('info-updated')
292 return property(**locals())
294 @apply
295 def location():
296 doc = ""
297 def fget(self):
298 return self._location
299 def fset(self, location):
300 if self._location != location:
301 self._location = location
302 self.emit('info-updated')
303 return property(**locals())
305 @apply
306 def channel_title():
307 doc = ""
308 def fget(self):
309 text = ''
310 if self._channel_title:
311 text = self._channel_title
312 return text
313 def fset(self, t):
314 changed = self._channel_title != t
315 self._channel_title = t
316 if changed:
317 self.emit('info-updated')
318 return property(**locals())
320 @apply
321 def channel_description():
322 doc = ""
323 def fget(self):
324 text = ''
325 if self._channel_description:
326 text = self._channel_description
327 return text
328 def fset(self, t):
329 changed = self._channel_description != t
330 self._channel_description = t
331 if changed:
332 self.emit('info-updated')
333 return property(**locals())
335 @apply
336 def channel_link():
337 doc = ""
338 def fget(self):
339 return self._channel_link
340 def fset(self, t):
341 changed = self._channel_link != t
342 self._channel_link = t
343 if changed:
344 self.emit('info-updated')
345 return property(**locals())
347 @apply
348 def channel_copyright():
349 doc = ""
350 def fget(self):
351 return self._channel_copyright
352 def fset(self, t):
353 changed = self._channel_copyright != t
354 self._channel_copyright = t
355 if changed:
356 self.emit('info-updated')
357 return property(**locals())
359 @apply
360 def number_of_items_stored():
361 doc = ""
362 def fget(self):
363 return self._items_stored
364 def fset(self, num=None):
365 if self._items_stored != num:
366 self._items_stored = num
367 return property(**locals())
369 @apply
370 def poll_frequency():
371 doc = ""
372 def fget(self):
373 return self._poll_freq
374 def fset(self, freq):
375 if self._poll_freq != freq:
376 self._poll_freq = freq
377 return property(**locals())
379 @apply
380 def last_poll():
381 doc = ""
382 def fget(self):
383 return self._last_poll
384 def fset(self, time):
385 if self._last_poll != time:
386 self._last_poll = time
387 return property(**locals())
389 @apply
390 def n_items_unread():
391 doc = ""
392 def fget(self):
393 return self._n_items_unread
394 def fset(self, n):
395 if self._n_items_unread != n:
396 self._n_items_unread = n
397 return property(**locals())
399 @apply
400 def error():
401 doc = ""
402 def fget(self):
403 return self._error
404 def fset(self, error):
405 if self._error != error:
406 self._error = error
407 self.emit('error-occurred')
408 return property(**locals())
410 @apply
411 def process_status():
412 doc = ""
413 def fget(self):
414 return self._process_status
415 def fset(self, status):
416 if status != self._process_status:
417 self._process_status = status
418 self.emit('process_status_changed')
419 return property(**locals())
421 @apply
422 def parent():
423 doc = ""
424 def fget(self):
425 return self._parent
426 def fset(self, parent):
427 self._parent = parent
428 return property(**locals())
430 @property
431 def next_refresh(self):
432 """ return the feed's next refresh (time)"""
433 nr = None
434 if self._poll_freq == self.DEFAULT:
435 increment = Config.get_instance().poll_frequency
436 else:
437 increment = self._poll_freq
438 if increment > 0:
439 nr = self.last_poll + increment
440 return nr
442 def dump(self):
443 fl = {}
444 for f, default in self.__save_fields:
445 fl[f] = self.__getattribute__(f)
446 return fl
448 def undump(self, dict):
449 for f, default in self.__save_fields:
450 self.__setattr__(f, dict.get(f, default))
451 return
453 def poll_done(self):
454 self.emit('poll-done')
456 # FeedItems stuff
457 def _sort(self, items):
459 Sorts the given items according to the sort order
461 items = sorted(items, cmp=self._cmp)
462 # XXX
463 #if self._items.sort_order:
464 # items.reverse()
465 return items
467 def _cmp(self, a, b):
469 Comparator method to compare items based on the item's pub_date attribute
471 If an item doesn't have a pub_date, it uses title and prioritizes the
472 unread items
474 try:
475 return cmp(a.pub_date, b.pub_date)
476 except AttributeError:
477 return locale.strcoll(a.title, b.title) and not a.seen or not b.seen
479 def add_items(self, items):
480 config = Config.get_instance()
481 cutpoint = self.number_of_items_stored
482 if cutpoint == Feed.DEFAULT:
483 cutpoint = config.number_of_items_stored
484 items = set(sorted(items, cmp=self._cmp))
485 maxid = reduce(max, [item.id for item in self._items])
486 for item in items:
487 maxid += 1
488 item.id = maxid
489 item.feed = self
490 items.updated(item)
491 self._items = self._items.union(items)
492 self.restore_items(self._items)
493 self.emit('items-updated', items)
495 def restore_items(self, items):
496 olditems = []
497 num_unread = 0
498 config = Config.get_instance()
499 cutpoint = self.number_of_items_stored
500 if cutpoint == Feed.DEFAULT:
501 cutpoint = config.number_of_items_stored
502 items = self._sort(items)
503 for idx, item in enumerate(items):
504 item.feed = self
505 if item.sticky or not item.seen or idx <= cutpoint:
506 item.connect('read', self.item_marked_read)
507 # forward sticky signal? XXX
508 self._items.add(item)
509 num_unread += item.seen and 1 or 0
510 continue
511 else:
512 item.clean_up()
513 olditems.append(item)
514 self.n_items_unread = num_unread
515 #self.number_of_items_stored = len(self.items)
516 if olditems:
517 self.emit('items-deleted', olditems)
518 return
520 def item_marked_read(self, item):
521 self.n_items_unread += item.seen and -1 or 1
523 def delete_all_items(self):
524 self.emit('items-deleted', self._items)
526 @property
527 def items(self):
528 return self._items
530 @property
531 def number_of_unread(self):
532 return self._n_items_unread
534 @property
535 def number_of_items(self):
536 return len(self._items)
538 def mark_all_items_as_read(self):
539 unread = [item for item in self._items if not item.seen]
540 self.emit('items-read', unread)
541 self.n_items_unread = 0
543 def load_contents(self):
544 if self._items_loaded:
545 return False
546 itemstore = ItemStore.get_instance()
547 items = itemstore.read_feed_items(self)
548 if items:
549 self.restore_items(items)
550 self._items_loaded = True
551 return self._items_loaded
553 def unload_contents(self):
555 Unloads the items by disconnecting the signals and reinitialising the
556 instance variables
558 TODO: unload seems to lose some circular references. garbage collector
559 will find them, though, so maybe it's not a problem.
561 if not self._items_loaded:
562 return
563 self._items.clear()
564 self._items_loaded = False
566 @classmethod
567 def create_new_feed(klass, title, location="", username="", password=""):
568 f = klass()
569 f._title = title
570 f._location = location
571 f._id = Config.get_instance().next_feed_id_seq()
572 f._username = username
573 f._password = password
574 return f
576 @classmethod
577 def create_empty_feed(klass):
578 f = klass()
579 return f