diff --git a/.hgtags b/.hgtags index f3028cde1494abe23e81cff0a85fa1be76668595_LmhndGFncw==..dfc1f4c541ed31d91f84c81e1ee537b692073def_LmhndGFncw== 100644 --- a/.hgtags +++ b/.hgtags @@ -15,3 +15,5 @@ 0a7368f774db033fc4839ee01480d12bf51f1630 cubicweb-comment-debian-version-1.6.1-1 15faf8f3b555feb5422fe2c1edc567225b574a09 cubicweb-comment-version-1.6.2 a14db4d459494a413d481c6e658718e5845a52d1 cubicweb-comment-debian-version-1.6.2-1 +4a103934e8352f41a82f42e9b1184d30a64aab5b cubicweb-comment-version-1.6.3 +afad635158a298ccc66a8592aad3a81bdc032233 cubicweb-comment-debian-version-1.6.3-1 diff --git a/README b/README new file mode 100644 index 0000000000000000000000000000000000000000..dfc1f4c541ed31d91f84c81e1ee537b692073def_UkVBRE1F --- /dev/null +++ b/README @@ -0,0 +1,28 @@ +Summary +------- + +The `comment` cube provides threadable comments feature. + +Usage +----- + +This cube creates a new entity type called `Comment` which could basically be +read by every body but only added by application's users. +It also defines a relation `comments` which provides the ability to add a +`Comment` which `comments` a `Comment`. + +To use this cube, you want to add the relation `comments` on the entity type +you want to be able to comment. For instance, let's say your cube defines a +schema for a blog. You want all the blog entries to be commentable. +Here is how to define it in your schema: + +.. sourcecode:: python + + from yams.buildobjs import RelationDefinition + class comments(RelationDefinition): + subject = 'Comment' + object = 'BlogEntry' + cardinality = '1*' + +Once this relation is defined, you can post comments and view threadable +comments automatically on blog entry's primary view. diff --git a/__pkginfo__.py b/__pkginfo__.py index f3028cde1494abe23e81cff0a85fa1be76668595_X19wa2dpbmZvX18ucHk=..dfc1f4c541ed31d91f84c81e1ee537b692073def_X19wa2dpbmZvX18ucHk= 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -4,7 +4,7 @@ modname = 'comment' distname = "cubicweb-%s" % modname -numversion = (1, 6, 2) +numversion = (1, 6, 3) version = '.'.join(str(num) for num in numversion) license = 'LGPL' @@ -8,10 +8,8 @@ version = '.'.join(str(num) for num in numversion) license = 'LGPL' -copyright = '''Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). -http://www.logilab.fr/ -- mailto:contact@logilab.fr''' author = "Logilab" author_email = "contact@logilab.fr" web = 'http://www.cubicweb.org/project/%s' % distname @@ -13,33 +11,8 @@ author = "Logilab" author_email = "contact@logilab.fr" web = 'http://www.cubicweb.org/project/%s' % distname -short_desc = "commenting system for the CubicWeb framework" -long_desc = """\ -Summary -------- - -The `comment` cube provides threadable comments feature. - -Usage ------ - -This cube creates a new entity type called `Comment` which could basically be -read by every body but only added by application's users. -It also defines a relation `comments` which provides the ability to add a -`Comment` which `comments` a `Comment`. - -To use this cube, you want to add the relation `comments` on the entity type -you want to be able to comment. For instance, let's say your cube defines a -schema for a blog. You want all the blog entries to be commentable. -Here is how to define it in your schema: - -.. sourcecode:: python - - from yams.buildobjs import RelationDefinition - class comments(RelationDefinition): - subject = 'Comment' - object = 'BlogEntry' - cardinality = '1*' +description = "commenting system for the CubicWeb framework" +short_desc = description # XXX cw < 3.8 bw compat @@ -45,38 +18,7 @@ -Once this relation is defined, you can post comments and view threadable -comments automatically on blog entry's primary view. -""" - -from os import listdir -from os.path import join - -CUBES_DIR = join('share', 'cubicweb', 'cubes') - -try: - data_files = [ - [join(CUBES_DIR, 'comment'), - [fname for fname in listdir('.') - if fname.endswith('.py') and fname != 'setup.py']], - [join(CUBES_DIR, 'comment', 'data'), - [join('data', fname) for fname in listdir('data')]], - [join(CUBES_DIR, 'comment', 'i18n'), - [join('i18n', fname) for fname in listdir('i18n')]], - [join(CUBES_DIR, 'comment', 'migration'), - [join('migration', fname) for fname in listdir('migration')]], - ] -except OSError: - # we are in an installed directory - pass - - -cube_eid = 20316 -# used packages -__depends_cubes__ = {} -__depends__ = {'cubicweb': '>= 3.6.0'} -__use__ = tuple(__depends_cubes__) classifiers = [ 'Environment :: Web Environment', 'Framework :: CubicWeb', 'Programming Language :: Python', 'Programming Language :: JavaScript', ] @@ -77,6 +19,31 @@ classifiers = [ 'Environment :: Web Environment', 'Framework :: CubicWeb', 'Programming Language :: Python', 'Programming Language :: JavaScript', ] + +__depends__ = {'cubicweb': '>= 3.6.0'} + +# package ### + +from os import listdir as _listdir +from os.path import join, isdir, exists +from glob import glob + +THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname) + +def listdir(dirpath): + return [join(dirpath, fname) for fname in _listdir(dirpath) + if fname[0] != '.' and not fname.endswith('.pyc') + and not fname.endswith('~') + and not isdir(join(dirpath, fname))] + +data_files = [ + # common files + [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']], + ] +# check for possible extended cube layout +for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration'): + if isdir(dname): + data_files.append([join(THIS_CUBE_DIR, dname), listdir(dname)]) diff --git a/data/cubes.comment.js b/data/cubes.comment.js index f3028cde1494abe23e81cff0a85fa1be76668595_ZGF0YS9jdWJlcy5jb21tZW50Lmpz..dfc1f4c541ed31d91f84c81e1ee537b692073def_ZGF0YS9jdWJlcy5jb21tZW50Lmpz 100644 --- a/data/cubes.comment.js +++ b/data/cubes.comment.js @@ -66,12 +66,9 @@ } } -$(document).ready(function() { - function scroll_top(event){ - toggleVisibility('popupLoginBox'); - $('html, body').animate({scrollTop:0}, 'fast'); - return false; - } - $('a.loadPopupLogin').click(scroll_top); -}); +function showLoginBox() { + toggleVisibility('popupLoginBox'); + $('html, body').animate({scrollTop:0}, 'fast'); + return false; +} @@ -77,3 +74,3 @@ -CubicWeb.provide('ecomment.js'); +CubicWeb.provide('comment.js'); diff --git a/debian/changelog b/debian/changelog index f3028cde1494abe23e81cff0a85fa1be76668595_ZGViaWFuL2NoYW5nZWxvZw==..dfc1f4c541ed31d91f84c81e1ee537b692073def_ZGViaWFuL2NoYW5nZWxvZw== 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cubicweb-comment (1.6.3-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Mon, 26 Apr 2010 15:33:26 +0200 + cubicweb-comment (1.6.2-1) unstable; urgency=low * new upstream release diff --git a/debian/rules b/debian/rules index f3028cde1494abe23e81cff0a85fa1be76668595_ZGViaWFuL3J1bGVz..dfc1f4c541ed31d91f84c81e1ee537b692073def_ZGViaWFuL3J1bGVz 100755 --- a/debian/rules +++ b/debian/rules @@ -7,7 +7,7 @@ build: build-stamp build-stamp: dh_testdir - python setup.py -q build + NO_SETUPTOOLS=1 python setup.py -q build touch build-stamp clean: @@ -24,7 +24,7 @@ dh_testroot dh_clean -k dh_installdirs -i - python setup.py -q install --no-compile --prefix=debian/cubicweb-comment/usr/ + NO_SETUPTOOLS=1 python setup.py -q install --no-compile --prefix=debian/cubicweb-comment/usr/ # remove generated .egg-info file rm -rf debian/cubicweb-comment/usr/lib/python* diff --git a/entities.py b/entities.py index f3028cde1494abe23e81cff0a85fa1be76668595_ZW50aXRpZXMucHk=..dfc1f4c541ed31d91f84c81e1ee537b692073def_ZW50aXRpZXMucHk= 100644 --- a/entities.py +++ b/entities.py @@ -1,7 +1,7 @@ """entity classes for Comment entities :organization: Logilab -:copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr """ __docformat__ = "restructuredtext en" @@ -14,6 +14,11 @@ from cubicweb.selectors import implements from cubicweb.entities import AnyEntity, fetch_config + +def subcomments_count(commentable): + return sum([len(commentable.reverse_comments)] + + [subcomments_count(c) for c in commentable.reverse_comments]) + class Comment(TreeMixIn, AnyEntity): """customized class for Comment entities""" __regid__ = 'Comment' @@ -34,7 +39,9 @@ """ return self.root().rest_path(), {} + subcomments_count = subcomments_count + # some views potentially needed on web *and* server side (for notification) # so put them here @@ -37,8 +44,7 @@ # some views potentially needed on web *and* server side (for notification) # so put them here - class CommentFullTextView(EntityView): __regid__ = 'fulltext' __select__ = implements('Comment') diff --git a/i18n/en.po b/i18n/en.po index f3028cde1494abe23e81cff0a85fa1be76668595_aTE4bi9lbi5wbw==..dfc1f4c541ed31d91f84c81e1ee537b692073def_aTE4bi9lbi5wbw== 100644 --- a/i18n/en.po +++ b/i18n/en.po @@ -24,6 +24,9 @@ msgid "Comment_plural" msgstr "Comments" +msgid "Latest comments" +msgstr "" + msgid "New Comment" msgstr "New comment" @@ -37,6 +40,9 @@ msgid "Unknown author" msgstr "" +msgid "You are not authenticated." +msgstr "" + msgid "a comment is a reply about another entity" msgstr "" @@ -58,10 +64,9 @@ msgid "actions_reply_comment" msgstr "reply comment" -# add related box generated message msgid "actions_reply_comment_description" msgstr "" msgid "add comment" msgstr "" @@ -62,9 +67,12 @@ msgid "actions_reply_comment_description" msgstr "" msgid "add comment" msgstr "" +msgid "comment content" +msgstr "comment" + # #-#-#-#-# schema.pot #-#-#-#-# # subject and object forms for each relation type # (no object form for final relation types) @@ -104,6 +112,12 @@ "section containing comments thread an allowing to post comment on " "commentable entities" +msgid "contentnavigation_latestcomments" +msgstr "" + +msgid "contentnavigation_latestcomments_description" +msgstr "" + msgid "delete comment" msgstr "" @@ -113,12 +127,9 @@ msgid "i18n_by_author_field" msgstr "by" -msgid "latest comment(s):" -msgstr "" - msgid "login" msgstr "" msgid "new comment for" msgstr "" @@ -119,9 +130,12 @@ msgid "login" msgstr "" msgid "new comment for" msgstr "" +msgid "on date" +msgstr "date" + msgid "register" msgstr "" @@ -135,9 +149,6 @@ msgid "thread view" msgstr "" -msgid "to comment" -msgstr "" - msgid "unknown author" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index f3028cde1494abe23e81cff0a85fa1be76668595_aTE4bi9mci5wbw==..dfc1f4c541ed31d91f84c81e1ee537b692073def_aTE4bi9mci5wbw== 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -24,6 +24,9 @@ msgid "Comment_plural" msgstr "Commentaires" +msgid "Latest comments" +msgstr "Derniers commentaires" + msgid "New Comment" msgstr "Nouveau commentaire" @@ -37,6 +40,9 @@ msgid "Unknown author" msgstr "Auteur inconnu" +msgid "You are not authenticated." +msgstr "Vous n'êtes pas authentifié." + msgid "a comment is a reply about another entity" msgstr "un commentaire autour d'une autre entité" @@ -65,6 +71,9 @@ msgid "add comment" msgstr "ajouter un commentaire" +msgid "comment content" +msgstr "commentaire" + # #-#-#-#-# schema.pot #-#-#-#-# # subject and object forms for each relation type # (no object form for final relation types) @@ -101,7 +110,14 @@ msgid "contentnavigation_commentsection_description" msgstr "" -"partie affichant la liste des commentaires à propos de l'entité visualisée" +"section affichant la liste des commentaires à propos de l'entité visualisée" + +msgid "contentnavigation_latestcomments" +msgstr "derniers commentaires de l'utilisateur" + +msgid "contentnavigation_latestcomments_description" +msgstr "" +"section affichant la liste des dernierscommentaires postés par un utilisateur" msgid "delete comment" msgstr "supprimer ce commentaire" @@ -112,12 +128,9 @@ msgid "i18n_by_author_field" msgstr "par" -msgid "latest comment(s):" -msgstr "derniers commentaire(s) :" - msgid "login" msgstr "s'authentifier" msgid "new comment for" msgstr "nouveau commentaire pour" @@ -118,9 +131,12 @@ msgid "login" msgstr "s'authentifier" msgid "new comment for" msgstr "nouveau commentaire pour" +msgid "on date" +msgstr "date" + msgid "register" msgstr "s'enregistrer" @@ -134,9 +150,6 @@ msgid "thread view" msgstr "vue fil de discussions" -msgid "to comment" -msgstr "pour commenter" - msgid "unknown author" msgstr "auteur inconnu" diff --git a/schema.py b/schema.py index f3028cde1494abe23e81cff0a85fa1be76668595_c2NoZW1hLnB5..dfc1f4c541ed31d91f84c81e1ee537b692073def_c2NoZW1hLnB5 100644 --- a/schema.py +++ b/schema.py @@ -3,11 +3,7 @@ :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr """ -from yams.buildobjs import EntityType, RelationType, SubjectRelation -try: - from yams.buildobjs import RichString -except: - from cubicweb.schema import RichString +from yams.buildobjs import EntityType, RelationType, SubjectRelation, RichString from cubicweb.schema import RRQLExpression class Comment(EntityType): diff --git a/setup.py b/setup.py index f3028cde1494abe23e81cff0a85fa1be76668595_c2V0dXAucHk=..dfc1f4c541ed31d91f84c81e1ee537b692073def_c2V0dXAucHk= 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,13 @@ #!/usr/bin/env python # pylint: disable-msg=W0404,W0622,W0704,W0613,W0152 -# Copyright (c) 2003-2004 LOGILAB S.A. (Paris, FRANCE). -# http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Generic Setup script, takes package info from __pkginfo__.py file """ +"""Generic Setup script, takes package info from __pkginfo__.py file. + +:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +__docformat__ = "restructuredtext en" import os import sys import shutil @@ -19,9 +10,6 @@ import os import sys import shutil -from distutils.core import setup -from distutils import command -from distutils.command import install_lib from os.path import isdir, exists, join, walk @@ -26,3 +14,16 @@ from os.path import isdir, exists, join, walk +try: + if os.environ.get('NO_SETUPTOOLS'): + raise ImportError() + from setuptools import setup + from setuptools.command import install_lib + USE_SETUPTOOLS = 1 +except ImportError: + from distutils.core import setup + from distutils.command import install_lib + USE_SETUPTOOLS = 0 + + +sys.modules.pop('__pkginfo__', None) # import required features @@ -28,4 +29,4 @@ # import required features -from __pkginfo__ import distname, version, license, short_desc, long_desc, \ +from __pkginfo__ import modname, version, license, description, \ web, author, author_email # import optional features @@ -30,19 +31,14 @@ web, author, author_email # import optional features -try: - from __pkginfo__ import distname -except ImportError: - distname = distname -try: - from __pkginfo__ import scripts -except ImportError: - scripts = [] -try: - from __pkginfo__ import data_files -except ImportError: - data_files = None -try: - from __pkginfo__ import include_dirs -except ImportError: - include_dirs = [] +import __pkginfo__ +distname = getattr(__pkginfo__, 'distname', modname) +scripts = getattr(__pkginfo__, 'scripts', []) +data_files = getattr(__pkginfo__, 'data_files', None) +include_dirs = getattr(__pkginfo__, 'include_dirs', []) +ext_modules = getattr(__pkginfo__, 'ext_modules', None) +dependency_links = getattr(__pkginfo__, 'dependency_links', []) + +STD_BLACKLIST = ('CVS', '.svn', '.hg', 'debian', 'dist', 'build') + +IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc', '~') @@ -48,6 +44,16 @@ -BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog') -IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc') - +if exists('README'): + long_description = file('README').read() +else: + long_description = '' +if USE_SETUPTOOLS: + requires = {} + for entry in ("__depends__", "__recommends__"): + requires.update(getattr(__pkginfo__, entry, {})) + install_requires = [("%s %s" % (d, v and v or "")).strip() + for d, v in requires.iteritems()] +else: + install_requires = [] + def ensure_scripts(linux_scripts): @@ -52,6 +58,6 @@ def ensure_scripts(linux_scripts): - """creates the proper script names required for each platform + """Creates the proper script names required for each platform (taken from 4Suite) """ from distutils import util @@ -61,4 +67,18 @@ scripts_ = linux_scripts return scripts_ +def get_packages(directory, prefix): + """return a list of subpackages for the given directory""" + result = [] + for package in os.listdir(directory): + absfile = join(directory, package) + if isdir(absfile): + if exists(join(absfile, '__init__.py')) or \ + package in ('test', 'tests'): + if prefix: + result.append('%s.%s' % (prefix, package)) + else: + result.append(package) + result += get_packages(absfile, result[-1]) + return result @@ -64,4 +84,58 @@ +def export(from_dir, to_dir, + blacklist=STD_BLACKLIST, + ignore_ext=IGNORED_EXTENSIONS, + verbose=True): + """make a mirror of from_dir in to_dir, omitting directories and files + listed in the black list + """ + def make_mirror(arg, directory, fnames): + """walk handler""" + for norecurs in blacklist: + try: + fnames.remove(norecurs) + except ValueError: + pass + for filename in fnames: + # don't include binary files + if filename[-4:] in ignore_ext: + continue + if filename[-1] == '~': + continue + src = join(directory, filename) + dest = to_dir + src[len(from_dir):] + if verbose: + print >> sys.stderr, src, '->', dest + if os.path.isdir(src): + if not exists(dest): + os.mkdir(dest) + else: + if exists(dest): + os.remove(dest) + shutil.copy2(src, dest) + try: + os.mkdir(to_dir) + except OSError, ex: + # file exists ? + import errno + if ex.errno != errno.EEXIST: + raise + walk(from_dir, make_mirror, None) + + +class MyInstallLib(install_lib.install_lib): + """extend install_lib command to handle package __init__.py and + include_dirs variable if necessary + """ + def run(self): + """overridden from install_lib class""" + install_lib.install_lib.run(self) + # manually install included directories if any + if include_dirs: + base = modname + for directory in include_dirs: + dest = join(self.install_dir, base, directory) + export(directory, dest, verbose=False) def install(**kwargs): """setup entry point""" @@ -65,18 +139,32 @@ def install(**kwargs): """setup entry point""" - #kwargs['distname'] = modname - return setup(name=distname, - version=version, - license =license, - description=short_desc, - long_description=long_desc, - author=author, - author_email=author_email, - url=web, - scripts=ensure_scripts(scripts), - data_files=data_files, - **kwargs) - + if USE_SETUPTOOLS: + if '--force-manifest' in sys.argv: + sys.argv.remove('--force-manifest') + # install-layout option was introduced in 2.5.3-1~exp1 + elif sys.version_info < (2, 5, 4) and '--install-layout=deb' in sys.argv: + sys.argv.remove('--install-layout=deb') + kwargs['package_dir'] = {modname : '.'} + packages = [modname] + get_packages(os.getcwd(), modname) + if USE_SETUPTOOLS and install_requires: + kwargs['install_requires'] = install_requires + kwargs['dependency_links'] = dependency_links + kwargs['packages'] = packages + return setup(name = distname, + version = version, + license = license, + description = description, + long_description = long_description, + author = author, + author_email = author_email, + url = web, + scripts = ensure_scripts(scripts), + data_files = data_files, + ext_modules = ext_modules, + cmdclass = {'install_lib': MyInstallLib}, + **kwargs + ) + if __name__ == '__main__' : install() diff --git a/test/unittest_comment.py b/test/unittest_comment.py index f3028cde1494abe23e81cff0a85fa1be76668595_dGVzdC91bml0dGVzdF9jb21tZW50LnB5..dfc1f4c541ed31d91f84c81e1ee537b692073def_dGVzdC91bml0dGVzdF9jb21tZW50LnB5 100644 --- a/test/unittest_comment.py +++ b/test/unittest_comment.py @@ -7,7 +7,12 @@ class CommentTC(CubicWebTC): """Comment""" + + def setup_database(self): + req = self.request() + self.b = req.create_entity('BlogEntry', title=u"yo", content=u"qu\'il est beau") + def test_schema(self): self.assertEquals(self.schema['comments'].rdef('Comment', 'BlogEntry').composite, 'object') @@ -10,13 +15,7 @@ def test_schema(self): self.assertEquals(self.schema['comments'].rdef('Comment', 'BlogEntry').composite, 'object') - def setup_database(self): - super(CommentTC, self).setup_database() - self.b = self.execute('INSERT BlogEntry X: X title "yo", X content "qu\'il est beau"').get_entity(0, 0) - self.c = self.execute('INSERT Comment X: X content "bouh!", X comments B WHERE B is BlogEntry') - self.create_user('user') - def test_possible_views(self): # comment primary view priority req = self.request() @@ -20,7 +19,7 @@ def test_possible_views(self): # comment primary view priority req = self.request() - rset = req.execute('Comment X') + rset = req.create_entity('Comment', content=u"bouh!", comments=self.b).as_rset() self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset), views.CommentPrimaryView) self.assertIsInstance(self.vreg['views'].select('tree', req, rset=rset), @@ -28,6 +27,8 @@ def test_possible_actions(self): req = self.request() + req.create_entity('Comment', content=u"bouh!", comments=self.b) + self.create_user('user') # will commit rset = req.execute('Any X WHERE X is BlogEntry') actions = self.pactions(req, rset) self.failUnless(('reply_comment', views.AddCommentAction) in actions) @@ -56,14 +57,13 @@ def test_nonregr_possible_actions(self): req = self.request() - rset = req.execute('Any B WHERE B is BlogEntry') - beid = rset[0][0] - self.execute('INSERT Comment X: X content "Yooo !", X comments B WHERE B eid %s' % beid) - # now two comments are commenting this blog - rset = req.execute('Any C WHERE C comments X, X eid %s' % beid) + req.create_entity('Comment', content=u"bouh!", comments=self.b) + req.create_entity('Comment', content=u"Yooo!", comments=self.b) + # now two comments are commenting the blog + rset = self.b.related('comments', 'object') self.assertEquals(len(rset), 2) self.failUnless(self.vreg['actions'].select('reply_comment', req, rset=rset, row=0)) self.failUnless(self.vreg['actions'].select('reply_comment', req, rset=rset, row=1)) def test_add_related_actions(self): req = self.request() @@ -64,9 +64,11 @@ self.assertEquals(len(rset), 2) self.failUnless(self.vreg['actions'].select('reply_comment', req, rset=rset, row=0)) self.failUnless(self.vreg['actions'].select('reply_comment', req, rset=rset, row=1)) def test_add_related_actions(self): req = self.request() + req.create_entity('Comment', content=u"bouh!", comments=self.b) + self.create_user('user') # will comit rset = req.execute('Any X WHERE X is Comment') self.failUnlessEqual(self.pactions_by_cats(req, rset), []) cnx = self.login('user') @@ -79,14 +81,12 @@ cnx.rollback() def test_path(self): - teid = self.execute('BlogEntry X')[0][0] - eid1 = self.execute('INSERT Comment X: X content "oijzr", X comments Y WHERE Y is BlogEntry')[0][0] - eid2 = self.execute('INSERT Comment X: X content "duh?", X comments Y WHERE Y eid %s'%eid1)[0][0] - comment1 = self.entity('Any X WHERE X eid %(x)s', {'x':eid1}, 'x') - self.assertEquals(comment1.path(), [teid, eid1]) - self.assertEquals(comment1.root().eid, teid) - comment2 = self.entity('Any X WHERE X eid %(x)s', {'x':eid2}, 'x') - self.assertEquals(comment2.path(), [teid, eid1, eid2]) - self.assertEquals(comment2.root().eid, teid) + req = self.request() + c1 = req.create_entity('Comment', content=u"oijzr", comments=self.b) + c11 = req.create_entity('Comment', content=u"duh?", comments=c1) + self.assertEquals(c1.path(), [self.b.eid, c1.eid]) + self.assertEquals(c1.root().eid, self.b.eid) + self.assertEquals(c11.path(), [self.b.eid, c1.eid, c11.eid]) + self.assertEquals(c11.root().eid, self.b.eid) def test_comments_ascending_order(self): @@ -91,20 +91,21 @@ def test_comments_ascending_order(self): - teid = self.execute('BlogEntry X')[0][0] - c1 = self.entity('INSERT Comment X: X content "one", X comments Y WHERE Y eid %s'%teid) - eid1 = c1.eid - c11 = self.execute('INSERT Comment X: X content "one-one", X comments Y WHERE Y eid %s'%eid1) - c12 = self.execute('INSERT Comment X: X content "one-two", X comments Y WHERE Y eid %s'%eid1) - c2 = self.entity('INSERT Comment X: X content "two", X comments Y WHERE Y eid %s'%teid) - eid2 = c2.eid - c21= self.execute('INSERT Comment X: X content "two-one", X comments Y WHERE Y eid %s'%eid2) - c22= self.execute('INSERT Comment X: X content "two-two", X comments Y WHERE Y eid %s'%eid2) - self.commit() - rql = u'Any C,CD,CC,CCF,U,UL,US,UF ORDERBY CD WHERE C is Comment, '\ - 'C comments X, C creation_date CD, C content CC, C content_format CCF, ' \ - 'C created_by U?, U login UL, U firstname UF, U surname US, X eid %(x)s' - all_comments = self.execute(rql, {'x': teid}) - self.assertEquals([c.eid for c in all_comments.entities()], [self.c[0][0], eid1, eid2]) - self.assertEquals([c.eid for c in c1.children()], [c11[0][0], c12[0][0]]) + req = self.request() + c1 = req.create_entity('Comment', content=u"one", comments=self.b) + c11 = req.create_entity('Comment', content=u"one-one", comments=c1) + c12 = req.create_entity('Comment', content=u"one-two", comments=c1) + c2 = req.create_entity('Comment', content=u"two", comments=self.b) + self.assertEquals([c.eid for c in self.b.reverse_comments], + [c1.eid, c2.eid]) + self.assertEquals([c.eid for c in c1.children()], + [c11.eid, c12.eid]) + + def test_subcomments_count(self): + req = self.request() + c1 = req.create_entity('Comment', content=u"one", comments=self.b) + c11 = req.create_entity('Comment', content=u"one-one", comments=c1) + c12 = req.create_entity('Comment', content=u"one-two", comments=c1) + c21 = req.create_entity('Comment', content=u"two-one", comments=c12) + self.assertEquals(c1.subcomments_count(), 3) def test_fullthreadtext_views(self): @@ -109,7 +110,6 @@ def test_fullthreadtext_views(self): - c = self.entity('Comment X') - c2eid = self.execute('INSERT Comment X: X content %(text)s, X content_format "text/html", ' - 'X comments %(x)s', - {'x': c.eid, 'text': u""" + req = self.request() + c = req.create_entity('Comment', content=u"bouh!", comments=self.b) + c2 = req.create_entity('Comment', content=u""" some long <b>HTML</b> text which <em>should not</em> fit on 80 characters, so i'll add some extra xxxxxxx. @@ -115,10 +115,7 @@ some long <b>HTML</b> text which <em>should not</em> fit on 80 characters, so i'll add some extra xxxxxxx. -Yeah !"""})[0][0] - self.commit() - c2rset= self.execute('Any X WHERE X eid %(x)s', {'x': c2eid}, 'x') - v = self.vreg['views'].select('fullthreadtext', self.request(), - rset=c2rset, row=0) - content = v.render(row=0) +Yeah !""", content_format=u"text/html", comments=c) + self.commit() # needed to set author + content = c2.view('fullthreadtext') # remove date content = re.sub('..../../.. ..:..', '', content) self.assertTextEquals(content, diff --git a/views.py b/views.py index f3028cde1494abe23e81cff0a85fa1be76668595_dmlld3MucHk=..dfc1f4c541ed31d91f84c81e1ee537b692073def_dmlld3MucHk= 100644 --- a/views.py +++ b/views.py @@ -14,10 +14,8 @@ from simplejson import dumps -from cubicweb.selectors import (one_line_rset, implements, - has_permission, relation_possible, yes, - match_kwargs, score_entity, - authenticated_user) +from cubicweb.selectors import (implements, has_permission, authenticated_user, + score_entity, relation_possible, one_line_rset) from cubicweb.view import EntityView from cubicweb.uilib import rql_for_eid, cut, safe_cut from cubicweb.mixins import TreeViewMixIn @@ -21,5 +19,5 @@ from cubicweb.view import EntityView from cubicweb.uilib import rql_for_eid, cut, safe_cut from cubicweb.mixins import TreeViewMixIn -from cubicweb.web import stdmsgs, uicfg +from cubicweb.web import stdmsgs, uicfg, component, form, formwidgets as fw from cubicweb.web.action import LinkToEntityAction, Action @@ -25,7 +23,3 @@ from cubicweb.web.action import LinkToEntityAction, Action -from cubicweb.web.form import FormViewMixIn -from cubicweb.web.formwidgets import Button -from cubicweb.web.views import primary, baseviews, xmlrss -from cubicweb.web.component import EntityVComponent -from cubicweb.web.views.basecontrollers import JSonController, jsonize +from cubicweb.web.views import primary, baseviews, xmlrss, basecontrollers @@ -31,2 +25,5 @@ +_afs = uicfg.autoform_section +_afs.tag_subject_of(('*', 'comments', '*'), formtype='main', section='hidden') +_afs.tag_object_of(('*', 'comments', '*'), formtype='main', section='hidden') @@ -32,14 +29,5 @@ -uicfg.autoform_section.tag_subject_of(('*', 'comments', '*'), formtype='main', section='hidden') -uicfg.autoform_section.tag_object_of(('*', 'comments', '*'), formtype='main', section='hidden') -uicfg.actionbox_appearsin_addmenu.tag_subject_of(('*', 'comments', '*'), False) -uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'comments', '*'), False) -uicfg.primaryview_section.tag_subject_of(('*', 'comments', '*'), 'hidden') -uicfg.primaryview_section.tag_object_of(('*', 'comments', '*'), 'hidden') -# XXX this is probably *very* inefficient since we'll fetch all entities created by the user -uicfg.primaryview_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations') -uicfg.primaryview_display_ctrl.tag_object_of( - ('*', 'created_by', 'CWUser'), - {'vid': 'list', 'label': _('latest comment(s):'), 'limit': True, - 'filter': lambda rset: rset.filtered_rset(lambda x: x.e_schema == 'Comment')}) +_abaa = uicfg.actionbox_appearsin_addmenu +_abaa.tag_subject_of(('*', 'comments', '*'), False) +_abaa.tag_object_of(('*', 'comments', '*'), False) @@ -45,12 +33,8 @@ -def _login_register_link(req): - if 'registration' in req.vreg.config.cubes(): - link = u'<a href="%s">%s</a> or ' % (req.build_url('register'), - req._(u'register')) - else: - link = u'' - link += u'<a class="loadPopupLogin">%s</a>' % req._(u'login') - return link +_pvs = uicfg.primaryview_section +_pvs.tag_subject_of(('*', 'comments', '*'), 'hidden') +_pvs.tag_object_of(('*', 'comments', '*'), 'hidden') + # comment views ############################################################### @@ -88,9 +72,32 @@ self.w(u'</div>\n') +class CommentRootView(EntityView): + __regid__ = 'commentroot' + __select__ = implements('Comment') + + def cell_call(self, row, col, **kwargs): + entity = self.cw_rset.get_entity(row, col) + root = entity.root() + self.w(u'<a href="%s">%s %s</a> ' % ( + xml_escape(root.absolute_url()), + xml_escape(root.dc_type()), + xml_escape(cut(root.dc_title(), 40)))) + + +class CommentSummary(EntityView): + __regid__ = 'commentsummary' + __select__ = implements('Comment') + + def cell_call(self, row, col, **kwargs): + entity = self.cw_rset.get_entity(row, col) + maxsize = self._cw.property_value('navigation.short-line-size') + content = entity.printable_value('content', format='text/plain') + self.w(xml_escape(cut(content, maxsize))) + class CommentOneLineView(baseviews.OneLineView): __select__ = implements('Comment') def cell_call(self, row, col, **kwargs): entity = self.cw_rset.get_entity(row, col) @@ -91,18 +98,13 @@ class CommentOneLineView(baseviews.OneLineView): __select__ = implements('Comment') def cell_call(self, row, col, **kwargs): entity = self.cw_rset.get_entity(row, col) - root = entity.root() - self.w(u'[<a href="%s">#%s</a>] ' - % (xml_escape(root.absolute_url()), root.eid)) - maxsize = self._cw.property_value('navigation.short-line-size') - maxsize = maxsize - len(str(root.eid)) - content = entity.printable_value('content', format='text/plain') - content = xml_escape(cut(content, maxsize)) - self.w(u'<a href="%s">#%s <i>%s</i></a>\n' % ( - xml_escape(entity.absolute_url()), entity.eid, content)) + self.w(u'[%s] ' % entity.view('commentroot')) + self.w(u'<a href="%s"><i>%s</i></a>\n' % ( + xml_escape(entity.absolute_url()), + entity.view('commentsummary'))) class CommentTreeItemView(baseviews.ListItemView): @@ -129,17 +131,12 @@ if replyaction is not None: url = self._cw.build_ajax_replace_url( 'comment%sHolder' % entity.eid, rql_for_eid(entity.eid), - 'inlinecomment') - if self._cw.cnx.anonymous_connection: - self.w(u' | <span class="replyto">%s <a href="%s">%s</a></span>' - % (_login_register_link(self._cw), - xml_escape(url), self._cw._(replyaction.title))) - else: - self.w(u' | <span class="replyto"><a href="%s">%s</a></span>' - % (xml_escape(url), self._cw._(replyaction.title))) + 'addcommentform') + self.w(u' | <span class="replyto"><a href="%s">%s</a></span>' + % (xml_escape(url), self._cw._(replyaction.title))) editaction = actions.select_or_none('edit_comment', self._cw, rset=self.cw_rset, row=row) if editaction is not None: # split(':', 1)[1] to remove javascript: formjs = self._cw.build_ajax_replace_url( cdivid, rql_for_eid(entity.eid), @@ -140,10 +137,10 @@ editaction = actions.select_or_none('edit_comment', self._cw, rset=self.cw_rset, row=row) if editaction is not None: # split(':', 1)[1] to remove javascript: formjs = self._cw.build_ajax_replace_url( cdivid, rql_for_eid(entity.eid), - 'editcomment', 'append').split(':', 1)[1] + 'editcommentform', 'append').split(':', 1)[1] url = "javascript: jQuery('#%s div').hide(); %s" % (cdivid, formjs) self.w(u' | <span class="replyto"><a href="%s">%s</a></span>' % (xml_escape(url), self._cw._(editaction.title))) @@ -174,10 +171,7 @@ self.w(u'<li id="comment%s" class="comment">\n' % entity.eid) -# comment edition views ####################################################### - -class InlineCommentView(EntityView): - __regid__ = 'inlinecomment' - __select__ = yes() # explicit call when it makes sense +class RssItemCommentView(xmlrss.RSSItemView): + __select__ = implements('Comment') def cell_call(self, row, col): @@ -182,5 +176,17 @@ def cell_call(self, row, col): - entity = self.cw_rset.get_entity(row, col) - self.wview('inlinecommentform', None, commented=entity) + entity = self.cw_rset.complete_entity(row, col) + self.w(u'<item>\n') + self.w(u'<guid isPermaLink="true">%s</guid>\n' + % xml_escape(entity.absolute_url())) + self.render_title_link(entity) + description = entity.dc_description(format='text/html') + \ + self._cw._(u'about') + \ + u' <a href=%s>%s</a>' % (entity.root().absolute_url(), + entity.root().dc_title()) + self._marker('description', description) + self._marker('dc:date', entity.dc_date(self.date_format)) + self.render_entity_creator(entity) + self.w(u'</item>\n') + self.wview('rssitem', entity.related('comments', 'object'), 'null') @@ -186,6 +192,9 @@ -class InlineEditCommentForm(FormViewMixIn, EntityView): - __regid__ = 'editcomment' + +# comment forms ################################################################ + +class InlineEditCommentForm(form.FormViewMixIn, EntityView): + __regid__ = 'editcommentform' __select__ = implements('Comment') jsfunc = "processComment(%s, '%s', false)" @@ -194,7 +203,15 @@ def cell_call(self, row, col): self.comment_form(self.cw_rset.get_entity(row, col)) + def propose_to_login(self): + self.w(u'<div class="warning">%s ' % self._cw._('You are not authenticated.')) + if 'registration' in self._cw.vreg.config.cubes(): + self.w(u'<a href="%s">%s</a> or ' % (self._cw.build_url('register'), + self._cw._(u'register'))) + self.w(u'<a onclick="showLoginBox()">%s</a>' % self._cw._(u'login')) + self.w(u'</div>') + def comment_form(self, commented, newcomment=None): self._cw.add_js('cubes.comment.js') if newcomment is None: newcomment = commented @@ -197,8 +214,10 @@ def comment_form(self, commented, newcomment=None): self._cw.add_js('cubes.comment.js') if newcomment is None: newcomment = commented + if self._cw.cnx.anonymous_connection: + self.propose_to_login() # hack to avoid tabindex conflicts caused by Ajax requests self._cw.next_tabindex = count(20).next jseid = dumps(commented.eid) cancel_action = self.jsfunc % (jseid, '') @@ -201,10 +220,10 @@ # hack to avoid tabindex conflicts caused by Ajax requests self._cw.next_tabindex = count(20).next jseid = dumps(commented.eid) cancel_action = self.jsfunc % (jseid, '') - buttons = [Button(onclick=self.jsfunc % (jseid, self.jsonmeth)), - Button(stdmsgs.BUTTON_CANCEL, - onclick=cancel_action)] + buttons = [fw.Button(onclick=self.jsfunc % (jseid, self.jsonmeth)), + fw.Button(stdmsgs.BUTTON_CANCEL, + onclick=cancel_action)] form = self._cw.vreg['forms'].select('edition', self._cw, entity=newcomment, form_buttons=buttons) @@ -214,10 +233,10 @@ display_relations_form=False))) -class InlineCommentForm(InlineEditCommentForm): - __regid__ = 'inlinecommentform' - __select__ = match_kwargs('commented') # explicit call when it makes sense +class InlineAddCommentForm(InlineEditCommentForm): + __regid__ = 'addcommentform' + __select__ = relation_possible('comments', 'object', 'Comment', 'add') jsfunc = "processComment(%s, '%s', true)" jsonmeth = 'add_comment' @@ -220,10 +239,11 @@ jsfunc = "processComment(%s, '%s', true)" jsonmeth = 'add_comment' - def call(self, commented): + def cell_call(self, row, col): + commented = self.cw_rset.get_entity(row, col) newcomment = self._cw.vreg['etypes'].etype_class('Comment')(self._cw) newcomment.eid = self._cw.varmaker.next() self.comment_form(commented, newcomment) @@ -225,7 +245,7 @@ newcomment = self._cw.vreg['etypes'].etype_class('Comment')(self._cw) newcomment.eid = self._cw.varmaker.next() self.comment_form(commented, newcomment) -# comment component ########################################################### +# contextual components ######################################################## @@ -231,6 +251,6 @@ -class CommentSectionVComponent(EntityVComponent): +class CommentSectionVComponent(component.EntityVComponent): """a component to display a <div> html section including comments related to an object """ __regid__ = 'commentsection' @@ -233,8 +253,8 @@ """a component to display a <div> html section including comments related to an object """ __regid__ = 'commentsection' - __select__ = (EntityVComponent.__select__ + __select__ = (component.EntityVComponent.__select__ & relation_possible('comments', 'object', 'Comment')) context = 'navcontentbottom' @@ -263,8 +283,8 @@ if addcomment is not None: self.w(u'<div id="comment%sHolder"></div>' % eid) url = req.build_ajax_replace_url( - 'comment%sHolder' % eid, rql_for_eid(eid), 'inlinecomment') - self.w(u' (<a href="%s" onclick="javascript:toggleVisibility(\'addCommentLinks\');">%s</a>)' % (url, req._(addcomment.title))) + 'comment%sHolder' % eid, rql_for_eid(eid), 'addcommentform') + self.w(u' (<a href="%s">%s</a>)' % (url, req._(addcomment.title))) # XXX still necessary? #if req.use_fckeditor() and req.property_value('ui.default-text-format') == 'text/html': # req.fckeditor_config() @@ -268,8 +288,5 @@ # XXX still necessary? #if req.use_fckeditor() and req.property_value('ui.default-text-format') == 'text/html': # req.fckeditor_config() - if req.cnx.anonymous_connection: - self.w(u'<div id="addCommentLinks" class="hidden">%s %s</div>' % \ - (_login_register_link(req), req._(u'to comment'))) @@ -274,6 +291,32 @@ -# comment actions ############################################################# +class UserLatestCommentsSection(component.EntityVComponent): + """a section to display latest comments by a user""" + __select__ = component.EntityVComponent.__select__ & implements('CWUser') + __regid__ = 'latestcomments' + + def cell_call(self, row, col, view=None): + user = self.cw_rset.get_entity(row, col) + maxrelated = self._cw.property_value('navigation.related-limit') + 1 + rset = self._cw.execute( + 'Any C,CD,C,CCF ORDERBY CD DESC LIMIT %s WHERE C is Comment, ' + 'C creation_date CD, C content CC, C content_format CCF, ' + 'C created_by U, U eid %%(u)s' % maxrelated, + {'u': user.eid}) + if rset: + self.w(u'<div class="section">') + self.w(u'<h4>%s</h4>\n' % self._cw._('Latest comments').capitalize()) + self.wview('table', rset, + displaycols=range(3), # XXX may be removed with cw >= 3.8 + headers=[_('about'), _('on date'), + _('comment content')], + cellvids={0: 'commentroot', + 2: 'commentsummary', + }) + self.w(u'</div>') + + +# actions ###################################################################### class ReplyCommentAction(LinkToEntityAction): __regid__ = 'reply_comment' @@ -323,8 +366,7 @@ class DeleteCommentAction(Action): __regid__ = 'delete_comment' - __select__ = implements('Comment') & \ - authenticated_user() & \ + __select__ = implements('Comment') & authenticated_user() & \ score_entity(lambda x: not x.reverse_comments and x.has_perm('delete')) title = _('delete comment') @@ -334,5 +376,6 @@ def url(self): return self._cw.build_url(rql=self.cw_rset.printable_rql(), vid='deleteconf') -# add some comments related methods to the Jsoncontroller ##################### + +# JSONController extensions through monkey-patching ############################ @@ -338,8 +381,8 @@ -@monkeypatch(JSonController) -@jsonize +@monkeypatch(basecontrollers.JSonController) +@basecontrollers.jsonize def js_add_comment(self, commented, text, format): return self._cw.execute('INSERT Comment C: C comments X, C content %(text)s, ' 'C content_format %(format)s WHERE X eid %(x)s', {'format' : format, 'text' : text, 'x' : commented}, 'x')[0][0] @@ -341,11 +384,11 @@ def js_add_comment(self, commented, text, format): return self._cw.execute('INSERT Comment C: C comments X, C content %(text)s, ' 'C content_format %(format)s WHERE X eid %(x)s', {'format' : format, 'text' : text, 'x' : commented}, 'x')[0][0] -@monkeypatch(JSonController) -@jsonize +@monkeypatch(basecontrollers.JSonController) +@basecontrollers.jsonize def js_edit_comment(self, comment, text, format): self._cw.execute('SET C content %(text)s, C content_format %(format)s ' 'WHERE C eid %(x)s', {'format' : format, 'text' : text, 'x' : comment}, 'x') @@ -348,26 +391,4 @@ def js_edit_comment(self, comment, text, format): self._cw.execute('SET C content %(text)s, C content_format %(format)s ' 'WHERE C eid %(x)s', {'format' : format, 'text' : text, 'x' : comment}, 'x') - - -# RSS view #################################################################### - -class RssItemCommentView(xmlrss.RSSItemView): - __select__ = implements('Comment') - - def cell_call(self, row, col): - entity = self.cw_rset.complete_entity(row, col) - self.w(u'<item>\n') - self.w(u'<guid isPermaLink="true">%s</guid>\n' - % xml_escape(entity.absolute_url())) - self.render_title_link(entity) - description = entity.dc_description(format='text/html') + \ - self._cw._(u'about') + \ - u' <a href=%s>%s</a>' % (entity.root().absolute_url(), - entity.root().dc_title()) - self._marker('description', description) - self._marker('dc:date', entity.dc_date(self.date_format)) - self.render_entity_creator(entity) - self.w(u'</item>\n') - self.wview('rssitem', entity.related('comments', 'object'), 'null')