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. """
23 from StringIO
import StringIO
31 PSEUDO_ALL_KEY
= 'ALL'
32 PSEUDO_UNCATEGORIZED_KEY
= 'UNCATEGORIZED'
34 class FeedCategoryList(gobject
.GObject
):
37 'added' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,)),
38 'deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,)),
39 'category-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
40 'pseudo-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
44 gobject
.GObject
.__init
__(self
)
45 # have to define this here so the titles can be translated
46 PSEUDO_TITLES
= {PSEUDO_ALL_KEY
: _('All'),
47 PSEUDO_UNCATEGORIZED_KEY
: _('Uncategorized')}
48 self
._all
_category
= PseudoCategory(PSEUDO_TITLES
[PSEUDO_ALL_KEY
],
50 self
._un
_category
= PseudoCategory(
51 PSEUDO_TITLES
[PSEUDO_UNCATEGORIZED_KEY
], PSEUDO_UNCATEGORIZED_KEY
)
52 self
._user
_categories
= []
53 self
._pseudo
_categories
= (self
._all
_category
, self
._un
_category
)
55 self
._feedlist
= feeds
.get_instance()
56 self
._feedlist
.connect('deleted', self
.feed_deleted
)
57 self
._feedlist
.connect('updated', self
.feed_created
)
58 self
._feedlist
.connect('imported', self
.feeds_imported
)
61 """Loads categories from config.
62 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
63 {'id', feed_id1, 'from_subscription': bool} ...], ...]
65 cats
= Config
.get_instance().categories
or []
70 pseudo_key
= head
.get('pseudo', None)
71 if pseudo_key
== PSEUDO_ALL_KEY
:
72 fc
= self
.all_category
73 elif pseudo_key
== PSEUDO_UNCATEGORIZED_KEY
:
76 fc
= FeedCategory(head
['title'])
77 sub
= head
.get('subscription', None)
79 fc
.subscription
= undump_subscription(sub
)
81 # backwards compatibility for stuff saved with versions <= 0.23
87 from_sub
= f
['from_subscription']
88 feed
= self
._feedlist
.get_feed_with_id(fid
)
89 if feed
and not pseudo_key
: # we deal with pseudos later
91 error
.log("%s (%d) was already in %s, skipping" % (str(feed
), fid
, str(fc
)))
93 fc
.append_feed(feed
, from_sub
)
94 categorized
[feed
] = True
95 # User categories: connect pseudos later
97 fc
.connect('changed', self
.category_changed
)
98 self
._user
_categories
.append(fc
)
99 # just in case we've missed any feeds, go through the list
100 # and add to the pseudocategories. cache the feed list of all_category
101 # so we don't get a function call (and a list comprehension loop
102 # inside it) on each feed. it should be ok here, there are no
103 # duplicates in feedlist. right?
104 pseudos_changed
= False
105 all_feeds
= self
.all_category
.feeds
106 for f
in self
._feedlist
:
107 if f
not in all_feeds
:
108 self
.all_category
.append_feed(f
, False)
109 pseudos_changed
= True
110 uf
= categorized
.get(f
, None)
112 self
.un_category
.append_feed(f
, False)
113 pseudos_changed
= True
116 for cat
in self
.pseudo_categories
:
117 cat
.connect('changed', self
.pseudo_category_changed
)
120 Config
.get_instance().categories
= [
121 cat
.dump() for cat
in self
]
123 def pseudo_category_changed(self
, signal
):
125 self
.emit('pseudo-changed')
127 def category_changed(self
, signal
):
128 if signal
.feed
is not None:
130 for cat
in self
.user_categories
:
131 if signal
.feed
in cat
.feeds
:
132 uncategorized
= False
135 self
.un_category
.append_feed(signal
.feed
, False)
138 self
.un_category
.remove_feed(signal
.feed
)
142 self
.emit('category-changed')
144 def feed_deleted(self
, feedlist
, feed
):
151 def feed_created(self
, feedlist
, value
, category
, index
):
152 if category
and category
not in self
.pseudo_categories
:
154 category
.insert_feed(index
, value
, False)
156 category
.append_feed(value
, False)
158 self
.un_category
.append_feed(value
, False)
159 self
.all_category
.append_feed(value
, False)
161 def feeds_imported(self
, feedlist
, feeds
, category
, from_sub
):
162 if category
and category
not in self
.pseudo_categories
:
163 category
.extend_feed(feeds
, from_sub
)
165 print 'feed imported ', feeds
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
.connect('changed', self
.category_changed
)
218 self
._user
_categories
.append(category
)
219 auxlist
= [(x
.title
.lower(),x
) for x
in self
._user
_categories
]
221 self
._user
_categories
= [x
[1] for x
in auxlist
]
223 self
.emit('added', category
)
225 def remove_category(self
, category
):
226 for feed
in category
.feeds
:
227 category
.remove_feed(feed
)
228 self
._user
_categories
.remove(category
)
230 self
.emit('deleted', category
)
232 # It might be good to have a superclass FeedCategorySubscription or something
233 # so we could support different formats. However, I don't know of any other
234 # relevant format used for this purpose, so that can be done later if needed.
235 # Of course, they could also just implement the same interface.
236 class OPMLCategorySubscription(gobject
.GObject
):
240 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
241 'updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
244 def __init__(self
, location
=None):
245 gobject
.GObject
.__init
__(self
)
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
262 return property(**locals())
268 return self
._username
269 def fset(self
, username
):
270 self
._username
= username
272 return property(**locals())
278 return self
._password
280 self
._password
= password
282 return property(**locals())
288 return self
._frequency
289 def fset(self
, freq
):
290 self
._frequency
= freq
292 return property(**locals())
298 return self
._last
_poll
299 def fset(self
, last_poll
):
300 self
._last
_poll
= last_poll
302 return property(**locals())
309 def fset(self
, error
):
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
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(gobject
.GObject
):
383 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
384 (gobject
.TYPE_PYOBJECT
,))
387 def __init__(self
, title
=""):
388 gobject
.GObject
.__init
__(self
)
391 self
._subscription
= None
398 def fset(self
, title
):
400 self
.emit('changed', None)
401 return property(**locals())
407 return self
._subscription
409 self
._subscription
= sub
410 self
._subscription
.connect('changed', self
._subscription
_changed
)
411 self
._subscription
.connect('updated', self
._subscription
_contents
_updated
)
412 self
.emit('changed', None)
413 return property(**locals())
415 def read_contents_from_subscription(self
):
416 if self
.subscription
is None:
418 subfeeds
= self
.subscription
.contents
419 sfdict
= dict(subfeeds
)
420 feedlist
= feeds
.get_instance()
421 current
= dict([(feed
.location
, feed
) for feed
in self
.feeds
])
422 allfeeds
= dict([(feed
.location
, feed
) for feed
in feedlist
])
423 common
, toadd
, toremove
= utils
.listdiff(sfdict
.keys(), current
.keys())
424 existing
, nonexisting
, ignore
= utils
.listdiff(
425 toadd
, allfeeds
.keys())
427 newfeeds
= [feeds
.Feed
.create_new_feed(sfdict
[f
], f
) for f
in nonexisting
]
428 feedlist
.extend(self
.feedlist
, newfeeds
, from_sub
=True) # will call extend_feed
429 self
.extend_feed([allfeeds
[f
] for f
in existing
], True)
432 index
= self
.index_feed(allfeeds
[f
])
433 member
= self
.feedlist
[index
]
434 if member
.from_subscription
:
435 self
.remove_feed(allfeeds
[f
])
438 def _subscription_changed(self
, *args
):
439 self
.emit('changed', None)
441 def _subscription_contents_updated(self
, *args
):
442 self
.read_contents_from_subscription()
445 return "FeedCategory %s" % self
.title
448 return hash(id(self
))
450 def append(self
, value
):
451 error
.log("warning, probably should be using append_feed?")
452 self
.feedlist
.append(value
)
453 self
.emit('changed', value
.feed
)
455 def append_feed(self
, value
, from_sub
):
456 self
.feedlist
.append(CategoryMember(value
, from_sub
))
457 self
.emit('changed', value
)
459 def extend_feed(self
, values
, from_sub
):
460 self
.feedlist
.extend([CategoryMember(v
, from_sub
) for v
in values
])
461 self
.emit('changed', None)
463 def insert(self
, index
, value
):
464 error
.log("warning, probably should be using insert_feed?")
465 self
.feedlist
.insert(index
, value
)
466 self
.emit('changed', value
.feed
)
468 def insert_feed(self
, index
, value
, from_sub
):
469 self
.feedlist
.insert(index
, CategoryMember(value
, from_sub
))
470 self
.emit('changed', value
)
472 def remove(self
, value
):
473 self
.feedlist
.remove(value
)
474 self
.emit('changed', value
.feed
)
476 def remove_feed(self
, value
):
477 for index
, member
in enumerate(self
.feedlist
):
478 if member
.feed
is value
:
479 del self
.feedlist
[index
]
482 raise ValueError(value
)
483 self
.emit('changed', value
)
486 self
.feedlist
.reverse()
487 self
.emit('changed') # reverse=True))
489 def index_feed(self
, value
):
490 for index
, f
in enumerate(self
.feedlist
):
491 if self
.feedlist
[index
].feed
is value
:
493 raise ValueError(value
)
495 def _sort_dsu(self
, seq
):
496 aux_list
= [(x
.feed
.title
.lower(), x
) for x
in seq
]
497 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
498 return [x
[1] for x
in aux_list
]
500 def sort(self
, indices
=None):
501 if not indices
or len(indices
) == 1:
502 self
.feedlist
[:] = self
._sort
_dsu
(self
.feedlist
)
504 items
= self
._sort
_dsu
(indices
)
505 for i
,x
in enumerate(items
):
506 list.__setitem
__(self
.feedlist
, indices
[i
], items
[i
])
509 def move_feed(self
, source
, target
):
516 self
.feedlist
.insert(target
, t
)
520 head
= {'title': self
.title
}
521 if self
.subscription
is not None:
522 head
['subscription'] = self
.subscription
.dump()
524 {'id': f
.feed
.id, 'from_subscription': f
.from_subscription
}
525 for f
in self
.feedlist
]
529 return [f
.feed
for f
in self
.feedlist
]
531 def __eq__(self
, ob
):
532 if isinstance(ob
, types
.NoneType
):
534 elif isinstance(ob
, FeedCategory
):
535 return self
.title
== ob
.title
and list.__eq
__(self
.feedlist
, ob
)
537 raise NotImplementedError
539 def __contains__(self
, item
):
540 error
.log("warning, should probably be querying the feeds property instead?")
541 return list.__contains
__(self
.feedlist
, item
)
543 class PseudoCategory(FeedCategory
):
544 def __init__(self
, title
="", key
=None):
545 if key
not in (PSEUDO_ALL_KEY
, PSEUDO_UNCATEGORIZED_KEY
):
546 raise ValueError, "Invalid key"
547 FeedCategory
.__init
__(self
, title
)
548 self
._pseudo
_key
= key
551 return "PseudoCategory %s" % self
.title
554 return [{'pseudo': self
._pseudo
_key
, 'title': ''}] + [
555 {'id': f
.feed
.id, 'from_subscription': False} for f
in self
]
557 def append_feed(self
, feed
, from_sub
):
559 FeedCategory
.append_feed(self
, feed
, False)
561 def insert_feed(self
, index
, feed
, from_sub
):
563 FeedCategory
.insert_feed(self
, index
, feed
, False)
570 fclist
= FeedCategoryList()