1 """ FeedCategoryList.py
3 Module for feed categories and category subscriptions.
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
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. """
22 from StringIO
import StringIO
31 PSEUDO_ALL_KEY
= 'ALL'
32 PSEUDO_UNCATEGORIZED_KEY
= 'UNCATEGORIZED'
34 class FeedCategoryList(object, Event
.SignalEmitter
):
36 Event
.SignalEmitter
.__init
__(self
)
37 self
.initialize_slots(Event
.FeedCategoryChangedSignal
,
38 Event
.FeedCategorySortedSignal
,
39 Event
.FeedCategoryAddedSignal
,
40 Event
.FeedCategoryRemovedSignal
,
41 Event
.FeedCategoryListLoadedSignal
)
42 # have to define this here so the titles can be translated
43 PSEUDO_TITLES
= {PSEUDO_ALL_KEY
: _('All'),
44 PSEUDO_UNCATEGORIZED_KEY
: _('Uncategorized')}
45 self
._all
_category
= PseudoCategory(PSEUDO_TITLES
[PSEUDO_ALL_KEY
],
47 self
._un
_category
= PseudoCategory(
48 PSEUDO_TITLES
[PSEUDO_UNCATEGORIZED_KEY
], PSEUDO_UNCATEGORIZED_KEY
)
49 self
._user
_categories
= []
50 self
._pseudo
_categories
= (self
._all
_category
, self
._un
_category
)
52 self
._feedlist
= feeds
.get_instance()
53 self
._feedlist
.connect('deleted', self
.feed_deleted
)
54 self
._feedlist
.connect('updated', self
.feed_created
)
55 self
._feedlist
.connect('imported', self
.feeds_imported
)
58 """Loads categories from config.
59 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
60 {'id', feed_id1, 'from_subscription': bool} ...], ...]
62 cats
= Config
.get_instance().categories
or []
67 pseudo_key
= head
.get('pseudo', None)
68 if pseudo_key
== PSEUDO_ALL_KEY
:
69 fc
= self
.all_category
70 elif pseudo_key
== PSEUDO_UNCATEGORIZED_KEY
:
73 fc
= FeedCategory(head
['title'])
74 sub
= head
.get('subscription', None)
76 fc
.subscription
= undump_subscription(sub
)
78 # backwards compatibility for stuff saved with versions <= 0.23
84 from_sub
= f
['from_subscription']
85 feed
= self
._feedlist
.get_feed_with_id(fid
)
88 error
.log("%s (%d) was already in %s, skipping" % (str(feed
), fid
, str(fc
)))
91 fc
.append_feed(feed
, from_sub
)
92 categorized
[feed
] = True
93 # User categories: connect pseudos later
95 fc
.signal_connect(Event
.FeedCategoryChangedSignal
,
96 self
.category_changed
)
97 self
._user
_categories
.append(fc
)
98 # just in case we've missed any feeds, go through the list
99 # and add to the pseudocategories. cache the feed list of all_category
100 # so we don't get a function call (and a list comprehension loop
101 # inside it) on each feed. it should be ok here, there are no
102 # duplicates in feedlist. right?
103 pseudos_changed
= False
104 all_feeds
= self
.all_category
.feeds
105 for f
in self
._feedlist
:
106 if f
not in all_feeds
:
107 self
.all_category
.append_feed(f
, False)
108 pseudos_changed
= True
109 uf
= categorized
.get(f
, None)
111 self
.un_category
.append_feed(f
, False)
112 pseudos_changed
= True
115 for cat
in self
.pseudo_categories
:
117 Event
.FeedCategoryChangedSignal
, self
.pseudo_category_changed
)
118 self
.emit_signal(Event
.FeedCategoryListLoadedSignal(self
))
121 Config
.get_instance().categories
= [
122 cat
.dump() for cat
in self
]
124 def pseudo_category_changed(self
, signal
):
126 self
.emit_signal(signal
)
128 def category_changed(self
, signal
):
129 if signal
.feed
is not None:
131 for cat
in self
.user_categories
:
132 if signal
.feed
in cat
.feeds
:
133 uncategorized
= False
136 self
.un_category
.append_feed(signal
.feed
, False)
139 self
.un_category
.remove_feed(signal
.feed
)
143 self
.emit_signal(signal
)
145 def feed_deleted(self
, feedlist
, feed
):
152 def feed_created(self
, feedlist
, value
, category
, index
):
153 if category
and category
not in self
.pseudo_categories
:
155 category
.insert_feed(index
, value
, False)
157 category
.append_feed(value
, False)
159 self
.un_category
.append_feed(value
, False)
160 self
.all_category
.append_feed(value
, False)
162 def feeds_imported(self
, feedlist
, feeds
, category
, from_sub
):
163 if category
and category
not in self
.pseudo_categories
:
164 category
.extend_feed(feeds
, from_sub
)
166 self
.un_category
.extend_feed(feeds
, from_sub
)
167 self
.all_category
.extend_feed(feeds
, from_sub
)
171 def user_categories(self
):
172 return self
._user
_categories
175 def pseudo_categories(self
):
176 return self
._pseudo
_categories
179 def all_categories(self
):
180 return self
.pseudo_categories
+ tuple(self
.user_categories
)
183 def all_category(self
):
184 return self
._all
_category
187 def un_category(self
):
188 return self
._un
_category
190 class CategoryIterator
:
191 def __init__(self
, fclist
):
192 self
._fclist
= fclist
201 uclen
= len(self
._fclist
.user_categories
)
203 return self
._fclist
.user_categories
[i
]
204 elif i
< uclen
+ len(self
._fclist
.pseudo_categories
):
205 return self
._fclist
.pseudo_categories
[i
- uclen
]
214 return self
.CategoryIterator(self
)
216 def add_category(self
, category
):
217 category
.signal_connect(Event
.FeedCategoryChangedSignal
,
218 self
.category_changed
)
219 self
._user
_categories
.append(category
)
220 auxlist
= [(x
.title
.lower(),x
) for x
in self
._user
_categories
]
222 self
._user
_categories
= [x
[1] for x
in auxlist
]
223 self
.emit_signal(Event
.FeedCategoryAddedSignal(self
, category
))
226 def remove_category(self
, category
):
227 for feed
in category
.feeds
:
228 category
.remove_feed(feed
)
229 category
.signal_disconnect(Event
.FeedCategoryChangedSignal
,
230 self
.category_changed
)
231 self
._user
_categories
.remove(category
)
232 self
.emit_signal(Event
.FeedCategoryRemovedSignal(self
, category
))
235 # It might be good to have a superclass FeedCategorySubscription or something
236 # so we could support different formats. However, I don't know of any other
237 # relevant format used for this purpose, so that can be done later if needed.
238 # Of course, they could also just implement the same interface.
239 class OPMLCategorySubscription(object, Event
.SignalEmitter
):
242 def __init__(self
, location
=None):
243 Event
.SignalEmitter
.__init
__(self
)
244 self
.initialize_slots(Event
.FeedCategoryChangedSignal
,
245 Event
.SubscriptionContentsUpdatedSignal
)
246 self
._location
= location
247 self
._username
= None
248 self
._password
= None
249 self
._contents
= None
250 self
._frequency
= OPMLCategorySubscription
.REFRESH_DEFAULT
258 return self
._location
259 def fset(self
, location
):
260 self
._location
= location
261 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
262 return property(**locals())
268 return self
._username
269 def fset(self
, username
):
270 self
._username
= username
271 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
272 return property(**locals())
278 return self
._password
280 self
._password
= password
281 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
282 return property(**locals())
288 return self
._frequency
289 def fset(self
, freq
):
290 self
._frequency
= freq
291 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
292 return property(**locals())
298 return self
._last
_poll
299 def fset(self
, last_poll
):
300 self
._last
_poll
= last_poll
301 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
302 return property(**locals())
309 def fset(self
, error
):
311 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
312 return property(**locals())
314 def parse(self
, data
):
315 datastream
= StringIO(data
)
316 entries
= OPMLImport
.read(datastream
)
317 contents
= [(e
.url
, e
.text
) for e
in entries
]
318 updated
= contents
== self
._contents
319 self
._contents
= contents
321 self
.emit_signal(Event
.SubscriptionContentsUpdatedSignal(self
))
326 return self
._contents
329 def undump(klass
, dictionary
):
331 sub
.location
= dictionary
.get('location')
332 sub
.username
= dictionary
.get('username')
333 sub
.password
= dictionary
.get('password')
334 sub
.frequency
= dictionary
.get(
335 'frequency', OPMLCategorySubscription
.REFRESH_DEFAULT
)
336 sub
.last_poll
= dictionary
.get('last_poll', 0)
337 sub
.error
= dictionary
.get('error')
341 return {'type': 'opml',
342 'location': self
.location
,
343 'username': self
.username
,
344 'password': self
.password
,
345 'frequency': self
.frequency
,
346 'last_poll': self
.last_poll
,
349 def undump_subscription(dictionary
):
351 if dictionary
.get('type') == 'opml':
352 return OPMLCategorySubscription
.undump(dictionary
)
354 error
.log("exception while undumping subscription: " + str(e
))
357 class CategoryMember(object):
358 def __init__(self
, feed
=None, from_sub
=False):
360 self
._from
_subscription
= from_sub
367 def fset(self
, feed
):
369 return property(**locals())
372 def from_subscription():
375 return self
._from
_subscription
377 self
._from
_subscription
= p
378 return property(**locals())
380 class FeedCategory(list, Event
.SignalEmitter
):
381 def __init__(self
, title
=""):
382 Event
.SignalEmitter
.__init
__(self
)
383 self
.initialize_slots(Event
.FeedCategoryChangedSignal
,
384 Event
.FeedCategorySortedSignal
)
386 self
._subscription
= None
393 def fset(self
, title
):
395 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
396 return property(**locals())
402 return self
._subscription
404 if self
._subscription
:
405 self
._subscription
.signal_disconnect(
406 Event
.FeedCategoryChangedSignal
, self
._subscription
_changed
)
407 self
._subscription
.signal_disconnect(
408 Event
.SubscriptionContentsUpdatedSignal
,
409 self
._subscription
_contents
_updated
)
411 sub
.signal_connect(Event
.FeedCategoryChangedSignal
,
412 self
._subscription
_changed
)
413 sub
.signal_connect(Event
.SubscriptionContentsUpdatedSignal
,
414 self
._subscription
_contents
_updated
)
415 self
._subscription
= sub
416 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
417 return property(**locals())
419 def read_contents_from_subscription(self
):
420 if self
.subscription
is None:
422 subfeeds
= self
.subscription
.contents
423 sfdict
= dict(subfeeds
)
424 feedlist
= feeds
.get_instance()
425 current
= dict([(feed
.location
, feed
) for feed
in self
.feeds
])
426 allfeeds
= dict([(feed
.location
, feed
) for feed
in feedlist
])
427 common
, toadd
, toremove
= utils
.listdiff(sfdict
.keys(), current
.keys())
428 existing
, nonexisting
, ignore
= utils
.listdiff(
429 toadd
, allfeeds
.keys())
431 newfeeds
= [feeds
.Feed
.create_new_feed(sfdict
[f
], f
) for f
in nonexisting
]
432 feedlist
.extend(self
, newfeeds
, from_sub
=True) # will call extend_feed
433 self
.extend_feed([allfeeds
[f
] for f
in existing
], True)
436 index
= self
.index_feed(allfeeds
[f
])
438 if member
.from_subscription
:
439 self
.remove_feed(allfeeds
[f
])
442 def _subscription_changed(self
, signal
):
443 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
445 def _subscription_contents_updated(self
, signal
):
446 self
.read_contents_from_subscription()
449 return "FeedCategory %s" % self
.title
452 return hash(id(self
))
454 def append(self
, value
):
455 error
.log("warning, probably should be using append_feed?")
456 list.append(self
, value
)
457 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
.feed
))
459 def append_feed(self
, value
, from_sub
):
460 list.append(self
, CategoryMember(value
, from_sub
))
461 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
))
463 def extend_feed(self
, values
, from_sub
):
464 list.extend(self
, [CategoryMember(v
, from_sub
) for v
in values
])
465 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
467 def insert(self
, index
, value
):
468 error
.log("warning, probably should be using insert_feed?")
469 list.insert(self
, index
, value
)
470 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
.feed
))
472 def insert_feed(self
, index
, value
, from_sub
):
473 list.insert(self
, index
, CategoryMember(value
, from_sub
))
474 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
))
476 def remove(self
, value
):
477 list.remove(self
, value
)
478 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
.feed
))
480 def remove_feed(self
, value
):
481 for index
, member
in enumerate(self
):
482 if member
.feed
is value
:
486 raise ValueError(value
)
487 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
))
491 self
.emit_signal(Event
.FeedCategorySortedSignal(self
,
494 def index_feed(self
, value
):
495 for index
, f
in enumerate(self
):
496 if self
[index
].feed
is value
:
498 raise ValueError(value
)
500 def _sort_dsu(self
, seq
):
501 aux_list
= [(x
.feed
.title
.lower(), x
) for x
in seq
]
502 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
503 return [x
[1] for x
in aux_list
]
505 def sort(self
, indices
=None):
506 if not indices
or len(indices
) == 1:
507 self
[:] = self
._sort
_dsu
(self
)
509 items
= self
._sort
_dsu
(indices
)
510 for i
,x
in enumerate(items
):
511 list.__setitem
__(self
, indices
[i
], items
[i
])
512 self
.emit_signal(Event
.FeedCategorySortedSignal(self
))
514 def move_feed(self
, source
, target
):
521 list.insert(self
, target
, t
)
522 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
525 head
= {'title': self
.title
}
526 if self
.subscription
is not None:
527 head
['subscription'] = self
.subscription
.dump()
529 {'id': f
.feed
.id, 'from_subscription': f
.from_subscription
}
534 return [f
.feed
for f
in self
]
536 def __eq__(self
, ob
):
537 if isinstance(ob
, types
.NoneType
):
539 elif isinstance(ob
, FeedCategory
):
540 return self
.title
== ob
.title
and list.__eq
__(self
, ob
)
542 raise NotImplementedError
544 def __contains__(self
, item
):
545 error
.log("warning, should probably be querying the feeds property instead?")
546 return list.__contains
__(self
, item
)
548 class PseudoCategory(FeedCategory
):
549 def __init__(self
, title
="", key
=None):
550 if key
not in (PSEUDO_ALL_KEY
, PSEUDO_UNCATEGORIZED_KEY
):
551 raise ValueError, "Invalid key"
552 FeedCategory
.__init
__(self
, title
)
553 self
._pseudo
_key
= key
556 return "PseudoCategory %s" % self
.title
559 return [{'pseudo': self
._pseudo
_key
, 'title': ''}] + [
560 {'id': f
.feed
.id, 'from_subscription': False} for f
in self
]
562 def append_feed(self
, feed
, from_sub
):
564 FeedCategory
.append_feed(self
, feed
, False)
566 def insert_feed(self
, index
, feed
, from_sub
):
568 FeedCategory
.insert_feed(self
, index
, feed
, False)
575 fclist
= FeedCategoryList()