Updated Arabic Translation by Djihed Afifi.
[straw.git] / src / lib / FeedCategoryList.py
blob40f88718da13a48598717edb498cdba7faf513e2
1 """ FeedCategoryList.py
3 Module for feed categories and category subscriptions.
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """ GNU General Public License
8 Straw is free software; you can redistribute it and/or modify it under the
9 terms of the GNU General Public License as published by the Free Software
10 Foundation; either version 2 of the License, or (at your option) any later
11 version.
13 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
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., 59 Temple
19 Place - Suite 330, Boston, MA 02111-1307, USA. """
21 import Feed
22 from StringIO import StringIO
23 import Config
24 import Event
25 import FeedList
26 import OPMLImport
27 import error
28 import locale
29 import utils
30 import types
32 PSEUDO_ALL_KEY = 'ALL'
33 PSEUDO_UNCATEGORIZED_KEY = 'UNCATEGORIZED'
35 class FeedCategoryList(object, Event.SignalEmitter):
36 def __init__(self):
37 Event.SignalEmitter.__init__(self)
38 self.initialize_slots(Event.FeedCategoryChangedSignal,
39 Event.FeedCategorySortedSignal,
40 Event.FeedCategoryAddedSignal,
41 Event.FeedCategoryRemovedSignal,
42 Event.FeedCategoryListLoadedSignal)
43 # have to define this here so the titles can be translated
44 PSEUDO_TITLES = {PSEUDO_ALL_KEY: _('All'),
45 PSEUDO_UNCATEGORIZED_KEY: _('Uncategorized')}
46 self._all_category = PseudoCategory(PSEUDO_TITLES[PSEUDO_ALL_KEY],
47 PSEUDO_ALL_KEY)
48 self._un_category = PseudoCategory(
49 PSEUDO_TITLES[PSEUDO_UNCATEGORIZED_KEY], PSEUDO_UNCATEGORIZED_KEY)
50 self._user_categories = []
51 self._pseudo_categories = (self._all_category, self._un_category)
52 self._loading = False
53 self._feedlist = FeedList.get_instance()
54 self._feedlist.signal_connect(Event.FeedDeletedSignal,
55 self.feed_deleted)
56 self._feedlist.signal_connect(Event.FeedCreatedSignal,
57 self.feed_created)
58 self._feedlist.signal_connect(Event.FeedsImportedSignal,
59 self.feeds_imported)
61 def load_data(self):
62 """Loads categories from config.
63 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
64 {'id', feed_id1, 'from_subscription': bool} ...], ...]
65 """
66 cats = Config.get_instance().categories or []
67 categorized = {}
68 for c in cats:
69 head = c[0]
70 tail = c[1:]
71 pseudo_key = head.get('pseudo', None)
72 if pseudo_key == PSEUDO_ALL_KEY:
73 fc = self.all_category
74 elif pseudo_key == PSEUDO_UNCATEGORIZED_KEY:
75 fc = self.un_category
76 else:
77 fc = FeedCategory(head['title'])
78 sub = head.get('subscription', None)
79 if sub is not None:
80 fc.subscription = undump_subscription(sub)
81 for f in tail:
82 # backwards compatibility for stuff saved with versions <= 0.23
83 if type(f) is int:
84 fid = f
85 from_sub = False
86 else:
87 fid = f['id']
88 from_sub = f['from_subscription']
89 feed = self._feedlist.get_feed_with_id(fid)
90 if feed is not None:
91 if feed in fc.feeds:
92 error.log("%s (%d) was already in %s, skipping" % (str(feed), fid, str(fc)))
93 continue
95 fc.append_feed(feed, from_sub)
96 categorized[feed] = True
97 # User categories: connect pseudos later
98 if not pseudo_key:
99 fc.signal_connect(Event.FeedCategoryChangedSignal,
100 self.category_changed)
101 self._user_categories.append(fc)
102 # just in case we've missed any feeds, go through the list
103 # and add to the pseudocategories. cache the feed list of all_category
104 # so we don't get a function call (and a list comprehension loop
105 # inside it) on each feed. it should be ok here, there are no
106 # duplicates in feedlist. right?
107 pseudos_changed = False
108 all_feeds = self.all_category.feeds
109 for f in self._feedlist:
110 if f not in all_feeds:
111 self.all_category.append_feed(f, False)
112 pseudos_changed = True
113 uf = categorized.get(f, None)
114 if uf is None:
115 self.un_category.append_feed(f, False)
116 pseudos_changed = True
117 if pseudos_changed:
118 self.save_data()
119 for cat in self.pseudo_categories:
120 cat.signal_connect(
121 Event.FeedCategoryChangedSignal, self.pseudo_category_changed)
122 self.emit_signal(Event.FeedCategoryListLoadedSignal(self))
124 def save_data(self):
125 Config.get_instance().categories = [
126 cat.dump() for cat in self]
128 def pseudo_category_changed(self, signal):
129 self.save_data()
130 self.emit_signal(signal)
132 def category_changed(self, signal):
133 if signal.feed is not None:
134 uncategorized = True
135 for cat in self.user_categories:
136 if signal.feed in cat.feeds:
137 uncategorized = False
138 break
139 if uncategorized:
140 self.un_category.append_feed(signal.feed, False)
141 else:
142 try:
143 self.un_category.remove_feed(signal.feed)
144 except ValueError:
145 pass
146 self.save_data()
147 self.emit_signal(signal)
149 def feed_deleted(self, signal):
150 for c in self:
151 try:
152 c.remove_feed(signal.feed)
153 except ValueError:
154 pass
156 def feed_created(self, signal):
157 value = signal.feed
158 category = signal.category
159 index = signal.index
160 if category and category not in self.pseudo_categories:
161 if index:
162 category.insert_feed(index, value, False)
163 else:
164 category.append_feed(value, False)
165 else:
166 self.un_category.append_feed(value, False)
167 self.all_category.append_feed(value, False)
169 def feeds_imported(self, signal):
170 category = signal.category
171 feeds = signal.feeds
172 from_sub = signal.from_sub
174 if category and category not in self.pseudo_categories:
175 category.extend_feed(feeds, from_sub)
176 else:
177 self.un_category.extend_feed(feeds, from_sub)
178 self.all_category.extend_feed(feeds, from_sub)
179 return
181 @property
182 def user_categories(self):
183 return self._user_categories
185 @property
186 def pseudo_categories(self):
187 return self._pseudo_categories
189 @property
190 def all_categories(self):
191 return self.pseudo_categories + tuple(self.user_categories)
193 @property
194 def all_category(self):
195 return self._all_category
197 @property
198 def un_category(self):
199 return self._un_category
201 class CategoryIterator:
202 def __init__(self, fclist):
203 self._fclist = fclist
204 self._index = -1
206 def __iter__(self):
207 return self
209 def _next(self):
210 self._index += 1
211 i = self._index
212 uclen = len(self._fclist.user_categories)
213 if i < uclen:
214 return self._fclist.user_categories[i]
215 elif i < uclen + len(self._fclist.pseudo_categories):
216 return self._fclist.pseudo_categories[i - uclen]
217 else:
218 raise StopIteration
220 def next(self):
221 v = self._next()
222 return v
224 def __iter__(self):
225 return self.CategoryIterator(self)
227 def add_category(self, category):
228 category.signal_connect(Event.FeedCategoryChangedSignal,
229 self.category_changed)
230 self._user_categories.append(category)
231 auxlist = [(x.title.lower(),x) for x in self._user_categories]
232 auxlist.sort()
233 self._user_categories = [x[1] for x in auxlist]
234 self.emit_signal(Event.FeedCategoryAddedSignal(self, category))
235 self.save_data()
237 def remove_category(self, category):
238 for feed in category.feeds:
239 category.remove_feed(feed)
240 category.signal_disconnect(Event.FeedCategoryChangedSignal,
241 self.category_changed)
242 self._user_categories.remove(category)
243 self.emit_signal(Event.FeedCategoryRemovedSignal(self, category))
244 self.save_data()
246 # It might be good to have a superclass FeedCategorySubscription or something
247 # so we could support different formats. However, I don't know of any other
248 # relevant format used for this purpose, so that can be done later if needed.
249 # Of course, they could also just implement the same interface.
250 class OPMLCategorySubscription(object, Event.SignalEmitter):
251 REFRESH_DEFAULT = -1
253 def __init__(self, location=None):
254 Event.SignalEmitter.__init__(self)
255 self.initialize_slots(Event.FeedCategoryChangedSignal,
256 Event.SubscriptionContentsUpdatedSignal)
257 self._location = location
258 self._username = None
259 self._password = None
260 self._previous_etag = None
261 self._contents = None
262 self._frequency = OPMLCategorySubscription.REFRESH_DEFAULT
263 self._last_poll = 0
264 self._error = None
266 @apply
267 def location():
268 doc = ""
269 def fget(self):
270 return self._location
271 def fset(self, location):
272 self._location = location
273 self.emit_signal(Event.FeedCategoryChangedSignal(self))
274 return property(**locals())
276 @apply
277 def username():
278 doc = ""
279 def fget(self):
280 return self._username
281 def fset(self, username):
282 self._username = username
283 self.emit_signal(Event.FeedCategoryChangedSignal(self))
284 return property(**locals())
286 @apply
287 def password():
288 doc = ""
289 def fget(self):
290 return self._password
291 def fset(self):
292 self._password = password
293 self.emit_signal(Event.FeedCategoryChangedSignal(self))
294 return property(**locals())
296 @apply
297 def previous_etag():
298 doc = ""
299 def fget(self):
300 return self._previous_etag
301 def fset(self, etag):
302 self._previous_etag = etag
303 self.emit_signal(Event.FeedCategoryChangedSignal(self))
304 return property(**locals())
306 @apply
307 def frequency():
308 doc = ""
309 def fget(self):
310 return self._frequency
311 def fset(self, freq):
312 self._frequency = freq
313 self.emit_signal(Event.FeedCategoryChangedSignal(self))
314 return property(**locals())
316 @apply
317 def last_poll():
318 doc = ""
319 def fget(self):
320 return self._last_poll
321 def fset(self, last_poll):
322 self._last_poll = last_poll
323 self.emit_signal(Event.FeedCategoryChangedSignal(self))
324 return property(**locals())
326 @apply
327 def error():
328 doc = ""
329 def fget(self):
330 return self._error
331 def fset(self, error):
332 self._error = error
333 self.emit_signal(Event.FeedCategoryChangedSignal(self))
334 return property(**locals())
336 def parse(self, data):
337 datastream = StringIO(data)
338 entries = OPMLImport.read(datastream)
339 contents = [(e.url, e.text) for e in entries]
340 updated = contents == self._contents
341 self._contents = contents
342 if updated:
343 self.emit_signal(Event.SubscriptionContentsUpdatedSignal(self))
344 return
346 @property
347 def contents(self):
348 return self._contents
350 @classmethod
351 def undump(klass, dictionary):
352 sub = klass()
353 sub.location = dictionary.get('location')
354 sub.username = dictionary.get('username')
355 sub.password = dictionary.get('password')
356 sub.frequency = dictionary.get(
357 'frequency', OPMLCategorySubscription.REFRESH_DEFAULT)
358 sub.last_poll = dictionary.get('last_poll', 0)
359 sub.error = dictionary.get('error')
360 return sub
362 def dump(self):
363 return {'type': 'opml',
364 'location': self.location,
365 'username': self.username,
366 'password': self.password,
367 'frequency': self.frequency,
368 'last_poll': self.last_poll,
369 'error': self.error}
371 def undump_subscription(dictionary):
372 try:
373 if dictionary.get('type') == 'opml':
374 return OPMLCategorySubscription.undump(dictionary)
375 except Exception, e:
376 error.log("exception while undumping subscription: " + str(e))
377 raise
379 class CategoryMember(object):
380 def __init__(self, feed=None, from_sub=False):
381 self._feed = feed
382 self._from_subscription = from_sub
384 @apply
385 def feed():
386 doc = ""
387 def fget(self):
388 return self._feed
389 def fset(self, feed):
390 self._feed = feed
391 return property(**locals())
393 @apply
394 def from_subscription():
395 doc = ""
396 def fget(self):
397 return self._from_subscription
398 def fset(self, p):
399 self._from_subscription = p
400 return property(**locals())
402 class FeedCategory(list, Event.SignalEmitter):
403 def __init__(self, title=""):
404 Event.SignalEmitter.__init__(self)
405 self.initialize_slots(Event.FeedCategoryChangedSignal,
406 Event.FeedCategorySortedSignal)
407 self._title = title
408 self._subscription = None
410 @apply
411 def title():
412 doc = ""
413 def fget(self):
414 return self._title
415 def fset(self, title):
416 self._title = title
417 self.emit_signal(Event.FeedCategoryChangedSignal(self))
418 return property(**locals())
420 @apply
421 def subscription():
422 doc = ""
423 def fget(self):
424 return self._subscription
425 def fset(self, sub):
426 if self._subscription:
427 self._subscription.signal_disconnect(
428 Event.FeedCategoryChangedSignal, self._subscription_changed)
429 self._subscription.signal_disconnect(
430 Event.SubscriptionContentsUpdatedSignal,
431 self._subscription_contents_updated)
432 if sub:
433 sub.signal_connect(Event.FeedCategoryChangedSignal,
434 self._subscription_changed)
435 sub.signal_connect(Event.SubscriptionContentsUpdatedSignal,
436 self._subscription_contents_updated)
437 self._subscription = sub
438 self.emit_signal(Event.FeedCategoryChangedSignal(self))
439 return property(**locals())
441 def read_contents_from_subscription(self):
442 if self.subscription is None:
443 return
444 subfeeds = self.subscription.contents
445 sfdict = dict(subfeeds)
446 feedlist = FeedList.get_instance()
447 current = dict([(feed.location, feed) for feed in self.feeds])
448 allfeeds = dict([(feed.location, feed) for feed in feedlist])
449 common, toadd, toremove = utils.listdiff(sfdict.keys(), current.keys())
450 existing, nonexisting, ignore = utils.listdiff(
451 toadd, allfeeds.keys())
453 newfeeds = [Feed.Feed.create_new_feed(sfdict[f], f) for f in nonexisting]
454 feedlist.extend(self, newfeeds, from_sub=True) # will call extend_feed
455 self.extend_feed([allfeeds[f] for f in existing], True)
457 for f in toremove:
458 index = self.index_feed(allfeeds[f])
459 member = self[index]
460 if member.from_subscription:
461 self.remove_feed(allfeeds[f])
462 return
464 def _subscription_changed(self, signal):
465 self.emit_signal(Event.FeedCategoryChangedSignal(self))
467 def _subscription_contents_updated(self, signal):
468 self.read_contents_from_subscription()
470 def __str__(self):
471 return "FeedCategory %s" % self.title
473 def __hash__(self):
474 return hash(id(self))
476 def append(self, value):
477 error.log("warning, probably should be using append_feed?")
478 list.append(self, value)
479 self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value.feed))
481 def append_feed(self, value, from_sub):
482 list.append(self, CategoryMember(value, from_sub))
483 self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value))
485 def extend_feed(self, values, from_sub):
486 list.extend(self, [CategoryMember(v, from_sub) for v in values])
487 self.emit_signal(Event.FeedCategoryChangedSignal(self))
489 def insert(self, index, value):
490 error.log("warning, probably should be using insert_feed?")
491 list.insert(self, index, value)
492 self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value.feed))
494 def insert_feed(self, index, value, from_sub):
495 list.insert(self, index, CategoryMember(value, from_sub))
496 self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value))
498 def remove(self, value):
499 list.remove(self, value)
500 self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value.feed))
502 def remove_feed(self, value):
503 for index, member in enumerate(self):
504 if member.feed is value:
505 del self[index]
506 break
507 else:
508 raise ValueError(value)
509 self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value))
511 def reverse(self):
512 list.reverse(self)
513 self.emit_signal(Event.FeedCategorySortedSignal(self,
514 reverse=True))
516 def index_feed(self, value):
517 for index, f in enumerate(self):
518 if self[index].feed is value:
519 return index
520 raise ValueError(value)
522 def _sort_dsu(self, seq):
523 aux_list = [(x.feed.title.lower(), x) for x in seq]
524 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
525 return [x[1] for x in aux_list]
527 def sort(self, indices=None):
528 if not indices or len(indices) == 1:
529 self[:] = self._sort_dsu(self)
530 else:
531 items = self._sort_dsu(indices)
532 for i,x in enumerate(items):
533 list.__setitem__(self, indices[i], items[i])
534 self.emit_signal(Event.FeedCategorySortedSignal(self))
536 def move_feed(self, source, target):
537 if target > source:
538 target -= 1
539 if target == source:
540 return
541 t = self[source]
542 del self[source]
543 list.insert(self, target, t)
544 self.emit_signal(Event.FeedCategoryChangedSignal(self))
546 def dump(self):
547 head = {'title': self.title}
548 if self.subscription is not None:
549 head['subscription'] = self.subscription.dump()
550 return [head] + [
551 {'id': f.feed.id, 'from_subscription': f.from_subscription}
552 for f in self]
554 @property
555 def feeds(self):
556 return [f.feed for f in self]
558 def __eq__(self, ob):
559 if isinstance(ob, types.NoneType):
560 return 0
561 elif isinstance(ob, FeedCategory):
562 return self.title == ob.title and list.__eq__(self, ob)
563 else:
564 raise NotImplementedError
566 def __contains__(self, item):
567 error.log("warning, should probably be querying the feeds property instead?")
568 return list.__contains__(self, item)
570 class PseudoCategory(FeedCategory):
571 def __init__(self, title="", key=None):
572 if key not in (PSEUDO_ALL_KEY, PSEUDO_UNCATEGORIZED_KEY):
573 raise ValueError, "Invalid key"
574 FeedCategory.__init__(self, title)
575 self._pseudo_key = key
577 def __str__(self):
578 return "PseudoCategory %s" % self.title
580 def dump(self):
581 return [{'pseudo': self._pseudo_key, 'title': ''}] + [
582 {'id': f.feed.id, 'from_subscription': False} for f in self]
584 def append_feed(self, feed, from_sub):
585 assert not from_sub
586 FeedCategory.append_feed(self, feed, False)
588 def insert_feed(self, index, feed, from_sub):
589 assert not from_sub
590 FeedCategory.insert_feed(self, index, feed, False)
592 fclist = None
594 def get_instance():
595 global fclist
596 if fclist is None:
597 fclist = FeedCategoryList()
598 return fclist