1 | # Copyright 2006 by the LeMill Team (see AUTHORS) |
---|
2 | # |
---|
3 | # This file is part of LeMill. |
---|
4 | # |
---|
5 | # LeMill 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 | # LeMill 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 LeMill; if not, write to the Free Software |
---|
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
---|
18 | |
---|
19 | from config import PROJECTNAME, SHOW_METADATA_FIELDS, TEMPLATES, to_unicode, MIMETYPE_WHITELIST, CONTENT_TYPES, FEATURED_TYPES |
---|
20 | from Products.Archetypes.public import * |
---|
21 | from Products.ATContentTypes.content.folder import ATFolder, ATFolderSchema |
---|
22 | from Products.Archetypes.public import registerType |
---|
23 | from Products.CMFCore.utils import getToolByName |
---|
24 | from ZPublisher.HTTPRequest import FileUpload |
---|
25 | from DocumentTemplate import sequence |
---|
26 | from random import sample |
---|
27 | from DateTime import DateTime |
---|
28 | import re, datetime |
---|
29 | from AccessControl import ClassSecurityInfo |
---|
30 | |
---|
31 | |
---|
32 | communityschema= ATFolderSchema + Schema(( |
---|
33 | LinesField('collaboration_proposals', |
---|
34 | default= [], |
---|
35 | widget = LinesWidget( |
---|
36 | visible = {'view':'invisible', 'edit':'invisible'}, |
---|
37 | ) |
---|
38 | ) |
---|
39 | |
---|
40 | )) |
---|
41 | |
---|
42 | |
---|
43 | |
---|
44 | class SectionFolder(ATFolder): |
---|
45 | """Section Folder""" |
---|
46 | |
---|
47 | archetype_name = "Section Folder" |
---|
48 | meta_type = "Section Folder" |
---|
49 | security = ClassSecurityInfo() |
---|
50 | |
---|
51 | # Override initializeArchetype to turn on syndication by default |
---|
52 | def initializeArchetype(self, **kwargs): |
---|
53 | ret_val = ATFolder.initializeArchetype(self, **kwargs) |
---|
54 | # Enable topic syndication by default |
---|
55 | syn_tool = getToolByName(self, 'portal_syndication', None) |
---|
56 | if syn_tool is not None: |
---|
57 | if syn_tool.isSiteSyndicationAllowed(): |
---|
58 | try: |
---|
59 | syn_tool.enableSyndication(self) |
---|
60 | except: # might get 'Syndication Information Exists' |
---|
61 | pass |
---|
62 | return ret_val |
---|
63 | |
---|
64 | def _lemill_invokeFactory(self, container, meta_type, id=None, title=''): |
---|
65 | """ add new object, edit it's title and invoke _renameAfterCreation """ |
---|
66 | if id is None: |
---|
67 | id = self.generateUniqueId(meta_type) |
---|
68 | new_id = container.invokeFactory(meta_type, id) |
---|
69 | new = getattr(container, new_id) |
---|
70 | new.edit(title = title) |
---|
71 | renamed = new._renameAfterCreation() |
---|
72 | if not renamed: |
---|
73 | renamed = new_id |
---|
74 | obj = getattr(container, renamed) |
---|
75 | return obj |
---|
76 | |
---|
77 | def start_new_version(self, REQUEST, objId): |
---|
78 | """ |
---|
79 | Start a new version of an object. This is done by creating new object and |
---|
80 | then, based on schema, all data is copyied from an object to new object. |
---|
81 | objId - relative URL to object like /activities/demo_activity |
---|
82 | or /activities/remote/<remote_server_address>/activities/demo_activity |
---|
83 | """ |
---|
84 | portal_url = getToolByName(self, 'portal_url') |
---|
85 | base_obj = portal_url.getPortalObject().restrictedTraverse(objId) |
---|
86 | meta_type = base_obj.meta_type |
---|
87 | folder_path = objId.split('/') |
---|
88 | folder_url = '/'.join(folder_path[:-1]) |
---|
89 | folder = portal_url.getPortalObject().restrictedTraverse(folder_url) |
---|
90 | #new_id = folder.invokeFactory(meta_type, id=meta_type.lower()+str(id(base_obj))) |
---|
91 | #new = getattr(folder, new_id) |
---|
92 | #new.edit(title=base_obj.Title()) |
---|
93 | new = self._lemill_invokeFactory(folder, meta_type, id=meta_type.lower()+str(id(base_obj)), title=base_obj.Title()) |
---|
94 | for k in base_obj.schema.keys(): |
---|
95 | # list all fields here that shouldn't be copyied to new object |
---|
96 | if k in ['id', 'effectiveDate', 'expirationDate', 'creation_date', 'modification_date']: |
---|
97 | continue |
---|
98 | old_accessor = base_obj.schema[k].getEditAccessor(base_obj) |
---|
99 | new_mutator = new.schema[k].getMutator(new) |
---|
100 | if k == 'parentVersion': |
---|
101 | new_mutator(base_obj.absolute_url()) |
---|
102 | continue |
---|
103 | val = old_accessor() |
---|
104 | new_mutator(val) |
---|
105 | mess = 'New %s has been created for you!' % meta_type.lower() |
---|
106 | REQUEST.set('portal_status_message', mess) |
---|
107 | return new.base_edit(portal_status_message=mess) |
---|
108 | |
---|
109 | def getMetadaFieldsToShowInEditor(self, object): |
---|
110 | """ gets fields which are shown in metadata edit area """ |
---|
111 | type = object.meta_type |
---|
112 | shownFieldsList = SHOW_METADATA_FIELDS[type] |
---|
113 | shownFields = [] |
---|
114 | fields = object.Schemata()['metadata'].fields() |
---|
115 | # At this point, the method can return a list of all metadata |
---|
116 | if 'all' in shownFieldsList: |
---|
117 | return fields |
---|
118 | else: |
---|
119 | for field in fields: |
---|
120 | if field.getName() in shownFieldsList: |
---|
121 | shownFields.append(field) |
---|
122 | return shownFields |
---|
123 | |
---|
124 | def amIManager(self): |
---|
125 | """Check whether I'm a manager.""" |
---|
126 | return 'Manager' in self.portal_membership.getAuthenticatedMember().getRoles() |
---|
127 | |
---|
128 | def whoami(self): |
---|
129 | return self.portal_membership.getAuthenticatedMember().getId() |
---|
130 | |
---|
131 | def getMember(self,uname=None): |
---|
132 | if not uname: |
---|
133 | uname=self.whoami() |
---|
134 | try: |
---|
135 | return getattr(self.community,uname) |
---|
136 | except AttributeError: |
---|
137 | return None |
---|
138 | |
---|
139 | def getSamples(self, search_results): |
---|
140 | """ Pick three random objects from search results that have non-default images to display in folders front pages. """ |
---|
141 | pic_results = [x for x in search_results if x.meta_type in FEATURED_TYPES and x.getHasCoverImage and x.review_state=='public' and x.Title] |
---|
142 | n=min(3,len(pic_results)) |
---|
143 | samples=sample(pic_results,n) |
---|
144 | return samples |
---|
145 | |
---|
146 | def makeSortable_title(self, title): |
---|
147 | """Helper method partially copied from CMFPlone.CatalogTool """ |
---|
148 | putils= getToolByName(self, 'plone_utils') |
---|
149 | def_charset = putils.getSiteEncoding() |
---|
150 | num_sort_regex = re.compile('\d+') |
---|
151 | sortabletitle = title.lower().strip() |
---|
152 | # Replace numbers with zero filled numbers |
---|
153 | sortabletitle = num_sort_regex.sub(lambda matchobj: matchobj.group().zfill(8), sortabletitle) |
---|
154 | # Truncate to prevent bloat |
---|
155 | for charset in [def_charset, 'latin-1', 'utf-8']: |
---|
156 | try: |
---|
157 | sortabletitle = unicode(sortabletitle, charset)[:30] |
---|
158 | sortabletitle = sortabletitle.encode(def_charset or 'utf-8') |
---|
159 | break |
---|
160 | except UnicodeError: |
---|
161 | pass |
---|
162 | except TypeError: |
---|
163 | # If we get a TypeError if we already have a unicode string |
---|
164 | sortabletitle = sortabletitle[:30] |
---|
165 | break |
---|
166 | return sortabletitle |
---|
167 | |
---|
168 | |
---|
169 | def getTopResults(self, search_results, index_type): |
---|
170 | """ Should return top three populated subgroups of search results in given index """ |
---|
171 | pc = getToolByName(self, 'portal_catalog') |
---|
172 | if index_type in pc.indexes() and index_type in pc.schema(): |
---|
173 | uniques = pc.uniqueValuesFor(index_type) |
---|
174 | if uniques == (): |
---|
175 | return [] |
---|
176 | hits={} |
---|
177 | for a in uniques: |
---|
178 | hits[a]=0 |
---|
179 | for counter in search_results: |
---|
180 | if counter.review_state!='hidden': |
---|
181 | values=getattr(counter, index_type) |
---|
182 | if isinstance(values,str): |
---|
183 | hits[values]=hits[values]+1 |
---|
184 | else: |
---|
185 | try: |
---|
186 | for a in values: |
---|
187 | hits[a]=hits[a]+1 |
---|
188 | except TypeError: |
---|
189 | pass # something wrong with data - let's continue without it |
---|
190 | topthree= [(x[1],x[0]) for x in hits.items() if x[1]>0] |
---|
191 | topthree.sort() |
---|
192 | topthree.reverse() |
---|
193 | topthree= topthree[:min(3,len(topthree))] |
---|
194 | topthree=[x[1] for x in topthree] |
---|
195 | |
---|
196 | return topthree |
---|
197 | else: |
---|
198 | return [] |
---|
199 | |
---|
200 | def getUniques(self, search_results, index_type='Date'): |
---|
201 | """ Should return unique values of search results in given index, results are list of index-metadata-like tuples in alphabetical order """ |
---|
202 | pc = getToolByName(self, 'portal_catalog') |
---|
203 | uniquetuplelist=[] |
---|
204 | maxcount=0 |
---|
205 | def adjust(i): |
---|
206 | # helper method to adjust hit count of this tag to relative size (1,...,8) |
---|
207 | (a,b,c,d,e)=i |
---|
208 | if b>1: |
---|
209 | b=int((8*b)/maxcount) |
---|
210 | i=(a,b,c,d,e) |
---|
211 | return i |
---|
212 | |
---|
213 | # -- Step 1 -- : Make list of unique values for this index. For members and groups this is time to collect their urls and title. |
---|
214 | if index_type in pc.indexes() and index_type!='getSortable_nicename' and index_type!='sortable_title': |
---|
215 | uniques = pc.uniqueValuesFor(index_type) |
---|
216 | elif index_type=='getSortable_nicename': |
---|
217 | people = pc.searchResults(portal_type='MemberFolder') |
---|
218 | uniques = [x.id for x in people] |
---|
219 | elif index_type=='sortable_title': |
---|
220 | groups = pc.searchResults(portal_type='GroupBlog') |
---|
221 | uniques = [x.id for x in groups] |
---|
222 | if uniques == (): |
---|
223 | return [] |
---|
224 | hits={} |
---|
225 | |
---|
226 | # -- Step 2 -- : Use list to make a dictionary. Values of list are keys. |
---|
227 | for a in uniques: |
---|
228 | hits[a]=(0,'','','') # value : (hits, objectURL, sort_title, title) |
---|
229 | |
---|
230 | # -- Step 3 -- : Go through the search_results and every time a certain key is found from selected index, add a hit to counter under that key. With people and group names the counter gets hits from different sources. |
---|
231 | if index_type not in pc.schema() or index_type=="getSortable_nicename": |
---|
232 | |
---|
233 | if index_type=="sortable_title": |
---|
234 | # Just get activity score from group metadata |
---|
235 | for group in groups: |
---|
236 | hits[group.id]=(max(1, group.getActivity_score), group.getURL(), self.makeSortable_title(group.Title), group.Title) |
---|
237 | |
---|
238 | elif index_type=="getSortable_nicename": |
---|
239 | # Just get activity score from member metadata |
---|
240 | for member in people: |
---|
241 | hits[member.id]=(max(1, member.getActivity_score), member.getURL(), member.getSortable_nicename, member.getNicename) |
---|
242 | else: |
---|
243 | return [] |
---|
244 | else: |
---|
245 | # Vanilla hits counter |
---|
246 | spent_urls=[] |
---|
247 | for counter in search_results: |
---|
248 | c_url=counter.getURL() |
---|
249 | if counter.review_state!='hidden' and not c_url in spent_urls: |
---|
250 | values=getattr(counter, index_type) |
---|
251 | if values!=None: |
---|
252 | if type(values)==str or type(values)==int: |
---|
253 | hits[values]=(hits[values][0]+1, c_url, values, values) |
---|
254 | else: |
---|
255 | for a in values: |
---|
256 | hits[a]=(hits[a][0]+1, c_url, a, a) |
---|
257 | spent_urls.append(c_url) |
---|
258 | # OK, the story so far: hits = dictionary, where keys are the distinct values of current index, or memberid:s |
---|
259 | # values of this dictionary are tuples, where: |
---|
260 | # value[0]= n of objects that have this value, |
---|
261 | # value[1]= url-of-object that has this value (sensible only if there is one object with this value, like names for users), |
---|
262 | # value[2]= title, nicer representation of key's name (title for group names and nicename for member names) |
---|
263 | # value[3]= another_title, because sortable_nicename already takes the title place for members. ugly... |
---|
264 | |
---|
265 | # -- Step 4 -- : Build a list from previous dictionary. Objects in list are tuples. Order list alphabetically. |
---|
266 | # This dictionary should be ordered alphabetically by title. |
---|
267 | uniquetuplelist=[(x[1][2],x[1][0],x[1][1],x[0],x[1][3]) for x in hits.items() if x[1][0]>0] |
---|
268 | # uniquetuplelist now contains dictionary reordered: (sort_title, count, url, indexvalue, title) |
---|
269 | if uniquetuplelist==[]: |
---|
270 | return uniquetuplelist |
---|
271 | maxcount=max(map(lambda x: x[1], uniquetuplelist)) |
---|
272 | uniquetuplelist=map(adjust, uniquetuplelist) |
---|
273 | uniquetuplelist.sort() |
---|
274 | return uniquetuplelist |
---|
275 | |
---|
276 | def getTagURL(self,context,category,value): |
---|
277 | value=to_unicode(value) |
---|
278 | return u"%s?%s=%s" % (context.absolute_url(),category,value) |
---|
279 | |
---|
280 | def js_queryForPieces(self, keyword): |
---|
281 | """ javascript is querying for pieces that are images """ |
---|
282 | result = [] |
---|
283 | stool = getToolByName(self, 'lemill_search') |
---|
284 | q = {'SearchableText': keyword, |
---|
285 | 'portal_type': ['Piece', ] |
---|
286 | } |
---|
287 | q_results = stool.local_search(q) |
---|
288 | for r in q_results: |
---|
289 | if not r.getObject().isImage(): continue |
---|
290 | tmp = [r.getObject().UID(), r.getId, to_unicode(r.Title).encode('iso-8859-15')] |
---|
291 | result.append(tmp) |
---|
292 | return str(result) |
---|
293 | |
---|
294 | security.declarePublic('public_getSortCriterion') |
---|
295 | def public_getSortCriterion(self,topic): |
---|
296 | """ Topics have this useful getSortCriterion -method, but it's available only if allowed to change content.""" |
---|
297 | if topic.hasSortCriterion(): |
---|
298 | return topic.getSortCriterion().field |
---|
299 | else: |
---|
300 | return False |
---|
301 | |
---|
302 | class ContentFolder(SectionFolder): |
---|
303 | |
---|
304 | archetype_name = "Content Folder" |
---|
305 | meta_type = "Content Folder" |
---|
306 | |
---|
307 | allowed_content_types = CONTENT_TYPES +('Topic',) |
---|
308 | default_view = ('lemill_content_view') |
---|
309 | filter_content_types = True |
---|
310 | |
---|
311 | def askPublish(self, REQUEST): |
---|
312 | """ask the user for publish the content, in the add_content_wizard""" |
---|
313 | obj_id = REQUEST.get('id') |
---|
314 | publish = REQUEST.get('publish') |
---|
315 | keep = REQUEST.get('keep') |
---|
316 | obj = getattr(self, obj_id) |
---|
317 | if keep is None: |
---|
318 | obj.content_status_modify(workflow_action='publish') |
---|
319 | return REQUEST.RESPONSE.redirect(obj.absolute_url_path()) |
---|
320 | if publish is None: |
---|
321 | return REQUEST.RESPONSE.redirect(obj.absolute_url_path()) |
---|
322 | |
---|
323 | def uploadIt(self, REQUEST): |
---|
324 | """ gets file from upload and makes new object. |
---|
325 | also does some content-type whitelist checking here. |
---|
326 | """ |
---|
327 | file = REQUEST.get('file') |
---|
328 | user_title = REQUEST.get('user_title') |
---|
329 | user_description = REQUEST.get('user_description') |
---|
330 | content_type = file.headers['Content-Type'] |
---|
331 | type = '' |
---|
332 | if content_type in MIMETYPE_WHITELIST: |
---|
333 | type = 'Piece' |
---|
334 | |
---|
335 | if not type or type != 'Piece': |
---|
336 | return REQUEST.RESPONSE.redirect('lemill_explain_upload_fail') |
---|
337 | |
---|
338 | new = self._lemill_invokeFactory(self, type, id=type+str(id(self)), title=user_title) |
---|
339 | new.edit(description=user_description, file=file.read()) |
---|
340 | if type=='Piece': |
---|
341 | new.edit(language='') |
---|
342 | new_id = new.getId() |
---|
343 | return REQUEST.RESPONSE.redirect(new.absolute_url()+'/piece_edit') |
---|
344 | #return REQUEST.RESPONSE.redirect(self.absolute_url()+'/lemill_check_content?id='+new_id+'&type='+type) |
---|
345 | |
---|
346 | def getTemplates(self): |
---|
347 | return TEMPLATES |
---|
348 | |
---|
349 | def getTemplate(self, template_id): |
---|
350 | return TEMPLATES.get(template_id, None) |
---|
351 | |
---|
352 | class ActivityFolder(SectionFolder): |
---|
353 | |
---|
354 | archetype_name = "Activity Folder" |
---|
355 | meta_type = "Activity Folder" |
---|
356 | |
---|
357 | allowed_content_types = ('Activity','KB', 'Topic') |
---|
358 | default_view = ('lemill_activities_view') |
---|
359 | filter_content_types = True |
---|
360 | |
---|
361 | class ToolFolder(SectionFolder): |
---|
362 | |
---|
363 | archetype_name = "Tool Folder" |
---|
364 | meta_type = "Tool Folder" |
---|
365 | |
---|
366 | allowed_content_types = ('Tool', 'Topic') |
---|
367 | default_view = ('lemill_tools_view') |
---|
368 | filter_content_types = True |
---|
369 | |
---|
370 | class CommunityFolder(SectionFolder): |
---|
371 | |
---|
372 | archetype_name = "Community Folder" |
---|
373 | meta_type = "Community Folder" |
---|
374 | |
---|
375 | allowed_content_types = ('Topic') |
---|
376 | default_view = ('lemill_community_view') |
---|
377 | filter_content_types = True |
---|
378 | |
---|
379 | schema=communityschema |
---|
380 | |
---|
381 | def my_page(self): |
---|
382 | """ Checks if user has MemberFolder and creates one if not. Returns the folder url.""" |
---|
383 | mtool = getToolByName(self, "portal_membership") |
---|
384 | member=mtool.getAuthenticatedMember() |
---|
385 | if member.getHomeFolder()==None: |
---|
386 | member.createMemberarea() |
---|
387 | folder=member.getHomeFolder() |
---|
388 | return folder.absolute_url() |
---|
389 | |
---|
390 | def getCollaboration_proposals(self): |
---|
391 | """ Because I prefer lists, not tuples """ |
---|
392 | return list(self.getField('collaboration_proposals').get(self)) |
---|
393 | |
---|
394 | def addCollaboration_proposal(self, obj_uid): |
---|
395 | """ Collaboration proposals are stored as list of UID:s, because then we don't have to care about group path when finding them """ |
---|
396 | uid_cat = getToolByName(self, "uid_catalog") |
---|
397 | |
---|
398 | def post_date(uid): |
---|
399 | post=uid_cat(UID=uid) |
---|
400 | post=post[0].getObject() |
---|
401 | return post.creation_date |
---|
402 | |
---|
403 | current_date = DateTime() |
---|
404 | cp= self.getCollaboration_proposals() |
---|
405 | cp=[obj_uid]+cp |
---|
406 | # pop out old collaboration proposals from tail of the list |
---|
407 | while post_date(cp[-1])< (current_date-31): |
---|
408 | cp.pop() |
---|
409 | cp=cp[:100] |
---|
410 | cp_field=self.getField('collaboration_proposals') |
---|
411 | cp_field.set(self, cp) |
---|
412 | |
---|
413 | def removeCollaboration_proposal(self,obj_uid): |
---|
414 | cp= self.getCollaboration_proposals() |
---|
415 | if obj_uid in cp: |
---|
416 | cp.remove(obj_uid) |
---|
417 | cp_field=self.getField('collaboration_proposals') |
---|
418 | cp_field.set(self, cp) |
---|
419 | |
---|
420 | def mergeLatestPostsInMyGroups(self): |
---|
421 | mtool = getToolByName(self, "portal_membership") |
---|
422 | gtool = getToolByName(self, "portal_groups") |
---|
423 | member=mtool.getAuthenticatedMember() |
---|
424 | memberid=member.getId() |
---|
425 | glist = self.lemill_usertool.getGroupsList(memberid); |
---|
426 | recents=[] |
---|
427 | for group in glist: |
---|
428 | gname= group.getGroupName() |
---|
429 | garea= gtool.getGroupareaFolder(gname) |
---|
430 | grecent= garea.getRecent_posts() |
---|
431 | for postid in grecent: |
---|
432 | try: |
---|
433 | post=garea._getOb(postid) |
---|
434 | recents.append((post.Date,post)) |
---|
435 | except AttributeError: |
---|
436 | # do some cleaning: |
---|
437 | garea.removeRecent_post(postid) |
---|
438 | recents.sort() |
---|
439 | recents = [x[1] for x in recents] |
---|
440 | return recents[:5] |
---|
441 | |
---|
442 | |
---|
443 | |
---|
444 | |
---|
445 | registerType(ContentFolder, PROJECTNAME) |
---|
446 | registerType(ActivityFolder, PROJECTNAME) |
---|
447 | registerType(ToolFolder, PROJECTNAME) |
---|
448 | registerType(CommunityFolder, PROJECTNAME) |
---|