Cleaned up unused tests and test path munging and removed FeedAdapter
[straw.git] / src / lib / feeds.py
blobda915710bf1a5fcaa5f53f702b0bd6a7c6cfd887
1 import locale, operator
2 import gobject
3 import ItemStore
4 import Config
5 from StringIO import StringIO
6 import OPMLImport
7 import error
8 import locale
9 import utils
10 import types
12 from utils import calltrace
15 class FeedList(gobject.GObject):
17 __gsignals__ = {
18 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
21 def __init__(self, init_seq = []):
22 gobject.GObject.__init__(self)
23 self.feedlist = []
24 self._loading = False
26 def __iter__(self):
27 return iter(self.feedlist)
29 def load_data(self):
30 def _load(feedlist, parent):
31 for df in feedlist:
32 if isinstance(df, list):
33 _load(df[1:], parent)
34 else:
35 f = Feed.create_empty_feed()
36 f.undump(df)
37 self.append(parent, f)
38 self._loading = True
39 feedlist = Config.get_instance().feeds
40 if feedlist:
41 _load(feedlist, None)
42 self._loading = False
43 self.emit('changed')
45 # these signals are forwarded so that listeners who just want to
46 # listen for a specific event regardless of what feed it came from can
47 # just connect to this feedlist instead of connecting to the
48 # individual feeds.
49 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
50 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
51 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
52 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
53 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
54 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
56 def __setitem__(self, key, value):
57 self.feedlist.__setitem__(key, value)
58 value.connect('changed', self.feed_detail_changed)
59 self.save_feeds_and_notify(True)
61 def extend(self, parent, values, from_sub=False):
62 list.extend(self.feedlist, values)
63 for f in values:
64 f.parent = parent
65 f.connect('changed', self.feed_detail_changed)
66 self.save_feeds()
68 def append(self, parent, value):
69 self.feedlist.append(value)
70 value.parent = parent
71 value.connect('changed', self.feed_detail_changed)
72 self.save_feeds()
74 def insert(self, index, parent, value):
75 self.feedlist.insert(index, value)
76 value.parent = parent
77 value.connect('changed', self.feed_detail_changed)
78 self.save_feeds()
80 def index(self, feed):
81 return self.feedlist.index(feed)
83 def reorder(self, move, delta):
84 k = self.feedlist[:]
85 move = list(move)
86 move.sort()
87 if delta > 0:
88 move.reverse()
89 if move[0] == 0 and delta < 0 or move[-1] == (len(self.feedlist) - 1) and delta > 0:
90 return
91 for m in move:
92 k[m + delta], k[m] = k[m], k[m + delta]
93 for i in range(len(k)):
94 list.__setitem__(self.feedlist, i, k[i])
95 self.save_feeds()
97 def __delitem__(self, value):
98 feed = self.feedlist[value]
99 list.__delitem__(self.feedlist, value)
100 feed.delete_all_items()
101 self.save_feeds()
103 def save_feeds(self):
104 if not self._loading:
105 config = Config.get_instance()
106 config.feeds = [f.dump() for f in self.feedlist]
107 return
109 def feed_detail_changed(self, feed):
110 self.save_feeds()
111 # self.emit('changed') # XXXX send the feed as well?
113 def _sort_dsu(self, seq):
114 aux_list = [(x.title, x) for x in seq]
115 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
116 return [x[1] for x in aux_list]
118 def sort(self, indices = None):
119 if not indices or len(indices) == 1:
120 self[:] = self._sort_dsu(self)
121 else:
122 items = self._sort_dsu(indices)
123 for i,x in enumerate(items):
124 list.__setitem__(self, indices[i], items[i])
125 self.save_feeds()
126 # self.emit('changed')
128 def __hash__(self):
129 h = 0
130 for item in self.feedlist:
131 h ^= hash(item)
132 return h
134 def get_feed_with_id(self, id):
135 for f in self.flatten_list():
136 if f.id == id:
137 return f
138 return None
140 def flatten_list(self, ob=None):
141 if ob is None:
142 ob = self.feedlist
143 l = []
144 for o in ob:
145 if isinstance(o, list):
146 l = l + self.flatten_list(o)
147 else:
148 l.append(o)
149 return l
151 feedlist_instance = None
153 def get_feedlist_instance():
154 global feedlist_instance
155 if feedlist_instance is None:
156 feedlist_instance = FeedList()
157 return feedlist_instance
159 feedlist = get_feedlist_instance()
161 class IdleState(object):
162 ''' state for idle or normal operation '''
163 def __init__(self):
164 filename = os.path.join(utils.find_image_dir(), 'feed.png')
165 self.icon = gtk.gdk.pixbuf_new_from_file(filename)
167 @property
168 def icon(self):
169 return self.icon
171 @property
172 def mesg(self):
173 return None
175 class PollingState(object):
176 ''' state when feed is polling '''
177 def __init__(self):
178 self._icon = gtk.Image()
179 self._icon.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU)
181 @property
182 def icon(self):
183 return self._icon.get_pixbuf()
185 @property
186 def mesg(self):
187 return None
189 class ErrorState(object):
190 ''' state when feed has errors '''
192 def __init__(self, mesg):
193 self._icon = gtk.Image()
194 self._icon.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
195 self._mesg = mesg
197 @property
198 def icon(self):
199 return self._icon.get_pixbuf()
201 @property
202 def mesg(self):
203 return self._mesg
205 class Feed(gobject.GObject):
206 "A Feed object stores information set by user about a RSS feed."
208 DEFAULT = -1
209 STATUS_IDLE = 0
210 STATUS_POLLING = 1
212 __slots__ = ('_title', '_location', '_username', '_password', '_parsed',
213 '__save_fields', '_items', '_slots',
214 '_id', '_channel_description',
215 '_channel_title', '_channel_link', '_channel_copyright',
216 'channel_lbd', 'channel_editor', 'channel_webmaster',
217 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
218 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
220 __save_fields = (('_title', ""), ('_location', ""), ('_username', ""),
221 ('_password', ""), ('_id', ""),
222 ('_channel_description', ""), ('_channel_title', ""),
223 ('_channel_link', ""), ('_channel_copyright', ""),
224 ('channel_creator', ""), ('_error', None),
225 ('_items_stored', DEFAULT),
226 ('_poll_freq', DEFAULT),
227 ('_last_poll', 0),
228 ('_n_items_unread',0))
231 __gsignals__ = {
232 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
233 'poll-done' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
234 'items-added' :(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
235 (gobject.TYPE_PYOBJECT,)),
236 'items-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
237 (gobject.TYPE_PYOBJECT,)),
238 'items-deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
239 (gobject.TYPE_PYOBJECT,))
243 # use one of the factory functions below instead of this directly
244 def __init__(self, title="", location="", username="", password=""):
245 import FeedDataRouter
246 gobject.GObject.__init__(self)
247 self._title = title
248 self._channel_description = ""
249 self._channel_title = ""
250 self._channel_link = ""
251 self._channel_copyright = ""
252 self.channel_lbd = None
253 self.channel_editor = ""
254 self.channel_webmaster = ""
255 self.channel_creator = ""
256 self._location = location
257 self._username = username
258 self._password = password
259 self._parsed = None
260 self._error = None
261 self._n_items_unread = 0
262 self._process_status = self.STATUS_IDLE
263 self.router = FeedDataRouter.FeedDataRouter(self)
264 self._parent = None
265 self._items_stored = Feed.DEFAULT
266 self._poll_freq = Feed.DEFAULT
267 self._last_poll = 0
269 self.config = Config.get_instance()
270 # XXX move this to subscriptions
271 self.config.connect('item-order-changed', self.item_order_changed_cb)
272 self.config.connect('item-stored-changed', self.item_stored_changed_cb)
273 self.item_order_reverse = self.config.item_order
274 self.item_stored_num = self.config.number_of_items_stored
275 self._items = FifoCache(num_entries=Feed.DEFAULT)
276 self._items_loaded = False
277 return
279 def __str__(self):
280 return "Feed '%s' from %s" % (self._title, self._location)
282 @property
283 def id(self):
284 return self._id
286 @apply
287 def parsed():
288 doc = "A ParsedSummary object generated from the summary file"
289 def fget(self):
290 return self._parsed
291 def fset(self, parsed):
292 self._parsed = parsed
293 return property(**locals())
295 @apply
296 def title():
297 doc = "The title of this Feed (as defined by user)"
298 def fget(self):
299 text = ''
300 if self._title:
301 text = self._title
302 return text
303 def fset(self, title):
304 if self._title != title:
305 self._title = title
306 self.emit('changed')
307 return property(**locals())
309 @apply
310 def access_info():
311 doc = "A tuple of location, username, password"
312 def fget(self):
313 return (self._location, self._username, self._password)
314 def fset(self, (location,username,password)):
315 self._location = location
316 self._username = username
317 self._password = password
318 self.emit('changed')
319 return property(**locals())
321 @apply
322 def location():
323 doc = ""
324 def fget(self):
325 return self._location
326 def fset(self, location):
327 if self._location != location:
328 self._location = location
329 self.emit('changed')
330 return property(**locals())
332 @apply
333 def channel_title():
334 doc = ""
335 def fget(self):
336 text = ''
337 if self._channel_title:
338 text = self._channel_title
339 return text
340 def fset(self, t):
341 changed = self._channel_title != t
342 self._channel_title = t
343 if changed:
344 self.emit('changed')
345 return property(**locals())
347 @apply
348 def channel_description():
349 doc = ""
350 def fget(self):
351 text = ''
352 if self._channel_description:
353 text = self._channel_description
354 return text
355 def fset(self, t):
356 changed = self._channel_description != t
357 self._channel_description = t
358 if changed:
359 self.emit('changed')
360 return property(**locals())
362 @apply
363 def channel_link():
364 doc = ""
365 def fget(self):
366 return self._channel_link
367 def fset(self, t):
368 changed = self._channel_link != t
369 self._channel_link = t
370 if changed:
371 self.emit('changed')
372 return property(**locals())
374 @apply
375 def channel_copyright():
376 doc = ""
377 def fget(self):
378 return self._channel_copyright
379 def fset(self, t):
380 changed = self._channel_copyright != t
381 self._channel_copyright = t
382 if changed:
383 self.emit('changed')
384 return property(**locals())
386 @apply
387 def number_of_items_stored():
388 doc = ""
389 def fget(self):
390 return self._items_stored
391 def fset(self, num=None):
392 if self._items_stored != num:
393 self._items_stored = num
394 return property(**locals())
396 @apply
397 def poll_frequency():
398 doc = ""
399 def fget(self):
400 return self._poll_freq
401 def fset(self, freq):
402 if self._poll_freq != freq:
403 self._poll_freq = freq
404 return property(**locals())
406 @apply
407 def last_poll():
408 doc = ""
409 def fget(self):
410 return self._last_poll
411 def fset(self, time):
412 if self._last_poll != time:
413 self._last_poll = time
414 return property(**locals())
416 @apply
417 def number_of_unread():
418 doc = "the number of unread items for this feed"
419 def fget(self):
420 return self._n_items_unread
421 def fset(self, n):
422 self._n_items_unread = n
423 self.emit('changed')
424 return property(**locals())
426 @apply
427 def error():
428 doc = ""
429 def fget(self):
430 return self._error
431 def fset(self, error):
432 if self._error != error:
433 self._error = error
434 self.emit('changed')
435 return property(**locals())
437 @apply
438 def process_status():
439 doc = ""
440 def fget(self):
441 return self._process_status
442 def fset(self, status):
443 if status != self._process_status:
444 self._process_status = status
445 self.emit('changed')
446 return property(**locals())
448 @apply
449 def parent():
450 doc = ""
451 def fget(self):
452 return self._parent
453 def fset(self, parent):
454 self._parent = parent
455 return property(**locals())
457 @property
458 def next_refresh(self):
459 """ return the feed's next refresh (time)"""
460 nr = None
461 if self._poll_freq == self.DEFAULT:
462 increment = self.config.poll_frequency
463 else:
464 increment = self._poll_freq
465 if increment > 0:
466 nr = self.last_poll + increment
467 return nr
469 def dump(self):
470 fl = {}
471 for f, default in self.__save_fields:
472 fl[f] = self.__getattribute__(f)
473 return fl
475 def undump(self, dict):
476 for f, default in self.__save_fields:
477 self.__setattr__(f, dict.get(f, default))
478 return
480 def poll_done(self):
481 self.emit('poll-done')
483 def item_order_changed_cb(self, config):
484 self.item_order_reverse = config.item_order
486 def item_stored_changed_cb(self, config):
487 self.item_stored_num = config.number_of_items_stored
489 def get_cutpoint(self):
490 cutpoint = self.number_of_items_stored
491 if cutpoint == Feed.DEFAULT:
492 cutpoint = self.item_stored_num
493 return cutpoint
496 def add_items(self, items):
497 if not self._items_loaded: self.load_contents()
498 items = sorted(items, key=operator.attrgetter('pub_date'),
499 reverse=self.item_order_reverse)
500 self._items.set_number_of_entries(self.get_cutpoint())
501 maxid = 0
502 if self._items:
503 maxid = reduce(max, [item.id for item in self._items.itervalues()])
504 newitems = filter(lambda x: x not in self._items.itervalues(), items)
505 for item in newitems:
506 maxid += 1
507 item.id = maxid
508 item.feed = self
509 item.connect('changed', self.item_changed_cb)
510 self._items[item.id] = item ## XXX pruned items don't get clean up properly
511 self.number_of_unread = len(filter(lambda x: not x.seen, self._items.itervalues()))
512 self.emit('items-added', newitems)
514 def restore_items(self, items):
515 items = sorted(items, key=operator.attrgetter('pub_date'),
516 reverse=self.item_order_reverse)
517 cutpoint = self.get_cutpoint()
518 self._items.set_number_of_entries(cutpoint)
519 olditems = []
520 for idx, item in enumerate(items):
521 if not item.sticky and idx >= cutpoint:
522 item.clean_up()
523 olditems.append(item)
524 continue
525 item.feed = self
526 item.connect('changed', self.item_changed_cb)
527 self._items[item.id] = item
528 self.number_of_unread = len(filter(lambda x: not x.seen, self._items.itervalues()))
529 if olditems: self.emit('items-deleted', olditems)
530 return
532 def item_changed_cb(self, item):
533 self.number_of_unread = len(filter(lambda x: not x.seen, self._items.itervalues()))
534 self.emit('items-changed', [item])
536 def delete_all_items(self):
537 self._items.clear()
538 self.emit('items-deleted', self._items.values())
540 @property
541 def items(self):
542 if not self._items_loaded:
543 self.load_contents()
544 return self._items.values()
546 def mark_items_as_read(self, items=None):
547 def mark(item): item.seen = True
548 unread = [item for item in self._items.itervalues() if not item.seen]
549 map(mark, unread)
550 self.emit('items-changed', unread)
552 def load_contents(self):
553 if self._items_loaded:
554 return False
555 itemstore = ItemStore.get_instance()
556 items = itemstore.read_feed_items(self)
557 print "feed.load_contents->items: ", len(items)
558 if items:
559 self.restore_items(items)
560 self._items_loaded = True
561 return self._items_loaded
563 def unload_contents(self):
564 if not self._items_loaded:
565 return
566 self._items.clear()
567 self._items_loaded = False
569 @classmethod
570 def create_new_feed(klass, title, location="", username="", password=""):
571 f = klass()
572 f._title = title
573 f._location = location
574 f._id = Config.get_instance().next_feed_id_seq()
575 f._username = username
576 f._password = password
577 return f
579 @classmethod
580 def create_empty_feed(klass):
581 f = klass()
582 return f
585 import UserDict
586 from collections import deque
588 class FifoCache(object, UserDict.DictMixin):
589 ''' A mapping that remembers the last 'num_entries' items that were set '''
591 def __init__(self, num_entries, dct=()):
592 self.num_entries = num_entries
593 self.dct = dict(dct)
594 self.lst = deque()
596 def __repr__(self):
597 return '%r(%r,%r)' % (
598 self.__class__.__name__, self.num_entries, self.dct)
600 def copy(self):
601 return self.__class__(self.num_entries, self.dct)
603 def keys(self):
604 return list(self.lst)
606 def __getitem__(self, key):
607 return self.dct[key]
609 def __setitem__(self, key, value):
610 dct = self.dct
611 lst = self.lst
612 if key in dct:
613 self.remove_from_deque(lst, key)
614 dct[key] = value
615 lst.append(key)
616 if len(lst) > self.num_entries:
617 del dct[lst.popleft()]
619 def __delitem__(self, key):
620 self.dct.pop(key)
621 self.remove_from_deque(self.lst, key)
623 # a method explicitly defined only as an optimization
624 def __contains__(self, item):
625 return item in self.dct
627 has_key = __contains__
629 def remove_from_deque(self, d, x):
630 for i, v in enumerate(d):
631 if v == x:
632 del d[i]
633 return
634 raise ValueError, '%r not in %r' % (x,d)
636 def set_number_of_entries(self, num):
637 self.num_entries = num
638 while len(self.lst) > num:
639 del self.dct[self.lst.popleft()]
641 PSEUDO_ALL_KEY = 'ALL'
642 PSEUDO_UNCATEGORIZED_KEY = 'UNCATEGORIZED'
644 class FeedCategoryList(gobject.GObject):
646 __gsignals__ = {
647 'added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
648 'deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
649 'category-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
650 'pseudo-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
653 def __init__(self):
654 gobject.GObject.__init__(self)
655 # have to define this here so the titles can be translated
656 PSEUDO_TITLES = {PSEUDO_ALL_KEY: _('All'),
657 PSEUDO_UNCATEGORIZED_KEY: _('Feeds')}
658 self._all_category = PseudoCategory(PSEUDO_TITLES[PSEUDO_ALL_KEY],
659 PSEUDO_ALL_KEY)
660 self._un_category = PseudoCategory(
661 PSEUDO_TITLES[PSEUDO_UNCATEGORIZED_KEY], PSEUDO_UNCATEGORIZED_KEY)
662 self._user_categories = []
663 self._pseudo_categories = (self._all_category, self._un_category)
664 self._loading = False
665 self._feedlist = get_feedlist_instance()
667 def load_data(self):
668 """Loads categories from config.
669 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
670 {'id', feed_id1, 'from_subscription': bool} ...], ...]
672 cats = Config.get_instance().categories or []
673 categorized = {}
674 for c in cats:
675 head = c[0]
676 tail = c[1:]
677 pseudo_key = head.get('pseudo', None)
678 if pseudo_key == PSEUDO_ALL_KEY:
679 fc = self.all_category
680 elif pseudo_key == PSEUDO_UNCATEGORIZED_KEY:
681 fc = self.un_category
682 else:
683 fc = FeedCategory(head['title'])
684 sub = head.get('subscription', None)
685 if sub is not None:
686 fc.subscription = undump_subscription(sub)
687 for f in tail:
688 # backwards compatibility for stuff saved with versions <= 0.23
689 if type(f) is int:
690 fid = f
691 from_sub = False
692 else:
693 fid = f['id']
694 from_sub = f['from_subscription']
695 feed = self._feedlist.get_feed_with_id(fid)
696 if feed and not pseudo_key: # we deal with pseudos later
697 if feed in fc.feeds:
698 error.log("%s (%d) was already in %s, skipping" % (str(feed), fid, str(fc)))
699 continue
700 fc.append_feed(feed, from_sub)
701 categorized[feed] = True
702 # User categories: connect pseudos later
703 if not pseudo_key:
704 fc.connect('changed', self.category_changed)
705 self._user_categories.append(fc)
706 # just in case we've missed any feeds, go through the list
707 # and add to the pseudocategories. cache the feed list of all_category
708 # so we don't get a function call (and a list comprehension loop
709 # inside it) on each feed. it should be ok here, there are no
710 # duplicates in feedlist. right?
711 pseudos_changed = False
712 all_feeds = self.all_category.feeds
713 for f in self._feedlist:
714 if f not in all_feeds:
715 self.all_category.append_feed(f, False)
716 pseudos_changed = True
717 uf = categorized.get(f, None)
718 if not uf:
719 self.un_category.append_feed(f, False)
720 pseudos_changed = True
721 if pseudos_changed:
722 self.save_data()
723 for cat in self.pseudo_categories:
724 cat.connect('changed', self.pseudo_category_changed)
726 def save_data(self):
727 Config.get_instance().categories = [
728 cat.dump() for cat in self]
730 def pseudo_category_changed(self, category, *args):
731 self.save_data()
732 self.emit('pseudo-changed')
734 def category_changed(self, signal):
735 if signal.feed is not None:
736 uncategorized = True
737 for cat in self.user_categories:
738 if signal.feed in cat.feeds:
739 uncategorized = False
740 break
741 if uncategorized:
742 self.un_category.append_feed(signal.feed, False)
743 else:
744 try:
745 self.un_category.remove_feed(signal.feed)
746 except ValueError:
747 pass
748 self.save_data()
749 self.emit('category-changed')
751 def remove_feed(self, feed):
752 for c in self:
753 try:
754 c.remove_feed(feed)
755 del self._feedlist[feed]
756 except ValueError:
757 pass
759 @calltrace
760 def append_feed(self, feed, category=None, index=None):
761 self._feedlist.append(category, feed)
762 if category and category not in self.pseudo_categories:
763 if index:
764 category.insert_feed(index, feed, False)
765 else:
766 category.append_feed(feed, False)
767 else:
768 print "uncategory append"
769 self.un_category.append_feed(feed, False)
770 print "all category append"
771 self.all_category.append_feed(feed, False)
773 def import_feeds(self, feeds, category=None, from_sub=False):
774 self._feedlist.extend(category, feeds)
775 if category and category not in self.pseudo_categories:
776 category.extend_feed(feeds, from_sub)
777 else:
778 print 'feed imported ', feeds
779 self.un_category.extend_feed(feeds, from_sub)
780 self.all_category.extend_feed(feeds, from_sub)
782 @property
783 def user_categories(self):
784 return self._user_categories
786 @property
787 def pseudo_categories(self):
788 return self._pseudo_categories
790 @property
791 def all_categories(self):
792 return self.pseudo_categories + tuple(self.user_categories)
794 @property
795 def all_category(self):
796 return self._all_category
798 @property
799 def un_category(self):
800 return self._un_category
802 class CategoryIterator:
803 def __init__(self, fclist):
804 self._fclist = fclist
805 self._index = -1
807 def __iter__(self):
808 return self
810 def _next(self):
811 self._index += 1
812 i = self._index
813 uclen = len(self._fclist.user_categories)
814 if i < uclen:
815 return self._fclist.user_categories[i]
816 elif i < uclen + len(self._fclist.pseudo_categories):
817 return self._fclist.pseudo_categories[i - uclen]
818 else:
819 raise StopIteration
821 def next(self):
822 v = self._next()
823 return v
825 def __iter__(self):
826 return self.CategoryIterator(self)
828 def add_category(self, category):
829 category.connect('changed', self.category_changed)
830 self._user_categories.append(category)
831 auxlist = [(x.title.lower(),x) for x in self._user_categories]
832 auxlist.sort()
833 self._user_categories = [x[1] for x in auxlist]
834 self.save_data()
835 self.emit('added', category)
837 def remove_category(self, category):
838 for feed in category.feeds:
839 category.remove_feed(feed)
840 self._user_categories.remove(category)
841 self.save_data()
842 self.emit('deleted', category)
844 # It might be good to have a superclass FeedCategorySubscription or something
845 # so we could support different formats. However, I don't know of any other
846 # relevant format used for this purpose, so that can be done later if needed.
847 # Of course, they could also just implement the same interface.
848 class OPMLCategorySubscription(gobject.GObject):
849 REFRESH_DEFAULT = -1
851 __gsignals__ = {
852 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
853 'updated' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
856 def __init__(self, location=None):
857 gobject.GObject.__init__(self)
858 self._location = location
859 self._username = None
860 self._password = None
861 self._contents = None
862 self._frequency = OPMLCategorySubscription.REFRESH_DEFAULT
863 self._last_poll = 0
864 self._error = None
866 @apply
867 def location():
868 doc = ""
869 def fget(self):
870 return self._location
871 def fset(self, location):
872 self._location = location
873 self.emit('changed')
874 return property(**locals())
876 @apply
877 def username():
878 doc = ""
879 def fget(self):
880 return self._username
881 def fset(self, username):
882 self._username = username
883 self.emit('changed')
884 return property(**locals())
886 @apply
887 def password():
888 doc = ""
889 def fget(self):
890 return self._password
891 def fset(self):
892 self._password = password
893 self.emit('changed')
894 return property(**locals())
896 @apply
897 def frequency():
898 doc = ""
899 def fget(self):
900 return self._frequency
901 def fset(self, freq):
902 self._frequency = freq
903 self.emit('changed')
904 return property(**locals())
906 @apply
907 def last_poll():
908 doc = ""
909 def fget(self):
910 return self._last_poll
911 def fset(self, last_poll):
912 self._last_poll = last_poll
913 self.emit('changed')
914 return property(**locals())
916 @apply
917 def error():
918 doc = ""
919 def fget(self):
920 return self._error
921 def fset(self, error):
922 self._error = error
923 self.emit('changed')
924 return property(**locals())
926 def parse(self, data):
927 datastream = StringIO(data)
928 entries = OPMLImport.read(datastream)
929 contents = [(e.url, e.text) for e in entries]
930 updated = contents == self._contents
931 self._contents = contents
932 if updated:
933 self.emit('updated')
934 return
936 @property
937 def contents(self):
938 return self._contents
940 @classmethod
941 def undump(klass, dictionary):
942 sub = klass()
943 sub.location = dictionary.get('location')
944 sub.username = dictionary.get('username')
945 sub.password = dictionary.get('password')
946 sub.frequency = dictionary.get(
947 'frequency', OPMLCategorySubscription.REFRESH_DEFAULT)
948 sub.last_poll = dictionary.get('last_poll', 0)
949 sub.error = dictionary.get('error')
950 return sub
952 def dump(self):
953 return {'type': 'opml',
954 'location': self.location,
955 'username': self.username,
956 'password': self.password,
957 'frequency': self.frequency,
958 'last_poll': self.last_poll,
959 'error': self.error}
961 def undump_subscription(dictionary):
962 try:
963 if dictionary.get('type') == 'opml':
964 return OPMLCategorySubscription.undump(dictionary)
965 except Exception, e:
966 error.log("exception while undumping subscription: " + str(e))
967 raise
969 class CategoryMember(object):
970 def __init__(self, feed=None, from_sub=False):
971 self._feed = feed
972 self._from_subscription = from_sub
974 @apply
975 def feed():
976 doc = ""
977 def fget(self):
978 return self._feed
979 def fset(self, feed):
980 self._feed = feed
981 return property(**locals())
983 @apply
984 def from_subscription():
985 doc = ""
986 def fget(self):
987 return self._from_subscription
988 def fset(self, p):
989 self._from_subscription = p
990 return property(**locals())
992 class FeedCategory(gobject.GObject):
994 __gsignals__ = {
995 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()),
996 'feed-added':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
997 (gobject.TYPE_PYOBJECT,)),
998 'feed-removed':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
999 (gobject.TYPE_PYOBJECT,))
1002 def __init__(self, title=""):
1003 gobject.GObject.__init__(self)
1004 self.feedlist = []
1005 self._title = title
1006 self._subscription = None
1008 @apply
1009 def title():
1010 doc = ""
1011 def fget(self):
1012 return self._title
1013 def fset(self, title):
1014 self._title = title
1015 self.emit('changed')
1016 return property(**locals())
1018 @apply
1019 def subscription():
1020 doc = ""
1021 def fget(self):
1022 return self._subscription
1023 def fset(self, sub):
1024 self._subscription = sub
1025 self._subscription.connect('changed', self._subscription_changed)
1026 self._subscription.connect('updated', self._subscription_contents_updated)
1027 self.emit('changed')
1028 return property(**locals())
1030 def read_contents_from_subscription(self):
1031 if self.subscription is None:
1032 return
1033 subfeeds = self.subscription.contents
1034 sfdict = dict(subfeeds)
1035 feedlist = get_feedlist_instance()
1036 current = dict([(feed.location, feed) for feed in self.feeds])
1037 allfeeds = dict([(feed.location, feed) for feed in feedlist])
1038 common, toadd, toremove = utils.listdiff(sfdict.keys(), current.keys())
1039 existing, nonexisting, ignore = utils.listdiff(
1040 toadd, allfeeds.keys())
1042 newfeeds = [Feed.create_new_feed(sfdict[f], f) for f in nonexisting]
1043 feedlist.extend(self.feedlist, newfeeds, from_sub=True) # will call extend_feed
1044 self.extend_feed([allfeeds[f] for f in existing], True)
1046 for f in toremove:
1047 index = self.index_feed(allfeeds[f])
1048 member = self.feedlist[index]
1049 if member.from_subscription:
1050 self.remove_feed(allfeeds[f])
1051 return
1053 def _subscription_changed(self, *args):
1054 self.emit('changed')
1056 def _subscription_contents_updated(self, *args):
1057 self.read_contents_from_subscription()
1059 def __str__(self):
1060 return "FeedCategory %s" % self.title
1062 def __hash__(self):
1063 return hash(id(self))
1065 def append_feed(self, value, from_sub):
1066 self.feedlist.append(CategoryMember(value, from_sub))
1067 self.emit('feed-added', value)
1069 def extend_feed(self, values, from_sub):
1070 self.feedlist.extend([CategoryMember(v, from_sub) for v in values])
1071 self.emit('changed')
1073 def insert_feed(self, index, value, from_sub):
1074 self.feedlist.insert(index, CategoryMember(value, from_sub))
1075 self.emit('feed-added', value)
1077 def remove(self, value):
1078 self.feedlist.remove(value)
1079 self.emit('feed-removed', value.feed)
1081 def remove_feed(self, value):
1082 for index, member in enumerate(self.feedlist):
1083 if member.feed is value:
1084 del self.feedlist[index]
1085 break
1086 else:
1087 raise ValueError(value)
1088 self.emit('feed-removed', value)
1090 def reverse(self):
1091 self.feedlist.reverse()
1092 self.emit('changed') # reverse=True))
1094 def index_feed(self, value):
1095 for index, f in enumerate(self.feedlist):
1096 if self.feedlist[index].feed is value:
1097 return index
1098 raise ValueError(value)
1100 def _sort_dsu(self, seq):
1101 aux_list = [(x.feed.title.lower(), x) for x in seq]
1102 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
1103 return [x[1] for x in aux_list]
1105 def sort(self, indices=None):
1106 if not indices or len(indices) == 1:
1107 self.feedlist[:] = self._sort_dsu(self.feedlist)
1108 else:
1109 items = self._sort_dsu(indices)
1110 for i,x in enumerate(items):
1111 list.__setitem__(self.feedlist, indices[i], items[i])
1112 self.emit('changed')
1114 def move_feed(self, source, target):
1115 if target > source:
1116 target -= 1
1117 if target == source:
1118 return
1119 t = self[source]
1120 del self[source]
1121 self.feedlist.insert(target, t)
1122 self.emit('changed')
1124 def dump(self):
1125 head = {'title': self.title}
1126 if self.subscription is not None:
1127 head['subscription'] = self.subscription.dump()
1128 return [head] + [
1129 {'id': f.feed.id, 'from_subscription': f.from_subscription}
1130 for f in self.feedlist]
1132 @property
1133 def feeds(self):
1134 return [f.feed for f in self.feedlist]
1136 def __eq__(self, ob):
1137 if isinstance(ob, types.NoneType):
1138 return 0
1139 elif isinstance(ob, FeedCategory):
1140 return self.title == ob.title and list.__eq__(self.feedlist, ob)
1141 else:
1142 raise NotImplementedError
1144 def __contains__(self, item):
1145 error.log("warning, should probably be querying the feeds property instead?")
1146 return list.__contains__(self.feedlist, item)
1148 class PseudoCategory(FeedCategory):
1149 def __init__(self, title="", key=None):
1150 if key not in (PSEUDO_ALL_KEY, PSEUDO_UNCATEGORIZED_KEY):
1151 raise ValueError, "Invalid key"
1152 FeedCategory.__init__(self, title)
1153 self._pseudo_key = key
1155 def __str__(self):
1156 return "PseudoCategory %s" % self.title
1158 def dump(self):
1159 return [{'pseudo': self._pseudo_key, 'title': ''}] + [
1160 {'id': f.feed.id, 'from_subscription': False} for f in self.feedlist]
1162 def append_feed(self, feed, from_sub):
1163 assert not from_sub
1164 FeedCategory.append_feed(self, feed, False)
1166 def insert_feed(self, index, feed, from_sub):
1167 assert not from_sub
1168 FeedCategory.insert_feed(self, index, feed, False)
1170 fclist = None
1172 def get_category_list():
1173 global fclist
1174 if fclist is None:
1175 fclist = FeedCategoryList()
1176 return fclist
1178 category_list = get_category_list()