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
32 PSEUDO_ALL_KEY
= 'ALL'
33 PSEUDO_UNCATEGORIZED_KEY
= 'UNCATEGORIZED'
35 class FeedCategoryList(object, Event
.SignalEmitter
):
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
],
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
)
53 self
._feedlist
= FeedList
.get_instance()
54 self
._feedlist
.signal_connect(Event
.FeedDeletedSignal
,
56 self
._feedlist
.signal_connect(Event
.FeedCreatedSignal
,
58 self
._feedlist
.signal_connect(Event
.FeedsImportedSignal
,
62 """Loads categories from config.
63 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
64 {'id', feed_id1, 'from_subscription': bool} ...], ...]
66 cats
= Config
.get_instance().categories
or []
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
:
77 fc
= FeedCategory(head
['title'])
78 sub
= head
.get('subscription', None)
80 fc
.subscription
= undump_subscription(sub
)
82 # backwards compatibility for stuff saved with versions <= 0.23
88 from_sub
= f
['from_subscription']
89 feed
= self
._feedlist
.get_feed_with_id(fid
)
92 error
.log("%s (%d) was already in %s, skipping" % (str(feed
), fid
, str(fc
)))
95 fc
.append_feed(feed
, from_sub
)
96 categorized
[feed
] = True
97 # User categories: connect pseudos later
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)
115 self
.un_category
.append_feed(f
, False)
116 pseudos_changed
= True
119 for cat
in self
.pseudo_categories
:
121 Event
.FeedCategoryChangedSignal
, self
.pseudo_category_changed
)
122 self
.emit_signal(Event
.FeedCategoryListLoadedSignal(self
))
125 Config
.get_instance().categories
= [
126 cat
.dump() for cat
in self
]
128 def pseudo_category_changed(self
, signal
):
130 self
.emit_signal(signal
)
132 def category_changed(self
, signal
):
133 if signal
.feed
is not None:
135 for cat
in self
.user_categories
:
136 if signal
.feed
in cat
.feeds
:
137 uncategorized
= False
140 self
.un_category
.append_feed(signal
.feed
, False)
143 self
.un_category
.remove_feed(signal
.feed
)
147 self
.emit_signal(signal
)
149 def feed_deleted(self
, signal
):
152 c
.remove_feed(signal
.feed
)
156 def feed_created(self
, signal
):
158 category
= signal
.category
160 if category
and category
not in self
.pseudo_categories
:
162 category
.insert_feed(index
, value
, False)
164 category
.append_feed(value
, False)
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
172 from_sub
= signal
.from_sub
174 if category
and category
not in self
.pseudo_categories
:
175 category
.extend_feed(feeds
, from_sub
)
177 self
.un_category
.extend_feed(feeds
, from_sub
)
178 self
.all_category
.extend_feed(feeds
, from_sub
)
182 def user_categories(self
):
183 return self
._user
_categories
186 def pseudo_categories(self
):
187 return self
._pseudo
_categories
190 def all_categories(self
):
191 return self
.pseudo_categories
+ tuple(self
.user_categories
)
194 def all_category(self
):
195 return self
._all
_category
198 def un_category(self
):
199 return self
._un
_category
201 class CategoryIterator
:
202 def __init__(self
, fclist
):
203 self
._fclist
= fclist
212 uclen
= len(self
._fclist
.user_categories
)
214 return self
._fclist
.user_categories
[i
]
215 elif i
< uclen
+ len(self
._fclist
.pseudo_categories
):
216 return self
._fclist
.pseudo_categories
[i
- uclen
]
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
]
233 self
._user
_categories
= [x
[1] for x
in auxlist
]
234 self
.emit_signal(Event
.FeedCategoryAddedSignal(self
, category
))
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
))
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
):
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
270 return self
._location
271 def fset(self
, location
):
272 self
._location
= location
273 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
274 return property(**locals())
280 return self
._username
281 def fset(self
, username
):
282 self
._username
= username
283 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
284 return property(**locals())
290 return self
._password
292 self
._password
= password
293 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
294 return property(**locals())
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())
310 return self
._frequency
311 def fset(self
, freq
):
312 self
._frequency
= freq
313 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
314 return property(**locals())
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())
331 def fset(self
, 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
343 self
.emit_signal(Event
.SubscriptionContentsUpdatedSignal(self
))
348 return self
._contents
351 def undump(klass
, dictionary
):
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')
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
,
371 def undump_subscription(dictionary
):
373 if dictionary
.get('type') == 'opml':
374 return OPMLCategorySubscription
.undump(dictionary
)
376 error
.log("exception while undumping subscription: " + str(e
))
379 class CategoryMember(object):
380 def __init__(self
, feed
=None, from_sub
=False):
382 self
._from
_subscription
= from_sub
389 def fset(self
, feed
):
391 return property(**locals())
394 def from_subscription():
397 return self
._from
_subscription
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
)
408 self
._subscription
= None
415 def fset(self
, title
):
417 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
418 return property(**locals())
424 return self
._subscription
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
)
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:
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)
458 index
= self
.index_feed(allfeeds
[f
])
460 if member
.from_subscription
:
461 self
.remove_feed(allfeeds
[f
])
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()
471 return "FeedCategory %s" % self
.title
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
:
508 raise ValueError(value
)
509 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
, feed
=value
))
513 self
.emit_signal(Event
.FeedCategorySortedSignal(self
,
516 def index_feed(self
, value
):
517 for index
, f
in enumerate(self
):
518 if self
[index
].feed
is value
:
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
)
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
):
543 list.insert(self
, target
, t
)
544 self
.emit_signal(Event
.FeedCategoryChangedSignal(self
))
547 head
= {'title': self
.title
}
548 if self
.subscription
is not None:
549 head
['subscription'] = self
.subscription
.dump()
551 {'id': f
.feed
.id, 'from_subscription': f
.from_subscription
}
556 return [f
.feed
for f
in self
]
558 def __eq__(self
, ob
):
559 if isinstance(ob
, types
.NoneType
):
561 elif isinstance(ob
, FeedCategory
):
562 return self
.title
== ob
.title
and list.__eq
__(self
, ob
)
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
578 return "PseudoCategory %s" % self
.title
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
):
586 FeedCategory
.append_feed(self
, feed
, False)
588 def insert_feed(self
, index
, feed
, from_sub
):
590 FeedCategory
.insert_feed(self
, index
, feed
, False)
597 fclist
= FeedCategoryList()