products/CPSRSS

view browser/channels.py @ 286:de8a31cea808

Added a protection against missing parameters that cause ERROR messages in the logs.
author M.-A. Darche <ma.darche@aful.org>
date Thu, 06 Oct 2011 09:07:25 +0200
parents 903be8003573
children eb4d70018698
line source
1 # (C) Copyright 2010 CPS-CMS Community <http://cps-cms.org/>
2 # Authors:
3 # G. Racinet <georges@racinet.fr>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 from copy import deepcopy
19 import logging
20 import operator
22 from Globals import InitializeClass
23 from AccessControl import ClassSecurityInfo
24 from Acquisition import aq_inner
26 from Products.CMFCore.utils import getToolByName
27 from Products.CMFCore.permissions import View
28 from Products.CPSonFive.browser import AqSafeBrowserView
30 from Products.CPSUtil.id import generateId
32 from Products.CPSRSS.RSSChannel import RSSChannel
33 from Products.CPSRSS.RSSChannelContainer import RSSChannelContainer
34 from Products.CPSRSS.RSSChannelContainer import addRSSChannelContainer
36 from Products.CPSRSS.interfaces import IRSSChannelContainer
38 from Products.CPSUtil.text import summarize
40 logger = logging.getLogger(__name__)
42 DEFAULT_RSS_ITEM_DISPLAY = 'cpsportlet_rssitem_display'
44 class ManageChannels(AqSafeBrowserView):
45 """This view class serves as a view mostly for local channels.
47 It does the container lookup, and provides interface to the container.
48 It also maintains the list of channels to be rendered on context proxy,
49 and provides the rendering logic.
51 Some options make it possible for the regular portlet to call it as well,
52 by forcing the container.
53 This is still experimental, and will likely evolve in something
54 more uniform, in which the portlet can also render local channels.
55 """
57 security = ClassSecurityInfo()
59 def __init__(self, *args, **kwargs):
60 AqSafeBrowserView.__init__(self, *args, **kwargs)
61 self.aqSafeSet('container', self.lookupContainer())
63 def lookupContainer(self, cont_id=None):
64 """Lookup the relevant container and set it up on self."""
65 folder = self.context.aq_inner
67 if cont_id is not None:
68 try:
69 cont = folder[cont_id]
70 except KeyError:
71 return None
72 if not IRSSChannelContainer.providedBy(cont):
73 return None
75 # coding style that works if objectValues turns out to be a generator
76 for cont in folder.objectValues([RSSChannelContainer.meta_type]):
77 return cont
79 def hasContainer(self):
80 return self.aqSafeGet('container') is not None
82 def delChannels(self, chan_ids):
83 chan_ids = self.request.form.get('chan_ids')
84 if chan_ids is None:
85 raise BadRequest("Missing channel ids to remove.")
86 if isinstance(chan_ids, basestring):
87 chan_ids = [chan_ids]
88 cont = self.aqSafeGet('container')
89 cont.manage_delObjects(chan_ids)
90 self.redirectManageChannels()
92 def channels(self, with_activation=True):
93 cont = self.aqSafeGet('container')
94 if cont is None:
95 return ()
96 proxy = self.context
97 channels = cont.objectValues([RSSChannel.meta_type])
98 if not with_activation:
99 return channels
101 dm = proxy.getContent().getDataModel(proxy=proxy)
102 activated = dm.get('channels', ())
103 return tuple(dict(channel=chan, activated=chan.getId() in activated)
104 for chan in channels)
106 # traditional security declaration is necessary for browser:view,
107 # and further in current Five 1.3.2 takes precedence over zcml
108 # Actual protection must be declared, docstring created on the fly if
109 # missing
110 security.declareProtected(View, 'rssItems')
111 def rssItems(self, cont_id=None, **kw):
112 # straight adaptation from old skins script
113 # now that proof-of-concept works, should be split
114 if cont_id is None:
115 cont = self.aqSafeGet('container')
116 else:
117 cont = self.context[cont_id]
119 if cont is None:
120 return ()
122 logger.info("RSS channels from %r", cont)
123 first_item = int(kw.get('first_item', 1))
124 max_items = int(kw.get('max_items', 0))
125 max_words = int(kw.get('max_words', 0))
127 data_items = []
128 channels_ids = kw.get('channels', [])
129 for channel_id in channels_ids:
130 if not cont.hasObject(channel_id):
131 continue
132 channel = cont[channel_id]
133 if channel is None:
134 continue
135 data = channel.getData(max_items + first_item - 1)
136 lines = deepcopy(data['lines']) # RSSChan did a simple copy
137 for line in lines:
138 # lines will be shuffled around (timely sort), so channel
139 # dependent display options have to be copied
140 line['newWindow'] = data['newWindow']
141 data_items += lines
142 if first_item > 1:
143 data_items = data_items[first_item - 1:]
145 # If there is more than 1 channel we need to sort the rss items to
146 # only keep the most recent ones, up to max_items.
147 if len(channels_ids) > 1:
148 # NOTE: One should replace "modified" with "updated" if switching
149 # to a newer version of Feed Parser
150 # http://feedparser.org/docs/date-parsing.html
151 # Relying on the 'modified_parsed' item for the sorting.
152 data_items.sort(key=operator.itemgetter('modified_parsed'),
153 reverse=True)
154 data_items = data_items[:max_items]
156 render_method = kw.get('render_method') or DEFAULT_RSS_ITEM_DISPLAY
157 render_method = getattr(aq_inner(self.context), render_method, None)
159 order = 0
160 for item in data_items:
161 description = item['description']
162 modified = item['modified']
163 author = item['author']
164 if not author:
165 author = 'unknown'
167 # Item rendering and display
168 rendered = ''
170 # render the item using a custom display method (.zpt, .py, .dtml)
171 if render_method is not None:
172 item['summary'] = summarize(description, max_words)
173 kw.update({'item': item,
174 'order': order,
175 })
176 rendered = apply(render_method, (), kw)
178 # this information is used by custom templates that call
179 # getRSSItems() directly. GR TODO: who are these ?
180 data_items[order].update(
181 {'description': description,
182 'rendered': rendered,
183 'metadata':
184 {'creator': author,
185 'contributor': author,
186 'date': modified,
187 'issued': modified,
188 'created': modified,
189 },
190 })
191 order += 1
193 return data_items
195 def setActivated(self):
196 """Set the list of activated channels."""
197 activated = self.request.form.get('activated', ())
198 proxy = self.context
199 doc = proxy.getEditableContent()
200 dm = doc.getDataModel(proxy=proxy)
201 if not 'channels' in dm:
202 raise RuntimeError(
203 "Document type %r lacks fields for channels management",
204 doc.portal_type)
206 available = set(chan.getId()
207 for chan in self.channels(with_activation=False))
208 dm['channels'] = [cid for cid in activated if cid in available]
209 dm._commit()
210 self.redirectManageChannels()
212 def redirectManageChannels(self):
213 self.request.RESPONSE.redirect('/'.join((
214 self.context.absolute_url_path(), 'manage_channels.html')))
216 def refresh(self):
217 cont = self.aqSafeGet('container')
218 if cont is not None:
219 cont.refresh()
220 self.redirectManageChannels()
222 def addChannel(self, url=None):
223 """Create a channel from explicit url or from request form.
225 All other properties are retrieved from the feed itself.
226 """
228 if url is None:
229 # taking from request
230 url = self.request.form['channel_url']
232 cont = self.aqSafeGet('container')
233 if cont is None:
234 cont = addRSSChannelContainer(self.context)
235 self.aqSafeSet('container', cont)
236 channel = RSSChannel('channel', url).__of__(cont)
237 d = channel.getData() # might be quite empty if feed has problems
239 title = d.get('title', '')
240 description = d.get('description', '')
241 cid = generateId(title, container=cont)
242 channel._setId(cid)
243 channel.manage_changeProperties(title=title, description=description)
245 cont._setObject(cid, channel)
246 self.redirectManageChannels()
248 InitializeClass(ManageChannels)