7 class FeedList(gobject
.GObject
):
10 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
11 'updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
12 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, gobject
.TYPE_INT
,)),
13 'imported' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
14 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
15 gobject
.TYPE_BOOLEAN
,)),
16 'deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,))
19 def __init__(self
, init_seq
= []):
20 gobject
.GObject
.__init
__(self
)
22 #self.initialize_slots(Event.FeedsChangedSignal,
23 # Event.FeedDeletedSignal,
24 # Event.FeedCreatedSignal,
25 # Event.AllItemsReadSignal,
26 # Event.ItemReadSignal,
27 # Event.ItemsAddedSignal,
28 # Event.FeedPolledSignal,
29 # Event.FeedStatusChangedSignal,
30 # Event.FeedErrorStatusChangedSignal,
31 # Event.FeedsImportedSignal,
32 # Event.FeedDetailChangedSignal)
36 return iter(self
.feedlist
)
39 def _load(feedlist
, parent
):
41 if isinstance(df
, list):
42 #fc = straw.FeedCategory()
44 #self.append(parent, fc)
47 f
= Feed
.create_empty_feed()
49 self
.append(parent
, f
)
52 feedlist
= Config
.get_instance().feeds
58 def _forward_signal(self
, signal
):
59 self
.emit_signal(signal
)
61 def connect_signals(self
, ob
):
62 ob
.connect('info-updated', self
.feed_detail_changed
)
64 # these signals are forwarded so that listeners who just want to
65 # listen for a specific event regardless of what feed it came from can
66 # just connect to this feedlist instead of connecting to the
68 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
69 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
70 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
71 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
72 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
73 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
75 def __setitem__(self
, key
, value
):
76 self
.feedlist
.__setitem
__(key
, value
)
77 self
.connect_signals(value
)
78 self
.save_feeds_and_notify(True)
80 def extend(self
, parent
, values
, from_sub
=False):
81 list.extend(self
.feedlist
, values
)
84 self
.connect_signals(f
)
86 self
.emit('imported', values
, parent
, from_sub
)
88 def append(self
, parent
, value
):
89 self
.feedlist
.append(value
)
91 self
.connect_signals(value
)
93 self
.emit('updated', value
, parent
, -1)
95 def insert(self
, index
, parent
, value
):
96 self
.feedlist
.insert(index
, value
)
98 self
.connect_signals(value
)
100 self
.emit('updated', value
, parent
, index
)
102 def index(self
, feed
):
103 return self
.feedlist
.index(feed
)
105 def reorder(self
, move
, delta
):
111 if move
[0] == 0 and delta
< 0 or move
[-1] == (len(self
.feedlist
) - 1) and delta
> 0:
114 k
[m
+ delta
], k
[m
] = k
[m
], k
[m
+ delta
]
115 for i
in range(len(k
)):
116 list.__setitem
__(self
.feedlist
, i
, k
[i
])
120 def __delitem__(self
, value
):
121 feed
= self
.feedlist
[value
]
122 list.__delitem
__(self
.feedlist
, value
)
123 feed
.delete_all_items()
125 self
.emit('deleted', feed
)
127 def save_feeds(self
):
128 if not self
._loading
:
129 config
= Config
.get_instance()
130 config
.feeds
= [f
.dump() for f
in self
.feedlist
]
133 def feed_detail_changed(self
, feed
):
135 self
.emit('changed') # XXXX send the feed as well?
137 def _sort_dsu(self
, seq
):
138 aux_list
= [(x
.title
, x
) for x
in seq
]
139 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
140 return [x
[1] for x
in aux_list
]
142 def sort(self
, indices
= None):
143 if not indices
or len(indices
) == 1:
144 self
[:] = self
._sort
_dsu
(self
)
146 items
= self
._sort
_dsu
(indices
)
147 for i
,x
in enumerate(items
):
148 list.__setitem
__(self
, indices
[i
], items
[i
])
158 def get_feed_with_id(self
, id):
159 for f
in self
.flatten_list():
164 def flatten_list(self
, ob
=None):
169 if isinstance(o
, list):
170 l
= l
+ self
.flatten_list(o
)
175 feedlist_instance
= None
178 global feedlist_instance
179 if feedlist_instance
is None:
180 feedlist_instance
= FeedList()
181 return feedlist_instance
183 class Feed(gobject
.GObject
):
184 "A Feed object stores information set by user about a RSS feed."
190 __slots__
= ('_title', '_location', '_username', '_password', '_parsed',
191 '__save_fields', '_items', '_slots',
192 '_id', '_channel_description',
193 '_channel_title', '_channel_link', '_channel_copyright',
194 'channel_lbd', 'channel_editor', 'channel_webmaster',
195 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
196 '_items_stored', '_poll_freq', '_last_poll', '_n_items_unread')
198 __save_fields
= (('_title', ""), ('_location', ""), ('_username', ""),
199 ('_password', ""), ('_id', ""),
200 ('_channel_description', ""), ('_channel_title', ""),
201 ('_channel_link', ""), ('_channel_copyright', ""),
202 ('channel_creator', ""), ('_error', None),
203 ('_items_stored', DEFAULT
),
204 ('_poll_freq', DEFAULT
),
206 ('_n_items_unread', 0))
209 'info-updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
210 'error-occurred' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
211 'process-status-changed' : (gobject
.SIGNAL_RUN_LAST
,
212 gobject
.TYPE_NONE
, ()),
213 'poll-done' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
214 'items-updated' :(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
215 (gobject
.TYPE_PYOBJECT
,)),
216 'items-read' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
217 (gobject
.TYPE_PYOBJECT
,)),
218 'items-deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
219 (gobject
.TYPE_PYOBJECT
,)),
220 'item-order-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,())
224 # use one of the factory functions below instead of this directly
225 def __init__(self
, title
="", location
="", username
="", password
=""):
226 gobject
.GObject
.__init
__(self
)
228 self
._channel
_description
= ""
229 self
._channel
_title
= ""
230 self
._channel
_link
= ""
231 self
._channel
_copyright
= ""
232 self
.channel_lbd
= None
233 self
.channel_editor
= ""
234 self
.channel_webmaster
= ""
235 self
.channel_creator
= ""
236 self
._location
= location
237 self
._username
= username
238 self
._password
= password
241 self
._process
_status
= self
.STATUS_IDLE
242 self
.router
= FeedDataRouter
.FeedDataRouter(self
)
244 self
._items
_stored
= Feed
.DEFAULT
245 self
._poll
_freq
= Feed
.DEFAULT
247 self
._n
_items
_unread
= 0
249 self
._items
_loaded
= False
253 return "Feed '%s' from %s" % (self
._title
, self
._location
)
261 doc
= "A ParsedSummary object generated from the summary file"
264 def fset(self
, parsed
):
265 self
._parsed
= parsed
266 return property(**locals())
270 doc
= "The title of this Feed (as defined by user)"
276 def fset(self
, title
):
277 if self
._title
!= title
:
279 self
.emit('info-updated')
280 return property(**locals())
284 doc
= "A tuple of location, username, password"
286 return (self
._location
, self
._username
, self
._password
)
287 def fset(self
, (location
,username
,password
)):
288 self
._location
= location
289 self
._username
= username
290 self
._password
= password
291 self
.emit('info-updated')
292 return property(**locals())
298 return self
._location
299 def fset(self
, location
):
300 if self
._location
!= location
:
301 self
._location
= location
302 self
.emit('info-updated')
303 return property(**locals())
310 if self
._channel
_title
:
311 text
= self
._channel
_title
314 changed
= self
._channel
_title
!= t
315 self
._channel
_title
= t
317 self
.emit('info-updated')
318 return property(**locals())
321 def channel_description():
325 if self
._channel
_description
:
326 text
= self
._channel
_description
329 changed
= self
._channel
_description
!= t
330 self
._channel
_description
= t
332 self
.emit('info-updated')
333 return property(**locals())
339 return self
._channel
_link
341 changed
= self
._channel
_link
!= t
342 self
._channel
_link
= t
344 self
.emit('info-updated')
345 return property(**locals())
348 def channel_copyright():
351 return self
._channel
_copyright
353 changed
= self
._channel
_copyright
!= t
354 self
._channel
_copyright
= t
356 self
.emit('info-updated')
357 return property(**locals())
360 def number_of_items_stored():
363 return self
._items
_stored
364 def fset(self
, num
=None):
365 if self
._items
_stored
!= num
:
366 self
._items
_stored
= num
367 return property(**locals())
370 def poll_frequency():
373 return self
._poll
_freq
374 def fset(self
, freq
):
375 if self
._poll
_freq
!= freq
:
376 self
._poll
_freq
= freq
377 return property(**locals())
383 return self
._last
_poll
384 def fset(self
, time
):
385 if self
._last
_poll
!= time
:
386 self
._last
_poll
= time
387 return property(**locals())
390 def n_items_unread():
393 return self
._n
_items
_unread
395 if self
._n
_items
_unread
!= n
:
396 self
._n
_items
_unread
= n
397 return property(**locals())
404 def fset(self
, error
):
405 if self
._error
!= error
:
407 self
.emit('error-occurred')
408 return property(**locals())
411 def process_status():
414 return self
._process
_status
415 def fset(self
, status
):
416 if status
!= self
._process
_status
:
417 self
._process
_status
= status
418 self
.emit('process_status_changed')
419 return property(**locals())
426 def fset(self
, parent
):
427 self
._parent
= parent
428 return property(**locals())
431 def next_refresh(self
):
432 """ return the feed's next refresh (time)"""
434 if self
._poll
_freq
== self
.DEFAULT
:
435 increment
= Config
.get_instance().poll_frequency
437 increment
= self
._poll
_freq
439 nr
= self
.last_poll
+ increment
444 for f
, default
in self
.__save
_fields
:
445 fl
[f
] = self
.__getattribute
__(f
)
448 def undump(self
, dict):
449 for f
, default
in self
.__save
_fields
:
450 self
.__setattr
__(f
, dict.get(f
, default
))
454 self
.emit('poll-done')
457 def _sort(self
, items
):
459 Sorts the given items according to the sort order
461 items
= sorted(items
, cmp=self
._cmp
)
463 #if self._items.sort_order:
467 def _cmp(self
, a
, b
):
469 Comparator method to compare items based on the item's pub_date attribute
471 If an item doesn't have a pub_date, it uses title and prioritizes the
475 return cmp(a
.pub_date
, b
.pub_date
)
476 except AttributeError:
477 return locale
.strcoll(a
.title
, b
.title
) and not a
.seen
or not b
.seen
479 def add_items(self
, items
):
480 config
= Config
.get_instance()
481 cutpoint
= self
.number_of_items_stored
482 if cutpoint
== Feed
.DEFAULT
:
483 cutpoint
= config
.number_of_items_stored
484 items
= set(sorted(items
, cmp=self
._cmp
))
485 maxid
= reduce(max, [item
.id for item
in self
._items
])
491 self
._items
= self
._items
.union(items
)
492 self
.restore_items(self
._items
)
493 self
.emit('items-updated', items
)
495 def restore_items(self
, items
):
498 config
= Config
.get_instance()
499 cutpoint
= self
.number_of_items_stored
500 if cutpoint
== Feed
.DEFAULT
:
501 cutpoint
= config
.number_of_items_stored
502 items
= self
._sort
(items
)
503 for idx
, item
in enumerate(items
):
505 if item
.sticky
or not item
.seen
or idx
<= cutpoint
:
506 item
.connect('read', self
.item_marked_read
)
507 # forward sticky signal? XXX
508 self
._items
.add(item
)
509 num_unread
+= item
.seen
and 1 or 0
513 olditems
.append(item
)
514 self
.n_items_unread
= num_unread
515 #self.number_of_items_stored = len(self.items)
517 self
.emit('items-deleted', olditems
)
520 def item_marked_read(self
, item
):
521 self
.n_items_unread
+= item
.seen
and -1 or 1
523 def delete_all_items(self
):
524 self
.emit('items-deleted', self
._items
)
531 def number_of_unread(self
):
532 return self
._n
_items
_unread
535 def number_of_items(self
):
536 return len(self
._items
)
538 def mark_all_items_as_read(self
):
539 unread
= [item
for item
in self
._items
if not item
.seen
]
540 self
.emit('items-read', unread
)
541 self
.n_items_unread
= 0
543 def load_contents(self
):
544 if self
._items
_loaded
:
546 itemstore
= ItemStore
.get_instance()
547 items
= itemstore
.read_feed_items(self
)
549 self
.restore_items(items
)
550 self
._items
_loaded
= True
551 return self
._items
_loaded
553 def unload_contents(self
):
555 Unloads the items by disconnecting the signals and reinitialising the
558 TODO: unload seems to lose some circular references. garbage collector
559 will find them, though, so maybe it's not a problem.
561 if not self
._items
_loaded
:
564 self
._items
_loaded
= False
567 def create_new_feed(klass
, title
, location
="", username
="", password
=""):
570 f
._location
= location
571 f
._id
= Config
.get_instance().next_feed_id_seq()
572 f
._username
= username
573 f
._password
= password
577 def create_empty_feed(klass
):