# -*- coding: utf-8 -*- # (c) Copyright 2005, CodeSyntax <http://www.codesyntax.com> # Authors: Mikel Larreategi <mlarreategi@codesyntax.com> # See also LICENSE.txt # Zope modules from Globals import HTMLFile import Globals from AccessControl import ClassSecurityInfo from BTrees.IOBTree import IOBTree from Products.PythonScripts.PythonScript import PythonScript # ZCatalog from Products.ZCatalog import ZCatalog from Products.ZCatalog.CatalogPathAwareness import CatalogAware # Localizer from Products.Localizer.Localizer import Localizer from Products.Localizer.MessageCatalog import MessageCatalog # BTreeFolder2 from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2 # Other stuff import DateTime # Own modules from utils import addDTML from utils import fillMessageCatalog, ok_chars from PingMethodContainer import PingMethodContainer from tinymce_conf import default_configurations manage_addBitakoraForm = HTMLFile('ui/Bitakora_add', globals()) def manage_addBitakora(self, id, title, subtitle, contact_mail, description=u'', REQUEST=None): """ Method called from ZMI to create a new Bitakora """ import Bitakora sq = Bitakora.Bitakora(id, title, subtitle, description, contact_mail) self._setObject(id, sq) sq = getattr(self, id) if REQUEST is not None and REQUEST.get('image', None): image = REQUEST.get('image') ext = image.filename.split('.')[-1] imgid = 'image.%s' % ext sq.manage_addImage(imgid, REQUEST.get('image')) #sq.imageUrl = '%s/%s' % (sq.absolute_url(), imgid) sq.imagename = imgid elif REQUEST is None or not REQUEST.get('image', None): from random import random imgnum = (int(random() * 10) % 3) + 1 file = 'face0%s.gif' % imgnum file_path = Globals.package_home(globals()) f = open(file_path + '/ui/communityTemplates/' + file, 'rb') contents = f.read() f.close() title = '' imgid = 'image.gif' sq.manage_addImage(imgid, contents, title=title) sq.imagename = imgid perms = {} perms['Anonymous'] = ['Add Bitakora Comment'] perms['Blogger'] = ['Manage Bitakora'] for role, perm in perms.items(): sq.manage_role(role_to_manage=role, permissions=perm) sq.manage_addProduct['ZTinyMCE'].manage_addZTinyMCE('TinyMCE', 'TinyMCE') maker = sq.manage_addProduct['ZTinyMCE'].manage_addZTinyConfiguration for config in default_configurations: maker(config['name'], configuration=config['config'], tinymce_instance_path='/'.join(sq.TinyMCE.getPhysicalPath()), title='Example configuration', optimize=True) # Add a MessageCatalog if we are a standalone Bitakora # if not, the BitakoraCommunity MessageCatalog will handle # the messages if self.meta_type == 'BitakoraCommunity': sq._delObject('Localizer') sq._delObject('gettext') self.Catalog.catalog_object(sq, '/'.join(sq.getPhysicalPath())) if REQUEST is not None: return self.manage_main(self, REQUEST) class Bitakora(BTreeFolder2, CatalogAware): """ Bitakora is a new blog product for Zope """ from Post import manage_addPost from utils import send_contact_mail, cleanBody, getCaptchaQuestion meta_type = 'Bitakora' __ac_roles__ = ('Blogger',) security = ClassSecurityInfo() security.setDefaultAccess('allow') security.setPermissionDefault('Manage Bitakora', ('Blogger', 'Manager',)) security.setPermissionDefault('Add Bitakora Comment', ('Anonymous', 'Manager',)) _properties = ( {'id': 'management_page_charset', 'type': 'string', 'mode': 'w'}, {'id': 'CAPTCHA_ENABLED', 'type': 'int'}, ) manage_options = ({'label': 'Contents', 'action': 'manage_main'}, {'label': 'View', 'action': 'index_html'}, {'label': 'Security', 'action': 'manage_access'}, {'label': 'Undo', 'action': 'manage_UndoForm'},) contact = HTMLFile('ui/contact', globals()) archive = HTMLFile('ui/archive', globals()) captcha_control = HTMLFile('ui/captcha_control', globals()) comment_body = HTMLFile('ui/comment_body', globals()) comment_email_template = HTMLFile('ui/comment_email_template', globals()) comment_form = HTMLFile('ui/comment_form', globals()) contact_email_template = HTMLFile('ui/contact_email_template', globals()) entry_body = HTMLFile('ui/entry_body', globals()) entry_preview = HTMLFile('ui/entry_preview', globals()) index_html = HTMLFile('ui/index_html', globals()) localmenu = HTMLFile('ui/localmenu', globals()) admin_header = HTMLFile('ui/admin_header', globals()) about = HTMLFile('ui/about', globals()) contents = HTMLFile('ui/contents', globals()) downloadTXT = HTMLFile('ui/downloadTXT', globals()) downloadXML = HTMLFile('ui/downloadXML', globals()) downloadWordPress = HTMLFile('ui/downloadWordPress', globals()) logged_in = HTMLFile('ui/logged_in', globals()) logged_out = HTMLFile('ui/logged_out', globals()) login_form = HTMLFile('ui/login_form', globals()) posting_html = HTMLFile('ui/posting_html', globals()) recent_comments = HTMLFile('ui/recent_comments', globals()) recent_references = HTMLFile('ui/recent_references', globals()) reference_body = HTMLFile('ui/reference_body', globals()) #standard_error_message = HTMLFile('ui/standard_error_message', globals()) standard_html_footer = HTMLFile('ui/standard_html_footer', globals()) standard_html_header = HTMLFile('ui/standard_html_header', globals()) tag_all_html = HTMLFile('ui/tag_all_html', globals()) tag_html = HTMLFile('ui/tag_html', globals()) user_links = HTMLFile('ui/user_links', globals()) security.declareProtected('Manage Bitakora', 'manage_addPost') security.declareProtected('Manage Bitakora', 'admin') admin = HTMLFile('ui/Post_list', globals()) security.declareProtected('Manage Bitakora', 'post') post = HTMLFile('ui/Post_add', globals()) security.declareProtected('Manage Bitakora', 'prefs') prefs = HTMLFile('ui/Blog_edit', globals()) security.declareProtected('Manage Bitakora', 'sidebar') sidebar = HTMLFile('ui/manage_sidebar', globals()) security.declareProtected('Manage Bitakora', 'template') template = HTMLFile('ui/manage_template', globals()) security.declareProtected('Manage Bitakora', 'comments') comments = HTMLFile('ui/manage_comments', globals()) admin_options = ['admin', 'post', 'sidebar', 'prefs', 'template', 'comments'] security.declarePrivate('__init__') def __init__(self, id, title, subtitle, description, contact_mail): """ Constructor """ BTreeFolder2.__init__(self, id) self.id = id self.title = title self.subtitle = subtitle self.description = description self.contact_mail = contact_mail self.management_page_charset = u'UTF-8' self.sidebar_html = u'' self._setObject('Catalog', ZCatalog.ZCatalog('Catalog', 'Catalog')) self._buildIndexes() self._addMethods() self._setObject('pingback', PingMethodContainer()) self.postcount = 0 self._links = IOBTree() # if 0 not allowed, 1 allowed, 2 allowed but moderated self.comment_allowed = 1 self.reference_allowed = 1 #self.imageUrl = None self.imagename = '' self.CAPTCHA_ENABLED = 1 security.declareProtected('View', 'searchResults') def searchResults(self, **kw): """ Call the Catalog """ return self.Catalog.searchResults(kw) security.declareProtected('View', 'all') def all(self, REQUEST=None, **kw): """ Just a wrapper around call """ return self.Catalog(REQUEST, **kw) security.declareProtected('Manage Bitakora', 'editBlog') def editBlog(self, title, subtitle, contact_mail, description, image=None, REQUEST=None): """ editing method """ self.title = title self.subtitle = subtitle self.contact_mail = contact_mail self.description = description if image.read(): if not image.headers['Content-Type'].lower().startswith('image'): return REQUEST.RESPONSE.redirect('%s/prefs?msg=%s' % (self.blogurl(), 'You tried to upload something that is not a valid picture image. Try with a 65x65 pixel sized jpg, png or gif.')) ext = image.filename.split('.')[-1] imgid = 'image.%s' % ext #original_id = self.imageUrl.split('/')[-1] original_id = self.imagename original_img = self.get(original_id).data self._delObject(original_id) image.seek(0) self.manage_addImage(imgid, image) img = self.get(imgid) if img.height > 65 and img.width > 65: self._delObject(imgid) self.manage_addImage(original_id, original_img) #self.imageUrl = '%s/%s' % (self.absolute_url(), original_id) self.imagename = original_id if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/prefs?msg=%s' % (self.blogurl(), 'Your image is too large. Try with a smaller one')) #self.imageUrl = '%s/%s' % (self.absolute_url(), imgid) self.imagename = imgid if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/prefs?msg=%s' % (self.blogurl(), 'Preferences edited succesfully')) security.declareProtected('Manage Bitakora', 'editCommentPolicy') def editCommentPolicy(self, comment_allowed, REQUEST=None): """ edit comment policy """ self.comment_allowed = comment_allowed self.reference_allowed = comment_allowed if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/comments?msg=%s' % (self.blogurl(), 'Comment policy edited succesfully')) security.declarePrivate('_buildIndexes') def _buildIndexes(self): """ Stuff to create Catalog indexes """ # delete any existing indexes for name in self.Catalog.indexes(): self.Catalog.delIndex(name) # add the default indexes for (name, index_type) in [('meta_type', 'FieldIndex'), ('published', 'FieldIndex'), ('date', 'DateIndex'), ('tags', 'KeywordIndex'), ('yearmonth', 'KeywordIndex')]: self.Catalog.addIndex(name, index_type) security.declarePrivate('_addMethods') def _addMethods(self): """ Just to have all methods adding something extra to the ZMI together """ file_path = Globals.package_home(globals()) localizer = Localizer('Localizer', ('en', 'es', 'eu',)) localizer._v_hook = 1 self._setObject('Localizer', localizer) try: # old MessageCatalog self._setObject('gettext', MessageCatalog('gettext', '', ('en', 'es', 'eu'))) except: # new MessageCatalog self._setObject('gettext', MessageCatalog('gettext', '', 'en', ['en', 'es', 'eu'])) gettext = getattr(self, 'gettext') fillMessageCatalog(gettext) # Add a special tag.py script which makes use of traverse subpath self._setObject('tag', PythonScript('tag')) tag = getattr(self, 'tag') f = open(file_path + '/tag.py') data = f.read() f.close() tag.ZPythonScript_edit('', data) # Add a feed.xml file with the RSS feed addDTML(self, 'feed.xml', '', 'ui/feed.xml') # Add the CSS file self.manage_addFile('blog.css') css = getattr(self, 'blog.css') f = open(file_path + '/ui/blog.css') data = f.read() f.close() css.update_data(data) security.declareProtected('Manage Bitakora', 'manage_delPosts') def manage_delPosts(self, ids=[], REQUEST=None): """ To delete posts from ZMI """ for id in ids: obj = getattr(self, id) obj.deleteAllComments() obj.deleteAllReferences() if self.inCommunity(): # We are in a Bitakora Community, so uncatalog the post there cat = self.getParentNode().get('Catalog', 'None') if cat is not None: cat.uncatalog_object('/'.join(obj.getPhysicalPath())) self._delObject(id) if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/admin?msg=%s' % (self.absolute_url(), 'Post deleted succesfully')) return self.manage_main(self, REQUEST) security.declareProtected('View', 'published_posts') def published_posts(self, size=10, start=None): """ The method for getting 'size' published posts""" if start is None: return self.Catalog.searchResults(meta_type='Post', published=1, date={'query': DateTime.DateTime(), 'range':'max'}, sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog.searchResults(meta_type='Post', published=1, date={'query': DateTime.DateTime(), 'range':'max'}, sort_on='date', sort_order='descending') security.declarePublic('last_post') def last_post(self): """ return the last published post """ posts = self.Catalog.searchResults(meta_type='Post', size=1, sort_on='date', sort_order='descending') if posts: return posts[0].getObject() return None security.declarePublic('View', 'getId') def getId(self): """ get id """ return self.id security.declarePublic('blog_title') def blog_title(self): """ blog title """ return self.title security.declarePublic('blog_subtitle') def blog_subtitle(self): """ blog subtitle """ return self.subtitle security.declarePublic('showDescription') def showDescription(self): return self.description security.declarePublic('show_contact_mail') def show_contact_mail(self): """ blog contact_mail """ return self.contact_mail security.declarePublic('show_sidebar_html') def show_sidebar_html(self): """ blog show_sidebar_html """ return self.sidebar_html security.declarePublic('title_or_id') def title_or_id(self): """ title or id """ return self.blog_title() or self.id security.declarePublic('blogurl') def blogurl(self): """ blog url """ return self.absolute_url() security.declarePublic('getImageUrl') def getImageUrl(self): """ return image's url """ try: self.imagename except: # For backwards compatibility : # image url wasn't created dinamically, but statically when saving # and if blog changed the url, the image wasn't shown. imgid = self.imageUrl.split('/')[-1] if self.get(imgid, None): self.imagename = imgid else: # Ups, something else happened here... imgs = [img for img in self.objectIds() if img.startswith('image')] if len(imgs) == 1: # Well done! We have an image. The images is always # saved with name 'image' and the corresponding extension self.imagename = imgs[0] else: # Two images? No..... Load a new one please... self.imagename = '' return self.blogurl() + '/'+self.imagename security.declarePublic('tagsAndPixels') def tagsAndPixels(self): """ returns a dictionary with (tag, pixelSize) pairs based on number of posts cataloged with the tags """ tags = [tag for tag in self.Catalog.uniqueValuesFor('tags') if tag.strip()] zenbat = {} for tag in tags: tagkop = self.Catalog.searchResults(tags=tag, meta_type='Post', published=1, date={'query': DateTime.DateTime(), 'range': 'max'} ) zenbat[tag] = len(tagkop) maxpx = 2.30 minpx = 0.70 difpx = maxpx - minpx if zenbat.values(): maxnum = max(zenbat.values()) minnum = max(zenbat.values()) else: maxnum = 0 hiz = {} for k, v in zenbat.items(): hiz[k] = float(difpx * v / maxnum) + minpx return hiz security.declarePrivate('blog') def blog(self): return self security.declareProtected('Manage Bitakora', 'addLink') def addLink(self, url, title, REQUEST=None): """ Add a new link in link menu """ try: max = self._links.maxKey() self._links[max + 1] = (url, title) except ValueError: self._links[0] = (url, title) except: min = self._links.minKey() self._links[min - 1] = (url, title) if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/sidebar?msg=%s' % (self.blogurl(), 'Link added succesfully')) security.declareProtected('Manage Bitakora', 'removeLink') def removeLink(self, key=None, REQUEST=None): """ remove link from link menu """ if key is not None: try: del self._links[key] except: if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/sidebar?msg=%s' % (self.blogurl(), 'Error when deleting selected link')) if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/sidebar?msg=%s' % (self.blogurl(), 'Selected link was removed succesfully')) else: if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/sidebar?msg=%s' % (self.blogurl(), 'What?')) security.declarePublic('showLinks') def showLinks(self): """ show links """ def sortByKey(el1, el2): return cmp(el1['key'], el2['key']) elems = [] for key in self._links.keys(): url = self._links.get(key)[0] title = self._links.get(key)[1] elems.append({'url': url, 'title': title, 'key': key}) elems.sort(sortByKey) return elems security.declareProtected('Manage Bitakora', 'save_sidebar_html') def save_sidebar_html(self, html=u'', REQUEST=None): """ save sidebar HTML """ self.sidebar_html = html if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/sidebar?msg=%s' % (self.absolute_url(), 'HTML saved succesfully')) security.declareProtected('Manage Bitakora', 'save_css') def save_css(self, css=u'', REQUEST=None): """ save CSS """ doc = getattr(self, 'blog.css') doc.update_data(css) if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/template?msg=%s' % (self.absolute_url(), 'CSS edited succesfully')) security.declareProtected('Manage Bitakora', 'select_template') def select_template(self, template, REQUEST=None): """ select an existing template """ if not template in self.templates.objectIds(): if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/template?msg=%s' % (self.absolute_url(), 'Selected template does not exist')) else: return obj = getattr(self.templates[template], 'blog.css').data current = getattr(self, 'blog.css') current.update_data(obj) if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/template?msg=%s' % (self.absolute_url(), 'Template changed succesfully')) security.declarePublic('inCommunity') def inCommunity(self): """ Return whether this Bitakora is in a Bitakora community """ if self.getParentNode().meta_type == 'BitakoraCommunity': return 1 return 0 security.declarePublic('showYearMonth') def showYearMonth(self, yearmonth): """ convert 200512 to December 2005 """ months = { '01': 'January', '02': 'February', '03': 'March', '04': 'April', '05': 'May', '06': 'June', '07': 'July', '08': 'August', '09': 'September', '10': 'October', '11': 'November', '12': 'December' } #ym = yearmonth.encode('utf-8') ym = yearmonth year = ym[:4] month = ym[4:] return self.gettext('%(year)s %(month)s') % {'month': self.gettext(months[month]), 'year': year} security.declarePublic('commentsAllowed') def commentsAllowed(self): """ Are comments allowed? """ return self.comment_allowed security.declarePublic('commentsModerated') def commentsModerated(self): """ Are comments moderated? """ return self.comment_allowed == 2 security.declarePublic('commentsNotAllowed') def commentsNotAllowed(self): """ Are not comments allowed? """ return not self.comment_allowed referencesAllowed = commentsAllowed referencesModerated = commentsModerated referencesNotAllowed = commentsNotAllowed security.declareProtected('Manage Bitakora', 'users') def users(self): """ Users """ users = self.users_with_local_role('Blogger') if users: return users[0] return '' security.declarePublic('getUnpublishedComments') def getUnpublishedComments(self, size=None): """ get unpublished comments """ if size is not None: return self.Catalog(meta_type='Comment', published=0, sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog(meta_type='Comment', published=0, sort_on='date', sort_order='descending') security.declarePublic('getPublishedComments') def getPublishedComments(self, size=None): """ get published comments """ if size is not None: return self.Catalog(meta_type='Comment', published=1, sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog(meta_type='Comment', published=1, sort_on='date', sort_order='descending') security.declarePublic('getComments') def getComments(self, size=None): """ get comments """ if size is not None: return self.Catalog(meta_type='Comment', sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog(meta_type='Comment', sort_on='date', sort_order='descending') security.declarePublic('getUnpublishedReference') def getUnpublishedReferences(self, size=None): """ get unpublished References """ if size is not None: return self.Catalog(meta_type='Reference', published=0, sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog(meta_type='Reference', published=0, sort_on='date', sort_order='descending') security.declarePublic('getPublishedReferences') def getPublishedReferences(self, size=None): """ get published References """ if size is not None: return self.Catalog(meta_type='Reference', published=1, sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog(meta_type='Reference', published=1, sort_on='date', sort_order='descending') security.declarePublic('getReferences') def getReferences(self, size=None): """ get References """ if size is not None: return self.Catalog(meta_type='Reference', sort_on='date', sort_order='descending', sort_limit=size) else: return self.Catalog(meta_type='Reference', sort_on='date', sort_order='descending') security.declarePrivate('createId') def createId(self, title): """ Create an id for a post based on its title """ s1 = unicode(r"'\;/ &:ÀÁÂÄÇÈÊÉËÌÎÍÏÒÔÓÖÙÛÚÜÝŸàâáäçèêéëìîíïòôóöùûúüýÿÑñ", 'utf-8') s2 = unicode(r'-------aaaaceeeeiiiioooouuuuyyaaaaceeeeiiiioooouuuuyyNn', 'utf-8') # XXX This is not efficient at all # but string.maketrans and id.translate # don't work correctly with unicode strings.... id = title.strip() for i in range(len(s1)): id = id.replace(s1[i], s2[i]) id = ''.join([c for c in id if c in ok_chars]) while id.startswith('-') or id.startswith('_') or id.startswith(' '): id = id[1:] while id.endswith('-') or id.endswith('_') or id.endswith(' '): id = id[:-1] while id.find('--') != -1: id = id.replace('--', '-') id = id.lower() if not id: return u'blogpost-%d' % self.postcount return u'-'.join(id.split(' ')) security.declarePrivate('createNewId') def createNewId(self, oldid): """ Create a new id if the previous one was taken """ if oldid[-1].isdigit(): num = oldid.split('-') end = int(num[-1]) + 1 return '-'.join(num[:-1]) + '-' + str(end) else: return oldid + '-1' security.declarePublic('prepareTags') def prepareTags(self, tags=[]): """ return tags to add and edit interfaces preview """ from utils import prepareTags as prepTags return prepTags(tags) security.declareProtected('Manage Bitakora', 'importXML') def importXML(self, file, REQUEST=None): """ upload XML file with blog data """ from XMLImporter import importXML as imp data = imp(xml=file.read()) for post in data: id = self.manage_addPost(title=post['title'], author=post['author'], body=post['body'], tags=post['tags'], date=post['date'], not_clean=1, sendping=0) posta = self.get(id) for comment in post.get('comments', []): posta.manage_addComment(author=comment['author'], body=comment['body'], url=comment['url'], email=comment['email'], date=comment['date']) if REQUEST is not None: return REQUEST.RESPONSE.redirect('%s/prefs?msg=%s' % (self.absolute_url(), 'XML file imported succesfully')) security.declareProtected('Manage Bitakora', 'migrate_comments') def migrate_comments(self): """ Migrate comment attribute url to author_url, not to clash with CatalogAware's url attribute """ from logging import getLogger log = getLogger('migrate_comments') log.info('starting') for post in self.objectValues('Post'): for comment in post.objectValues('Comment'): if hasattr(comment, 'url') and \ not callable(getattr(comment, 'url')): comment.author_url = comment.url log.info('Migrated: %s - %s' % (comment.getId(), comment.author_url)) delattr log.info('done') security.declareProtected('Manage Bitakora', 'migrate_tinymce') def migrate_tinymce(self): from logging import getLogger log = getLogger('migrate_tinymce') if 'TinyMCE' not in self.objectIds(): log.info('Adding TinyMCE') self.manage_addProduct['ZTinyMCE'].manage_addZTinyMCE('TinyMCE', 'TinyMCE') maker = self.manage_addProduct['ZTinyMCE'].manage_addZTinyConfiguration for config in default_configurations: maker(config['name'], configuration=config['config'], tinymce_instance_path='/'.join(self.TinyMCE.getPhysicalPath()), title='Example configuration', optimize=True) security.declareProtected('Manage Bitakora', 'migrate_textindexng2') def migrate_textindexng2(self): """ remove all textindeng2 indexes """ from logging import getLogger log = getLogger('migrate_textindexng2') indexes = ['author', 'body', 'excerpt', 'title'] for index in indexes: if index in self.Catalog.indexes(): self.Catalog.manage_delIndex(index) log.info('Deleted %s index' % index) log.info('Deleted TextIndexNG2 indexes') security.declareProtected('Manage Bitakora', 'migrate_to_1_dot_0') def migrate_to_1_dot_0(self): """ migrate to Bitakora 1.0 """ self.migrate_comments() self.migrate_tinymce() self.migrate_textindexng2() return 1 Globals.InitializeClass(Bitakora)