[ckan-changes] commit/ckan: 2 new changesets

Bitbucket commits-noreply at bitbucket.org
Fri Jun 17 22:54:08 UTC 2011


2 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/e7250483ecbb/
changeset:   e7250483ecbb
branch:      feature-1141-moderated-edits-ajax
user:        kindly
date:        2011-06-17 20:19:25
summary:     [merge] release 1.4.1
affected #:  44 files (20.2 KB)

--- a/CHANGELOG.txt	Thu Jun 16 17:50:05 2011 +0100
+++ b/CHANGELOG.txt	Fri Jun 17 19:19:25 2011 +0100
@@ -1,6 +1,18 @@
 CKAN CHANGELOG
 ++++++++++++++
 
+v1.4.1 2011-XX-XX
+=================
+Minor:
+  * Links in user-supplied text made less attractive to spammers (nofollow) #1181 
+  * Package change notifications - remove duplicates (#1149)
+  * Metadata dump linked to (#1169)
+  * Refactor authorization code to be common across Package, Group and Authorization Group (#1074)
+  * Refactor Web interface to use logic layer rather than model objects directly (#1078)
+
+Bug fixes
+  * Duplicate authorization roles were difficult to delete (#1083)
+
 v1.4 2011-05-19
 ===============
 Major:
@@ -48,6 +60,7 @@
 Major:
   * User list in the Web interface (#1010)
   * CKAN packaged as .deb for install on Ubuntu
+  * Resources can have extra fields (although not in web interface yet) (#826)
   * CSW Harvesting - numerous of fixes & improvements. Ready for deployment. (#738 etc)
  
 Minor:


--- a/UPGRADE.txt	Thu Jun 16 17:50:05 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-# This document describes manually upgrading a deployed CKAN instance.
-# NB: It should not be necessary to do it manually without good reason. The
-# fabric script should be used in preference to these instructions - see
-# fabfile.py
-
-# activate relevant virtualenv
-. pyenv/bin/activate
-
-# update pip-requirements.txt (or pip-requirements-metastable.txt / pip-requirements-stable.txt)
-wget http://bitbucket.org/okfn/ckan/raw/default/pip-requirements.txt
-
-# EITHER update all modules
-pip -E pyenv install -r pip-requirements.txt
-
-# OR just update ckan
-cd pyenv/src/ckan
-hg pull [-r yourrev]
-# may want to check log messages first
-# hg log -l 5
-hg up
-# only update to specific revision
-# hg up -r {rev}
-cd ../../../
-
-# bring db up to date
-paster --plugin ckan db upgrade --config {config.ini}
-
-# Restart apache (so modpython has latest code)
-sudo /etc/init.d/apache2 restart
-
-# Run smoketests in ckanext/blackbox
-# (install ckanext and run ckanext from another machine
-#  - see ckanext README.txt for instructions)
-python blackbox/smoke.py blackbox/ckan.net.profile.json


--- a/ckan/__init__.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/__init__.py	Fri Jun 17 19:19:25 2011 +0100
@@ -1,4 +1,4 @@
-__version__ = '1.4.1a'
+__version__ = '1.4.1b'
 __description__ = 'Comprehensive Knowledge Archive Network (CKAN) Software'
 __long_description__ = \
 '''The CKAN software is used to run the Comprehensive Knowledge Archive


--- a/ckan/config/deployment.ini_tmpl	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/config/deployment.ini_tmpl	Fri Jun 17 19:19:25 2011 +0100
@@ -75,8 +75,7 @@
 ckan.enable_call_timing = false
 
 # Location of RDF versions of packages
-# e.g. rdf_packages = http://semantic.ckan.net/package/
-rdf_packages =
+#rdf_packages = http://semantic.ckan.net/record/
 
 # Location of licenses group (defaults to local Python licenses package)
 #licenses_group_url = http://licenses.opendefinition.org/2.0/ckan_original


--- a/ckan/controllers/group.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/controllers/group.py	Fri Jun 17 19:19:25 2011 +0100
@@ -52,7 +52,6 @@
         query = authz.Authorizer().authorized_query(c.user, model.Group)
         query = query.order_by(model.Group.name.asc())
         query = query.order_by(model.Group.title.asc())
-        query = query.options(eagerload_all('packages'))
         c.page = Page(
             collection=query,
             page=request.params.get('page', 1),
@@ -72,7 +71,11 @@
         import ckan.misc
         format = ckan.misc.MarkdownFormat()
         desc_formatted = format.to_html(c.group.description)
-        desc_formatted = genshi.HTML(desc_formatted)
+        try: 
+            desc_formatted = genshi.HTML(desc_formatted)
+        except genshi.ParseError, e:
+            log.error('Could not print group description: %r Error: %r', c.group.description, e)
+            desc_formatted = 'Error: Could not parse group description'
         c.group_description_formatted = desc_formatted
         c.group_admins = self.authorizer.get_admins(c.group)
 


--- a/ckan/controllers/package.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/controllers/package.py	Fri Jun 17 19:19:25 2011 +0100
@@ -207,7 +207,7 @@
         # used by disqus plugin
         c.current_package_id = c.pkg.id
         
-        if config.get('rdf_packages') is not None:
+        if config.get('rdf_packages'):
             accept_header = request.headers.get('Accept', '*/*')
             for content_type, exts in negotiate(autoneg_cfg, accept_header):
                 if "html" not in exts: 
@@ -319,7 +319,7 @@
         if (context['save'] or context['preview']) and not data:
             return self._save_new(context)
 
-        data = data or {}
+        data = data or dict(request.params) 
         errors = errors or {}
         error_summary = error_summary or {}
         vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
@@ -340,7 +340,6 @@
 
         if (context['save'] or context['preview']) and not data:
             return self._save_edit(id, context)
-
         try:
             old_data = get.package_show(context)
             schema = self._db_to_form_schema()
@@ -349,6 +348,8 @@
             data = data or old_data
         except NotAuthorized:
             abort(401, _('Unauthorized to read package %s') % '')
+        except NotFound:
+            abort(404, _('Package not found'))
 
         c.pkg = context.get("package")
 
@@ -413,7 +414,7 @@
     def _save_new(self, context):
         try:
             data_dict = clean_dict(unflatten(
-                tuplize_dict(parse_params(request.params))))
+                tuplize_dict(parse_params(request.POST))))
             self._check_data_dict(data_dict)
             context['message'] = data_dict.get('log_message', '')
             pkg = create.package_create(data_dict, context)
@@ -441,7 +442,7 @@
     def _save_edit(self, id, context):
         try:
             data_dict = clean_dict(unflatten(
-                tuplize_dict(parse_params(request.params))))
+                tuplize_dict(parse_params(request.POST))))
             self._check_data_dict(data_dict)
             context['message'] = data_dict.get('log_message', '')
             if not context['moderated']:
@@ -726,7 +727,7 @@
         package_name = id
         package = model.Package.get(package_name)
         if package is None:
-            abort(404, gettext('404 Package Not Found'))
+            abort(404, gettext('Package Not Found'))
         self._clear_pkg_cache(package)
         rating = request.params.get('rating', '')
         if rating:


--- a/ckan/controllers/user.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/controllers/user.py	Fri Jun 17 19:19:25 2011 +0100
@@ -1,11 +1,15 @@
 import re
+import logging
 
 import genshi
 from sqlalchemy import or_, func, desc
+from urllib import quote
 
 import ckan.misc
 from ckan.lib.base import *
 
+log = logging.getLogger(__name__)
+
 def login_form():
     return render('user/login_form.html').replace('FORM_ACTION', '%s')
 
@@ -77,9 +81,18 @@
             c.login = request.params.getone('login')
             c.fullname = request.params.getone('fullname')
             c.email = request.params.getone('email')
+            if not c.login:
+                h.flash_error(_("Please enter a login name."))
+                return render("user/register.html")
+            if not model.User.check_name_valid(c.login):
+                h.flash_error(_('That login name is not valid. It must be at least 3 characters, restricted to alphanumerics and these symbols: %s') % '_\-')
+                return render("user/register.html")
             if not model.User.check_name_available(c.login):
-                h.flash_error(_("That username is not available."))
+                h.flash_error(_("That login name is not available."))
                 return render("user/register.html")
+            if not request.params.getone('password1'):
+                h.flash_error(_("Please enter a password."))
+                return render("user/register.html")                
             try:
                 password = self._get_form_password()
             except ValueError, ve:
@@ -90,7 +103,8 @@
             model.Session.add(user)
             model.Session.commit() 
             model.Session.remove()
-            h.redirect_to(str('/login_generic?login=%s&password=%s' % (c.login, password.encode('utf-8'))))
+            h.redirect_to('/login_generic?login=%s&password=%s' % (str(c.login), quote(password.encode('utf-8'))))
+
         return render('user/register.html')
 
     def login(self):
@@ -138,10 +152,15 @@
             c.user_email = request.params.getone('email')
         elif 'save' in request.params:
             try:
-                rev = model.repo.new_revision()
-                rev.author = c.author
-                rev.message = _(u'Changed user details')
-                user.about = request.params.getone('about')
+                about = request.params.getone('about')
+                if 'http://' in about or 'https://' in about:
+                    msg = _('Edit not allowed as it looks like spam. Please avoid links in your description.')
+                    h.flash_error(msg)
+                    c.user_about = about
+                    c.user_fullname = request.params.getone('fullname')
+                    c.user_email = request.params.getone('email')
+                    return render('user/edit.html')
+                user.about = about
                 user.fullname = request.params.getone('fullname')
                 user.email = request.params.getone('email')
                 try:
@@ -164,8 +183,13 @@
         
     def _format_about(self, about):
         about_formatted = ckan.misc.MarkdownFormat().to_html(about)
-        return genshi.HTML(about_formatted) 
-
+        try:
+            html = genshi.HTML(about_formatted)
+        except genshi.ParseError, e:
+            log.error('Could not print "about" field Field: %r Error: %r', about, e)
+            html = 'Error: Could not parse About text'
+        return html
+    
     def _get_form_password(self):
         password1 = request.params.getone('password1')
         password2 = request.params.getone('password2')


--- a/ckan/lib/authenticator.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/lib/authenticator.py	Fri Jun 17 19:19:25 2011 +0100
@@ -14,10 +14,10 @@
                 # TODO: Implement a mask to ask for an alternative user 
                 # name instead of just using the OpenID identifier. 
                 name = identity.get('repoze.who.plugins.openid.nickname')
+                if not User.check_name_valid(name):
+                    name = openid
                 if not User.check_name_available(name):
                     name = openid
-                if User.by_name(name):
-                    name = openid
                 user = User(openid=openid, name=name,
                         fullname=identity.get('repoze.who.plugins.openid.fullname'),
                         email=identity.get('repoze.who.plugins.openid.email'))


--- a/ckan/lib/cli.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/lib/cli.py	Fri Jun 17 19:19:25 2011 +0100
@@ -4,9 +4,22 @@
 from pprint import pprint
 
 import paste.script
+from paste.registry import Registry
 from paste.script.util.logging_config import fileConfig
 import re
 
+class MockTranslator(object): 
+    def gettext(self, value): 
+        return value 
+
+    def ugettext(self, value): 
+        return value 
+
+    def ungettext(self, singular, plural, n):
+        if n > 1:
+            return plural
+        return singular
+
 class CkanCommand(paste.script.command.Command):
     parser = paste.script.command.Command.standard_parser(verbose=True)
     parser.add_option('-c', '--config', dest='config',
@@ -30,7 +43,12 @@
         except Exception: pass
         conf = appconfig('config:' + self.filename)
         load_environment(conf.global_conf, conf.local_conf)
-        
+
+        self.registry=Registry()
+        self.registry.prepare()
+        import pylons
+        self.translator_obj = MockTranslator()
+        self.registry.register(pylons.translator, self.translator_obj)
 
     def _setup_app(self):
         cmd = paste.script.appinstall.SetupCommand('setup-app') 


--- a/ckan/lib/create_test_data.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/lib/create_test_data.py	Fri Jun 17 19:19:25 2011 +0100
@@ -380,7 +380,7 @@
         model.Session.add_all([
             model.User(name=u'tester', apikey=u'tester', password=u'tester'),
             model.User(name=u'joeadmin', password=u'joeadmin'),
-            model.User(name=u'annafan', about=u'I love reading Annakarenina', password=u'annafan'),
+            model.User(name=u'annafan', about=u'I love reading Annakarenina. My site: <a href="http://anna.com">anna.com</a>', password=u'annafan'),
             model.User(name=u'russianfan', password=u'russianfan'),
             model.User(name=u'testsysadmin', password=u'testsysadmin'),
             ])


--- a/ckan/lib/munge.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/lib/munge.py	Fri Jun 17 19:19:25 2011 +0100
@@ -93,3 +93,10 @@
         else:
             r += str(char)
     return r
+
+
+def munge_tag(tag):
+    tag = substitute_ascii_equivalents(tag)
+    tag = tag.lower().strip()
+    return re.sub(r'[^a-zA-Z0-9 ]', '', tag).replace(' ', '-')
+


--- a/ckan/lib/search/sql.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/lib/search/sql.py	Fri Jun 17 19:19:25 2011 +0100
@@ -142,7 +142,7 @@
                 if isinstance(terms, basestring):
                     terms = terms.split()
                    
-                if hasattr(model.Package, field):
+                if field in model.package_table.c:
                     model_attr = getattr(model.Package, field)
                     for term in terms:
                         q = q.filter(make_like(model_attr, term))
@@ -192,7 +192,7 @@
         group = model.Group.by_name(unicode(term), autoflush=False)
         if group:
             # need to keep joining for each filter
-            q = q.join('groups', aliased=True).filter(
+            q = q.join('package_group_all', 'group', aliased=True).filter(
                 model.Group.id==group.id)
         else:
             # unknown group, so torpedo search


--- a/ckan/lib/stats.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/lib/stats.py	Fri Jun 17 19:19:25 2011 +0100
@@ -49,6 +49,7 @@
         package_group = table('package_group')
         s = select([package_group.c.group_id, func.count(package_group.c.package_id)]).\
             group_by(package_group.c.group_id).\
+            where(package_group.c.group_id!=None).\
             order_by(func.count(package_group.c.package_id).desc()).\
             limit(limit)
         res_ids = model.Session.execute(s).fetchall()        


--- a/ckan/logic/action/get.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/logic/action/get.py	Fri Jun 17 19:19:25 2011 +0100
@@ -89,7 +89,7 @@
 
     query = ckan.authz.Authorizer().authorized_query(user, model.Group, model.Action.EDIT)
     groups = set(query.all())
-    return set([group.id for group in groups])
+    return dict((group.id, group.name) for group in groups)
 
 def group_list_availible(context):
     model = context['model']


--- a/ckan/misc.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/misc.py	Fri Jun 17 19:19:25 2011 +0100
@@ -16,34 +16,41 @@
     internal_link = re.compile('(package|tag|group):([a-z0-9\-_]+)')
     normal_link = re.compile('<(http:[^>]+)>')
 
-    html_whitelist = 'a b center li ol p table td tr ul'.split(' ')
+    html_whitelist = 'b center li ol p table td tr ul'.split(' ')
     whitelist_elem = re.compile(r'<(\/?(%s)[^>]*)>' % "|".join(html_whitelist), re.IGNORECASE)
     whitelist_escp = re.compile(r'\\xfc\\xfd(\/?(%s)[^>]*?)\\xfd\\xfc' % "|".join(html_whitelist), re.IGNORECASE)
+    normal_link = re.compile(r'<a[^>]*?href="([^"]*?)"[^>]*?>', re.IGNORECASE)
+    abbrev_link = re.compile(r'<(http://[^>]*)>', re.IGNORECASE)
+    any_link = re.compile(r'<a[^>]*?>', re.IGNORECASE)
+    close_link = re.compile(r'<(\/a[^>]*)>', re.IGNORECASE)
+    link_escp = re.compile(r'\\xfc\\xfd(\/?(%s)[^>]*?)\\xfd\\xfc' % "|".join(['a']), re.IGNORECASE)
     
     def to_html(self, text):
         if text is None:
             return ''
-        
-        try: 
-            # Encode whitelist elements.
-            text = self.whitelist_elem.sub(r'\\\\xfc\\\\xfd\1\\\\xfd\\\\xfc', text)
+        # Encode whitelist elements.
+        text = self.whitelist_elem.sub(r'\\\\xfc\\\\xfd\1\\\\xfd\\\\xfc', text)
 
-            # Convert internal links.
-            text = self.internal_link.sub(r'[\1:\2] (/\1/\2)', text)
+        # Encode links only in an acceptable format (guard against spammers)
+        text = self.normal_link.sub(r'\\\\xfc\\\\xfda href="\1" target="_blank" rel="nofollow"\\\\xfd\\\\xfc', text)
+        text = self.abbrev_link.sub(r'\\\\xfc\\\\xfda href="\1" target="_blank" rel="nofollow"\\\\xfd\\\\xfc\1</a>', text)
+        text = self.any_link.sub(r'\\\\xfc\\\\xfda href="TAG MALFORMED" target="_blank" rel="nofollow"\\\\xfd\\\\xfc', text)
+        text = self.close_link.sub(r'\\\\xfc\\\\xfd\1\\\\xfd\\\\xfc', text)
 
-            # Convert <link> to markdown format.
-            text = self.normal_link.sub(r'[\1] (\1)', text)
+        # Convert internal links.
+        text = self.internal_link.sub(r'[\1:\2] (/\1/\2)', text)
 
-            # Convert <link> to markdown format.
-            text = self.normal_link.sub(r'[\1] (\1)', text)
+        # Convert <link> to markdown format.
+        text = self.normal_link.sub(r'[\1] (\1)', text)
 
-            # Markdown to HTML.
-            text = webhelpers.markdown.markdown(text, safe_mode=True)
+        # Convert <link> to markdown format.
+        text = self.normal_link.sub(r'[\1] (\1)', text)
 
-            # Decode whitelist elements.
-            text = self.whitelist_escp.sub(r'<\1>', text)
-        except Exception, e: 
-            log.exception(e)
-            text = '<p>%s</p>' % _('<strong>Warning:</strong>: Text could not be rendered.')
+        # Markdown to HTML.
+        text = webhelpers.markdown.markdown(text, safe_mode=True)
+
+        # Decode whitelist elements.
+        text = self.whitelist_escp.sub(r'<\1>', text)
+        text = self.link_escp.sub(r'<\1>', text)
 
         return text


--- a/ckan/model/group.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/model/group.py	Fri Jun 17 19:19:25 2011 +0100
@@ -8,6 +8,7 @@
 from types import make_uuid
 import vdm.sqlalchemy
 from ckan.model import extension
+from sqlalchemy.ext.associationproxy import association_proxy
 
 __all__ = ['group_table', 'Group', 'package_revision_table',
            'PackageGroup', 'GroupRevision', 'PackageGroupRevision',
@@ -58,7 +59,6 @@
     def get(cls, reference):
         '''Returns a group object referenced by its id or name.'''
         query = Session.query(cls).filter(cls.id==reference)
-        query = query.options(eagerload_all('packages'))
         group = query.first()
         if group == None:
             group = cls.by_name(reference)
@@ -68,7 +68,7 @@
     def active_packages(self, load_eager=True):
         query = Session.query(Package).\
                filter_by(state=vdm.sqlalchemy.State.ACTIVE).\
-               join('groups').filter_by(id=self.id)
+               join('package_group_all', 'group').filter_by(id=self.id)
         if load_eager:
             query = query.options(eagerload_all('package_tags.tag'))
             query = query.options(eagerload_all('resource_groups_all.resources_all'))
@@ -120,13 +120,8 @@
         return '<Group %s>' % self.name
 
 
-mapper(Group, group_table, properties={
-    'packages': relation(Package, secondary=package_group_table,
-        backref='groups',
-        order_by=package_table.c.name
-    ),
-},
-    extension=[vdm.sqlalchemy.Revisioner(group_revision_table),],
+mapper(Group, group_table, 
+       extension=[vdm.sqlalchemy.Revisioner(group_revision_table),],
 )
 
 
@@ -134,18 +129,26 @@
 GroupRevision = vdm.sqlalchemy.create_object_version(mapper, Group,
         group_revision_table)
 
-
 mapper(PackageGroup, package_group_table, properties={
     'group': relation(Group,
-        backref='package_group_all',
+        backref=backref('package_group_all', cascade='all, delete-orphan'),
     ),
     'package': relation(Package,
-        backref='package_group_all',
+        backref=backref('package_group_all', cascade='all, delete-orphan'),
     ),
 },
     extension=[vdm.sqlalchemy.Revisioner(package_group_revision_table),],
 )
 
+def _create_group(group):
+    return PackageGroup(group=group)
+
+def _create_package(package):
+    return PackageGroup(package=package)
+
+Package.groups = association_proxy('package_group_all', 'group', creator=_create_group)
+Group.packages = association_proxy('package_group_all', 'package', creator=_create_package)
+
 
 vdm.sqlalchemy.modify_base_object_mapper(PackageGroup, Revision, State)
 PackageGroupRevision = vdm.sqlalchemy.create_object_version(mapper, PackageGroup,


--- a/ckan/model/package.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/model/package.py	Fri Jun 17 19:19:25 2011 +0100
@@ -75,8 +75,10 @@
 
     @property
     def resources(self):
+        if len(self.resource_groups_all) == 0:
+            return []
+
         assert len(self.resource_groups_all) == 1, "can only use resources on packages if there is only one resource_group"
-
         return self.resource_groups_all[0].resources
     
     def update_resources(self, res_dicts, autoflush=True):
@@ -527,4 +529,3 @@
 
         return fields
 
-


--- a/ckan/model/user.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/model/user.py	Fri Jun 17 19:19:25 2011 +0100
@@ -107,12 +107,16 @@
     password = property(_get_password, _set_password)
     
     @classmethod
+    def check_name_valid(cls, name):
+        if not name \
+            or not len(name.strip()) \
+            or not cls.VALID_NAME.match(name):
+            return False
+        return True
+
+    @classmethod
     def check_name_available(cls, name):
-        if not name \
-           or not len(name.strip()) \
-           or not cls.VALID_NAME.match(name):
-           return False
-        return cls.by_name(name)==None
+        return cls.by_name(name) == None
 
     def as_dict(self):
         _dict = DomainObject.as_dict(self)


--- a/ckan/public/css/forms.css	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/public/css/forms.css	Fri Jun 17 19:19:25 2011 +0100
@@ -46,7 +46,7 @@
 input.title {
   font-size: 1.5em; }
 input.short {
-  width: 15em; }
+  width: 10em; }
 input.medium-width {
   width: 25em; }
 input.long {


--- a/ckan/templates/layout_base.html	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/templates/layout_base.html	Fri Jun 17 19:19:25 2011 +0100
@@ -97,7 +97,10 @@
         <ul><li>${h.nav_link(c, _('Home'), controller='home', action='index', id=None)}</li><li>${h.nav_link(c, _('Search'), controller='package', action='index', id=None, highlight_actions = 'search index')}</li>
-          <li py:if="h.am_authorized(c, actions.PACKAGE_CREATE)">${h.nav_link(c, _('Add a package'), controller='package', action='new', id=None)}</li>
+<?python
+    am_authorized_package_create = h.am_authorized(c, actions.PACKAGE_CREATE)
+?>
+          <li py:if="am_authorized_package_create">${h.nav_link(c, _('Add a package'), controller='package', action='new', id=None)}</li><li>${h.nav_link(c, _('Tags'), controller='tag', action='index', id=None)}</li><li>${h.nav_link(c, _('Groups'), controller='group', action='index', id=None, highlight_actions = 'new index')}</li><li>${h.nav_link(c, _('About'), controller='home', action='about', id=None)}</li>
@@ -164,10 +167,16 @@
           <div class="textwidget"><ul><li>${h.nav_link(c, _('Search'), controller='package', action='search', id=None)}</li>                
-                <li py:if="h.am_authorized(c, actions.PACKAGE_CREATE)">${h.nav_link(c, _('Register a new Package'), controller='package', action='new', id=None)}</li>
+                <li py:if="am_authorized_package_create">${h.nav_link(c, _('Register a new Package'), controller='package', action='new', id=None)}</li><li>${h.nav_link(c, _('Revision History'), controller='revision', action='index', id=None)}</li><li>${h.link_to(_('API'), h.url_for(controller='api', action='get_api', id=None))}</li><li>${h.link_to(_('API Docs'), 'http://wiki.ckan.net/API')}</li>
+<?python
+  from pylons import config
+  dumps_url = config.get('ckan.dumps_url')
+  dumps_format = config.get('ckan.dumps_format', '')
+?>
+                <li py:if="dumps_url">${h.link_to(_('Full %s dump') % dumps_format, dumps_url)}</li></ul></div></li>


--- a/ckan/templates/package/new_package_form.html	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/templates/package/new_package_form.html	Fri Jun 17 19:19:25 2011 +0100
@@ -98,11 +98,11 @@
   <legend>Groups</legend><dl><py:for each="num, group in enumerate(data.get('groups', []))">
-      <dt>
+      <dt py:if="'id' in group"><input type="${'checkbox' if group['id'] in c.groups_authz else 'hidden'}" name="groups__${num}__id" checked="checked" value="${group['id']}" />
-      <input type="hidden" name="groups__${num}__name" value="${group['name']}" />
+      <input type="hidden" name="groups__${num}__name" value="${group.get('name', c.groups_authz.get(group['id']))}" /></dt>     
-      <dd><label for="groups__${num}__checked">${group['name']}</label></dd>
+      <dd py:if="'id' in group"><label for="groups__${num}__checked">${group.get('name', c.groups_authz.get(group['id']))}</label></dd></py:for><dt>Group</dt>


--- a/ckan/templates/package/search.html	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/templates/package/search.html	Fri Jun 17 19:19:25 2011 +0100
@@ -25,6 +25,22 @@
     ${facet_sidebar('res_format')}
     ${facet_sidebar('license')}
 
+    <li class="widget-container widget_text">
+        <h4>Other access</h4>
+<?python
+  from pylons import config
+  dumps_url = config.get('ckan.dumps_url')
+  dumps_format = config.get('ckan.dumps_format', '')
+?>
+        <p>
+            You can also access this registry using the 
+            ${h.link_to(_('API'), h.url_for(controller='api', action='get_api', id=None))}
+            (see ${h.link_to(_('API Docs'), 'http://wiki.ckan.net/API')})<py:if test="dumps_url">
+                or download a <a href="${dumps_url}">full ${dumps_format} 
+                dump</a></py:if>.
+        </p>
+    </li>
+ 
   </py:match><div py:match="content">


--- a/ckan/templates/user/register.html	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/templates/user/register.html	Fri Jun 17 19:19:25 2011 +0100
@@ -41,7 +41,7 @@
         <input type="password" name="password2" value="" /><br/></fieldset>
-      ${h.submit('s', _('Sign up'))}
+      ${h.submit('signup', _('Sign up'))}
     </form></div><xi:include href="layout.html" />


--- a/ckan/tests/functional/test_autoneg.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/functional/test_autoneg.py	Fri Jun 17 19:19:25 2011 +0100
@@ -66,6 +66,7 @@
         assert response.status == 303, response.status
         location = response.header("Location")
         assert location.endswith(".rdf"), location
+        assert location.startswith('http://test.com/package/'), location
 
     def test_turtle(self):
         url = url_for(controller='package', action='read', id='annakarenina')
@@ -73,4 +74,5 @@
         assert response.status == 303, response.status
         location = response.header("Location")
         assert location.endswith(".ttl"), location
+        assert location.startswith('http://test.com/package/'), location
 


--- a/ckan/tests/functional/test_package.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/functional/test_package.py	Fri Jun 17 19:19:25 2011 +0100
@@ -6,6 +6,7 @@
 from genshi.core import escape as genshi_escape
 from difflib import unified_diff
 from nose.plugins.skip import SkipTest
+from nose.tools import assert_equal
 
 from ckan.tests import *
 from ckan.tests import search_related
@@ -308,6 +309,10 @@
         assert anna.resources[0].description in res
         assert anna.resources[0].hash in res
         assert 'Some test notes' in res
+        self.check_named_element(res, 'a',
+                                 'http://ckan.net/',
+                                 'target="_blank"',
+                                 'rel="nofollow"')
         assert '<strong>Some bolded text.</strong>' in res
         self.check_tag_and_data(res, 'left arrow', '<')
         self.check_tag_and_data(res, 'umlaut', u'\xfc')
@@ -343,11 +348,8 @@
         offset = url_for(controller='package', action='read', id=pkg_name)
         res = self.app.get(offset)
         def check_link(res, controller, id):
-            link = '<a href="/%s/%s">%s:%s</a>' % (controller, id, controller, id)
-            if link not in res:
-                print self.main_div(res).encode('utf8')
-                print 'Missing link: %r' % link
-                raise AssertionError
+            self.check_tag_and_data(res, 'a ', '/%s/%s' % (controller, id),
+                                    '%s:%s' % (controller, id))
         check_link(res, 'package', 'pkg-1')
         check_link(res, 'tag', 'tag_1')
         check_link(res, 'group', 'test-group-1')
@@ -869,6 +871,8 @@
             assert field_name in res
             fv = res.forms['package-edit']
             fv[prefix + 'groups__0__id'] = grp.id
+            res = fv.submit('preview', extra_environ={'REMOTE_USER':'russianfan'})
+            assert not 'error' in res
             res = fv.submit('save', extra_environ={'REMOTE_USER':'russianfan'})
             res = res.follow()
             pkg = model.Package.by_name(u'editpkgtest')
@@ -901,6 +905,11 @@
         finally:
             self._reset_data()
 
+    def test_edit_404(self):
+        self.offset = url_for(controller='package', action='edit', id='random_name')
+        self.res = self.app.get(self.offset, status=404)
+
+
 class TestNew(TestPackageForm):
     pkg_names = []
 
@@ -915,18 +924,11 @@
 
     def test_new_with_params_1(self):
         offset = url_for(controller='package', action='new',
-                url='http://xxx.org')
+                url='http://xxx.org', name='xxx.org')
         res = self.app.get(offset)
         form = res.forms['package-edit']
-        form['url'].value == 'http://xxx.org/'
-        form['name'].value == 'xxx.org'
-
-    def test_new_with_params_2(self):
-        offset = url_for(controller='package', action='new',
-                url='http://www.xxx.org')
-        res = self.app.get(offset)
-        form = res.forms['package-edit']
-        form['name'].value == 'xxx.org'
+        assert_equal(form['url'].value, 'http://xxx.org')
+        assert_equal(form['name'].value, 'xxx.org')
 
     def test_new_without_resource(self):
         # new package
@@ -1385,7 +1387,7 @@
         self.body = str(self.res)
         self.assert_fragment('<table width="100%" border="1">')
         self.assert_fragment('<td rowspan="2"><b>Description</b></td>')
-        self.assert_fragment('<a href="http://www.nber.org/patents/subcategories.txt">subcategory.txt</a>')
+        self.assert_fragment('<a href="http://www.nber.org/patents/subcategories.txt" target="_blank" rel="nofollow">subcategory.txt</a>')
         self.assert_fragment('<td colspan="2"><center>--</center></td>')
         self.fail_if_fragment('<script>')
 


--- a/ckan/tests/functional/test_user.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/functional/test_user.py	Fri Jun 17 19:19:25 2011 +0100
@@ -1,15 +1,14 @@
 from routes import url_for
+from nose.tools import assert_equal
 
 from ckan.tests import search_related, CreateTestData
+from ckan.tests.html_check import HtmlCheckMethods
 import ckan.model as model
 from base import FunctionalTestCase
 
-class TestUserController(FunctionalTestCase):
+class TestUserController(FunctionalTestCase, HtmlCheckMethods):
     @classmethod
     def setup_class(self):
-        model.repo.init_db()
-        model.repo.rebuild_db()
-        model.repo.init_db()
         CreateTestData.create()
 
         # make 3 changes, authored by annafan
@@ -20,10 +19,19 @@
             rev.author = u'annafan'
             model.repo.commit_and_remove()
 
+        CreateTestData.create_user('unfinisher', about='<a href="http://unfinished.tag')
+        CreateTestData.create_user('uncloser', about='<a href="http://unclosed.tag">')
+        CreateTestData.create_user('spammer', about=u'<a href="http://mysite">mysite</a><a href=\u201dhttp://test2\u201d>test2</a>')
+        CreateTestData.create_user('spammer2', about=u'<a href="http://spamsite1.com\u201d>spamsite1</a>\r\n<a href="http://www.spamsite2.com\u201d>spamsite2</a>\r\n')
+        
     @classmethod
     def teardown_class(self):
         model.repo.rebuild_db()
 
+    def teardown(self):
+        # just ensure we're not logged in
+        self.app.get('/user/logout')
+
     def test_user_read(self):
         user = model.User.by_name(u'annafan')
         offset = '/user/%s' % user.id
@@ -34,6 +42,10 @@
         assert 'My Account' not in main_res, main_res
         assert 'about' in main_res, main_res
         assert 'I love reading Annakarenina' in res, main_res
+        self.check_named_element(res, 'a',
+                                 'http://anna.com',
+                                 'target="_blank"',
+                                 'rel="nofollow"')
         assert 'Edit' not in main_res, main_res
         assert 'Number of edits:</strong> 3' in res, res
         assert 'Number of packages administered:</strong> 1' in res, res
@@ -60,6 +72,48 @@
         assert 'My Account' in main_res, main_res
         assert 'Edit' in main_res, main_res
 
+    def test_user_read_about_unfinished(self):
+        user = model.User.by_name(u'unfinisher')
+        offset = '/user/%s' % user.id
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'unfinisher' in res, res
+        assert '<a href="http://unfinished.tag' in main_res, main_res
+
+    def test_user_read_about_unclosed(self):
+        user = model.User.by_name(u'uncloser')
+        offset = '/user/%s' % user.id
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'unclosed' in res, res
+        # tag gets closed by genshi
+        assert '<a href="http://unclosed.tag" target="_blank" rel="nofollow">\n</a>' in main_res, main_res
+
+    def test_user_read_about_spam(self):
+        user = model.User.by_name(u'spammer')
+        offset = '/user/%s' % user.id
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'spammer' in res, res
+        self.check_named_element(res, 'a',
+                                 'href="http://mysite"',
+                                 'target="_blank"',
+                                 'rel="nofollow"')
+
+        self.check_named_element(res, 'a',
+                                 'href="TAG MALFORMED"',
+                                 'target="_blank"',
+                                 'rel="nofollow"')
+
+    def test_user_read_about_spam2(self):
+        user = model.User.by_name(u'spammer2')
+        offset = '/user/%s' % user.id
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'spammer2' in res, res
+        assert 'spamsite2' not in res, res
+        assert 'Error: Could not parse About text' in res, res
+        
     def test_user_login(self):
         offset = url_for(controller='user', action='login', id=None)
         res = self.app.get(offset, status=200)
@@ -120,6 +174,154 @@
         res = self.app.get(offset, extra_environ={'REMOTE_USER': 'okfntest'})
         assert 'Your API key is: %s' % user.apikey in res, res
 
+    def test_user_create(self):
+        # create/register user
+        username = 'testcreate'
+        fullname = u'Test Create'
+        password = u'testpassword'
+        assert not model.User.by_name(unicode(username))
+
+        offset = url_for(controller='user', action='register')
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'Register' in main_res, main_res
+        fv = res.forms['register_form']
+        fv['login'] = username
+        fv['fullname'] = fullname
+        fv['password1'] = password
+        fv['password2'] = password
+        res = fv.submit('signup')
+        
+        # view user
+        assert res.status == 302, self.main_div(res).encode('utf8')
+        res = res.follow()
+        if res.status == 302:
+            res = res.follow()
+        if res.status == 302:
+            res = res.follow()
+        if res.status == 302:
+            res = res.follow()
+        assert res.status == 200, res
+        main_res = self.main_div(res)
+        assert username in main_res, main_res
+        assert fullname in main_res, main_res
+
+        user = model.User.by_name(unicode(username))
+        assert user
+        assert_equal(user.name, username)
+        assert_equal(user.fullname, fullname)
+        assert user.password
+
+    def test_user_create_unicode(self):
+        # create/register user
+        username = u'testcreate4'
+        fullname = u'Test Create\xc2\xa0'
+        password = u'testpassword\xc2\xa0'
+        assert not model.User.by_name(username)
+
+        offset = url_for(controller='user', action='register')
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'Register' in main_res, main_res
+        fv = res.forms['register_form']
+        fv['login'] = username
+        fv['fullname'] = fullname.encode('utf8')
+        fv['password1'] = password.encode('utf8')
+        fv['password2'] = password.encode('utf8')
+        res = fv.submit('signup')
+        
+        # view user
+        assert res.status == 302, self.main_div(res).encode('utf8')
+        res = res.follow()
+        if res.status == 302:
+            res = res.follow()
+        if res.status == 302:
+            res = res.follow()
+        if res.status == 302:
+            res = res.follow()
+        assert res.status == 200, res
+        main_res = self.main_div(res)
+        assert username in main_res, main_res
+        assert fullname in main_res, main_res
+
+        user = model.User.by_name(unicode(username))
+        assert user
+        assert_equal(user.name, username)
+        assert_equal(user.fullname, fullname)
+        assert user.password
+
+    def test_user_create_no_name(self):
+        # create/register user
+        password = u'testpassword'
+
+        offset = url_for(controller='user', action='register')
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'Register' in main_res, main_res
+        fv = res.forms['register_form']
+        fv['password1'] = password
+        fv['password2'] = password
+        res = fv.submit('signup')
+        assert res.status == 200, res
+        main_res = self.main_div(res)
+        assert 'Please enter a login name' in main_res, main_res
+
+    def test_user_create_bad_name(self):
+        # create/register user
+        username = u'%%%%%%' # characters not allowed
+        password = 'testpass'
+
+        offset = url_for(controller='user', action='register')
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'Register' in main_res, main_res
+        fv = res.forms['register_form']
+        fv['login'] = username
+        fv['password1'] = password
+        fv['password2'] = password
+        res = fv.submit('signup')
+        assert res.status == 200, res
+        main_res = self.main_div(res)
+        assert 'login name is not valid' in main_res, main_res
+        self.check_named_element(main_res, 'input', 'name="login"', 'value="%s"' % username)
+
+    def test_user_create_bad_password(self):
+        # create/register user
+        username = 'testcreate2'
+        password = u'a' # too short
+
+        offset = url_for(controller='user', action='register')
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'Register' in main_res, main_res
+        fv = res.forms['register_form']
+        fv['login'] = username
+        fv['password1'] = password
+        fv['password2'] = password
+        res = fv.submit('signup')
+        assert res.status == 200, res
+        main_res = self.main_div(res)
+        assert 'password must be 4 characters or longer' in main_res, main_res
+        self.check_named_element(main_res, 'input', 'name="login"', 'value="%s"' % username)
+
+    def test_user_create_without_password(self):
+        # create/register user
+        username = 'testcreate3'
+        user = model.User.by_name(unicode(username))
+
+        offset = url_for(controller='user', action='register')
+        res = self.app.get(offset, status=200)
+        main_res = self.main_div(res)
+        assert 'Register' in main_res, main_res
+        fv = res.forms['register_form']
+        fv['login'] = username
+        # no password
+        res = fv.submit('signup')
+        assert res.status == 200, res
+        main_res = self.main_div(res)
+        assert 'Please enter a password' in main_res, main_res
+        self.check_named_element(main_res, 'input', 'name="login"', 'value="%s"' % username)
+
     def test_user_edit(self):
         # create user
         username = 'testedit'
@@ -167,6 +369,32 @@
         main_res = self.main_div(res)
         assert new_about in main_res, main_res
 
+    def test_edit_spammer(self):
+        # create user
+        username = 'testeditspam'
+        about = u'Test About <a href="http://spamsite.net">spamsite</a>'
+        user = model.User.by_name(unicode(username))
+        if not user:
+            model.Session.add(model.User(name=unicode(username), about=about,
+                                         password='letmein'))
+            model.repo.commit_and_remove()
+            user = model.User.by_name(unicode(username))
+
+        # edit
+        offset = url_for(controller='user', action='edit', id=user.id)
+        res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER':username})
+        main_res = self.main_div(res)
+        assert 'Edit User: ' in main_res, main_res
+        assert 'Test About <a href="http://spamsite.net">spamsite</a>' in main_res, main_res
+        fv = res.forms['user-edit']
+        res = fv.submit('preview', extra_environ={'REMOTE_USER':username})
+        # commit
+        res = fv.submit('save', extra_environ={'REMOTE_USER':username})      
+        assert res.status == 200, res.status
+        main_res = self.main_div(res)
+        assert 'looks like spam' in main_res, main_res
+        assert 'Edit User: ' in main_res, main_res
+
 
     ############
     # Disabled


--- a/ckan/tests/html_check.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/html_check.py	Fri Jun 17 19:19:25 2011 +0100
@@ -41,7 +41,7 @@
         '''Searches in the html and returns True if it can find a particular
         tag and all its subtags & data which contains all the of the
         html_to_find'''
-        named_element_re = re.compile('(<(%(tag)s\w*).*?>.*?</%(tag)s>)' % {'tag':tag_name}) 
+        named_element_re = re.compile('(<(%(tag)s\w*).*?(>.*?</%(tag)s)?>)' % {'tag':tag_name}) 
         html_str = self._get_html_from_res(html)
         self._check_html(named_element_re, html_str.replace('\n', ''), html_to_find)
 
@@ -91,7 +91,11 @@
             if found_all:
                 return # found it
         # didn't find it
-        assert 0, "Couldn't find %s in html. Closest matches were:\n%s" % (', '.join(["'%s'" % html.encode('utf8') for html in html_to_find]), '\n'.join([tag.encode('utf8') for tag in partly_matching_tags]))
+        if partly_matching_tags:
+            assert 0, "Couldn't find %s in html. Closest matches were:\n%s" % (', '.join(["'%s'" % html.encode('utf8') for html in html_to_find]), '\n'.join([tag.encode('utf8') for tag in partly_matching_tags]))
+        else:
+            assert 0, "Couldn't find %s in html. Tags matched were:\n%s" % (', '.join(["'%s'" % html.encode('utf8') for html in html_to_find]), '\n'.join([tag.encode('utf8') for tag in regex_compiled.finditer(html_str)]))
+
 
 
 class Stripper(sgmllib.SGMLParser):


--- a/ckan/tests/lib/test_dictization_schema.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/lib/test_dictization_schema.py	Fri Jun 17 19:19:25 2011 +0100
@@ -132,14 +132,15 @@
         data = group_dictize(group, context)
 
         converted_data, errors = validate(data, default_group_schema(), context)
-        group.packages.sort(key=lambda x:x.id)
+        group_pack = sorted(group.packages, key=lambda x:x.id)
+
         converted_data["packages"] = sorted(converted_data["packages"], key=lambda x:x["id"])
 
         expected = {'description': u'These are books that David likes.',
                                  'id': group.id,
                                  'name': u'david',
-                                 'packages': sorted([{'id': group.packages[0].id},
-                                              {'id': group.packages[1].id,
+                                 'packages': sorted([{'id': group_pack[0].id},
+                                              {'id': group_pack[1].id,
                                                }], key=lambda x:x["id"]),
                                  'title': u"Dave's books"}
 


--- a/ckan/tests/lib/test_package_search.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/lib/test_package_search.py	Fri Jun 17 19:19:25 2011 +0100
@@ -99,6 +99,11 @@
         result = self.backend.query_for(model.Package).run(query=u'Expenditure Government China')
         assert len(result['results']) == 0, self._pkg_names(result)
 
+    def test_3_licence(self):
+        ## this should result, but it is here to check that at least it does not error
+        result = self.backend.query_for(model.Package).run(query=u'license:"OKD::Other (PublicsDomain)"')
+        assert result['count'] == 0, result
+
 # Quotation not supported now
 ##        # multiple words quoted
 ##        result = Search().search(u'"Government Expenditure"')


--- a/ckan/tests/misc/test_format_text.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/misc/test_format_text.py	Fri Jun 17 19:19:25 2011 +0100
@@ -40,9 +40,31 @@
         assert exp in out, '\nGot: %s\nWanted: %s' % (out, exp)
 
     def test_normal_link(self):
-        instr = '<http:/somelink/>'
-        exp = '<a href="http:/somelink/">http:/somelink/</a>'
+        instr = '<http://somelink/>'
+        exp = '<a href="http://somelink/" target="_blank" rel="nofollow">http://somelink/</a>'
         format = MarkdownFormat()
         out = format.to_html(instr)
         assert exp in out, '\nGot: %s\nWanted: %s' % (out, exp)
 
+    def test_malformed_link_1(self):
+        instr = u'<a href=\u201dsomelink\u201d>somelink</a>'
+        exp = '<a href="TAG MALFORMED" target="_blank" rel="nofollow">somelink</a>'
+        format = MarkdownFormat()
+        out = format.to_html(instr)
+        assert exp in out, '\nGot: %s\nWanted: %s' % (out, exp)
+
+    def test_malformed_link_2(self):
+        instr = u'<a href="http://url.com> url >'
+        exp = '<a href="TAG MALFORMED" target="_blank" rel="nofollow"> url >'
+        format = MarkdownFormat()
+        out = format.to_html(instr)
+        assert exp in out, '\nGot: %s\nWanted: %s' % (out, exp)
+
+    def test_malformed_link_3(self):
+        instr = u'<a href="http://url.com"> url'
+        exp = '<a href="http://url.com" target="_blank" rel="nofollow"> url'
+        # NB when this is put into Genshi, it will close the tag for you.
+        format = MarkdownFormat()
+        out = format.to_html(instr)
+        assert exp in out, '\nGot: %s\nWanted: %s' % (out, exp)
+


--- a/ckan/tests/models/test_package.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/models/test_package.py	Fri Jun 17 19:19:25 2011 +0100
@@ -26,6 +26,7 @@
     @classmethod
     def teardown_class(self):
         pkg1 = model.Session.query(model.Package).filter_by(name=self.name).one()
+        
         pkg1.purge()
         model.Session.commit()
         model.repo.rebuild_db()
@@ -394,3 +395,17 @@
         test_res(diff, self.res1, 'hash', 'abc123')
         test_res(diff, self.res1, 'state', 'active')
         test_res(diff, self.res2, 'url', 'http://url2.com')
+
+class TestPackagePurge:
+    @classmethod
+    def setup_class(self):
+        CreateTestData.create()
+    def test_purge(self):
+        pkgs = model.Session.query(model.Package).all()
+        for p in pkgs:
+           p.purge()
+        model.Session.commit()
+        pkgs = model.Session.query(model.Package).all()
+        assert len(pkgs) == 0
+
+


--- a/ckan/tests/pylons_controller.py	Thu Jun 16 17:50:05 2011 +0100
+++ b/ckan/tests/pylons_controller.py	Fri Jun 17 19:19:25 2011 +0100
@@ -12,20 +12,9 @@
 from pylons.controllers.util import Request, Response 
 
 from ckan.tests import *
+from ckan.lib.cli import MockTranslator
 
-class MockTranslator(object): 
-    def gettext(self, value): 
-        return value 
 
-    def ugettext(self, value): 
-        return value 
-
-    def ungettext(self, singular, plural, n):
-        if n > 1:
-            return plural
-        return singular
-
-    
 class PylonsTestCase(object):
     """A basic test case which allows access to pylons.c and pylons.request. 
     """


--- a/doc/configuration.rst	Thu Jun 16 17:50:05 2011 +0100
+++ b/doc/configuration.rst	Fri Jun 17 19:19:25 2011 +0100
@@ -58,15 +58,15 @@
 
 Example::
 
- rdf_packages = http://semantic.ckan.net/package/
+ rdf_packages = http://semantic.ckan.net/record/
 
 Configure this if you have an RDF store of the same packages as are in your CKAN instance. It will provide three sorts of links from each package page to the equivalent RDF URL given in `rdf_packages`:
 
-1. 303 redirects for clients that content-negotiate rdf-xml. e.g. client GETs `http://ckan.net/package/pollution-2008` with accept header `application/rdf+xml`. CKAN's response is a 303 redirect to `http://semantic.ckan.net/package/pollution-2008`
+1. 303 redirects for clients that content-negotiate rdf-xml or turtle. e.g. client GETs `http://ckan.net/package/pollution-2008` with accept header `application/rdf+xml` ``curl -H "Accept: application/rdf+xml" http://ckan.net/package/pollution-2008``. CKAN's response is a 303 redirect to `http://semantic.ckan.net/package/pollution-2008` which can be obtained with: ``curl -L -H "Accept: application/rdf+xml" http://ckan.net/package/pollution-2008``
 
-2. Embedded links for browsers that are aware. e.g. `<link rel="alternate" type="application/rdf+xml" href="http://semantic.ckan.net/package/pollution-2008">`
+2. Embedded links for browsers that are aware. e.g. `<link rel="alternate" type="application/rdf+xml" href="http://semantic.ckan.net/record/b410e678-8a96-40cf-8e46-e8bd4bf02684.rdf">`
 
-3. A visible RDF link on the page in the 'Alternative metadata formats' box. e.g. `<a href="http://semantic.ckan.net/package/pollution-2008">`
+3. A visible RDF link on the page. e.g. `<a href="http://semantic.ckan.net/record/b410e678-8a96-40cf-8e46-e8bd4bf02684.rdf">`
 
 
 cache_validation_enabled
@@ -335,6 +335,7 @@
 API is hosted on a different domain, for example when a third party site uses
 the forms API.
 
+
 default_roles
 -------------
 
@@ -360,3 +361,14 @@
   ckan.plugins = disqus synchronous_search datapreview googleanalytics stats storage admin follower
 
 Specify which CKAN extensions are to be enabled. If you specify an extension but have not installed the code then CKAN will not start. Format in a space separated list of the extension names. The extension name is the key in the [ckan.plugins] section of the extension's setup.py.
+
+
+dumps_url & dumps_format
+------------------------
+
+Example::
+
+  ckan.dumps_url = http://ckan.net/dump/
+  ckan.dumps_format = CSV/JSON
+
+If there is a page which allows you to download a dump of the entire catalogue then specify the URL and the format here, so that it can be advertised in the web interface. The dumps_format is just a string for display.
\ No newline at end of file


--- a/doc/deb.rst	Thu Jun 16 17:50:05 2011 +0100
+++ b/doc/deb.rst	Fri Jun 17 19:19:25 2011 +0100
@@ -425,25 +425,25 @@
 
 A user will follow the following process:
 
-First create the file ``/etc/apt/sources.list.d/okfn.list`` with this line, replacing ``lucid`` with the correct repo you want to use:
+First create the file ``/etc/apt/sources.list.d/okfn.list`` using this command, replacing ``ubuntu_ckan_dev`` with the correct repo you want to use:
 
 ::
 
-    echo "deb http://apt-alpha.ckan.org/lucid lucid universe" | sudo tee /etc/apt/sources.list.d/okfn.list
+    echo "deb http://apt.okfn.org/ubuntu_ckan_dev lucid universe" | sudo tee /etc/apt/sources.list.d/okfn.list
 
 Then add the package key to say you trust packages from this repository:
 
 ::
 
     sudo apt-get install wget
-    wget -qO-  http://apt-alpha.ckan.org/packages.okfn.key | sudo apt-key add -
+    wget -qO-  http://apt.okfn.org/packages.okfn.key | sudo apt-key add -
     sudo apt-get update
 
 Now you can not install a CKAN extension application, just like any other Debian package:
 
 ::
 
-    sudo apt-get install ckan-dgu
+    sudo apt-get install ckan-std
 
 At this point you should have a running instance. You may need to copy across
 an existing database if you need your instance pre-populated with data.
@@ -616,7 +616,7 @@
     python -m buildkit.deb . ckanext-csw 0.3~10 http://ckan.org python-ckanext-harvest python-owslib python-ckan
     python -m buildkit.deb . ckanext-dgu 0.2~11 http://ckan.org python-ckan python-ckanext-importlib python-ckanext-dgu python-ckanext-csw python-ckan python-ckanext-spatial python-ckanext-inspire
     python -m buildkit.deb . ckanext-qa 0.1~19 http://ckan.org python-ckan
-    python -m buildkit.deb . ckan 1.3.4~02 http://ckan.org python-routes python-vdm python-pylons python-genshi python-sqlalchemy python-repoze.who python-repoze.who-plugins python-pyutilib.component.core python-migrate python-formalchemy python-sphinx python-markupsafe python-setuptools python-psycopg2 python-licenses python-ckan-deps
+    python -m buildkit.deb . ckan 1.4~01 http://ckan.org python-routes python-vdm python-pylons python-genshi python-sqlalchemy python-repoze.who python-repoze.who-plugins python-pyutilib.component.core python-migrate python-formalchemy python-sphinx python-markupsafe python-setuptools python-psycopg2 python-licenses python-ckan-deps
 
 There's a dependency on postfix. Choose internet site and the default hostname unless you know better.
 


--- a/doc/deployment.rst	Thu Jun 16 17:50:05 2011 +0100
+++ b/doc/deployment.rst	Fri Jun 17 19:19:25 2011 +0100
@@ -13,7 +13,7 @@
    Package                Notes
    =====================  ============================================
    mercurial              Source control
-   python                 Python interpreter
+   python-dev             Python interpreter v2.5 - v2.7 and dev headers
    apache2                Web server
    libapache2-mod-python  Apache module for python
    libapache2-mod-wsgi    Apache module for WSGI
@@ -25,16 +25,18 @@
    python-libxslt1        Python XSLT library
    libxml2-dev            XML library development files
    libxslt1-dev           XSLT library development files
+   git-core               Git source control (for getting MarkupSafe src)
+   subversion             Subversion source control (for pyutilib)
    =====================  ============================================
 
    Now use easy_install (which comes with python-setuptools) to install
-   these packages:
+   these python packages:
    (e.g. sudo easy_install <package-name>)
 
    =====================  ============================================
-   Package                Notes
+   Python Package         Notes
    =====================  ============================================
-   python-virtualenv      Python virtual environment sandboxing
+   virtualenv             Python virtual environment sandboxing
    pip                    Python installer
    =====================  ============================================
 
@@ -61,7 +63,7 @@
 
   List existing databases::
 
-  $ psql -l
+  $ sudo -u postgres psql -l
 
   It is advisable to ensure that the encoding of databases is 'UTF8', or 
   internationalisation may be a problem. Since changing the encoding of Postgres
@@ -92,7 +94,7 @@
 
 5. Create the Pylons WSGI script
 
-  Create a file ~/var/srvc/demo.ckan.net/pyenv/bin/demp.ckan.net.py as follows (editing the first couple of variables as necessary)::
+  Create a file ~/var/srvc/demo.ckan.net/pyenv/bin/demo.ckan.net.py as follows (editing the first couple of variables as necessary)::
 
     import os
     instance_dir = '/home/USER/var/srvc/demo.ckan.net'
@@ -109,17 +111,19 @@
 
 6. Install code and dependent packages into the environment
 
-  For the most recent stable version use::
+  Decide which release of CKAN you want to install. The CHANGELOG.txt has details on the releases. You'll need the exact tag name, and these are listed on the bitbucket page: https://bitbucket.org/okfn/ckan/src and hover over tags to see the options, e.g. ``ckan-1.4``.
 
-  $ wget https://bitbucket.org/okfn/ckan/raw/default/pip-requirements-metastable.txt
+  $ wget https://bitbucket.org/okfn/ckan/raw/ckan-1.4/pip-requirements.txt
 
   Or for the bleeding edge use::
 
   $ wget https://bitbucket.org/okfn/ckan/raw/default/pip-requirements.txt
 
-  And install::
+  And now install::
 
-  $ pip -E pyenv install -r pip-requirements-metastable.txt 
+  $ pip -E pyenv install -r pip-requirements.txt 
+
+  If everything goes correctly then you'll finally see: ``Successfully installed``.
 
 
 7. Create CKAN config file
@@ -150,7 +154,7 @@
     
   8.3. loggers
      
-    CKAN can make a log file if you change the [loggers] section to this::
+    CKAN can make a log file if you change the ``[loggers]`` section to this::
 
       [loggers]
       keys = root, ckan
@@ -231,5 +235,40 @@
   $ sudo /etc/init.d/apache2 restart
 
 
-14. Browse website at http://demo.ckan.net/
+14. Browse CKAN website at http://demo.ckan.net/ (assuming you have the DNS setup for this server). Should you have problems, take a look at the log files specified in your apache config and ckan oconfig. e.g. ``/var/log/apache2/demo.ckan.net.error.log`` and ``/var/log/ckan/demo.ckan.log``.
 
+
+Upgrade
+=======
+
+Ideally production deployments are upgraded with fabric, but here are the manual instructions.
+
+1. Activate the virtual environment for your install::
+
+   $ cd ~/var/srvc/demo.ckan.net
+   $ . pyenv/bin/activate
+
+2. It's probably wise to backup your database::
+
+   $ paster --plugin=ckan db dump demo_ckan_backup.pg_dump --config=demo.ckan.net.ini
+
+3. Get a version of pip-requirements.txt for the new version you want to install (see info on finding a suitable tag name above)::
+
+   $ wget https://bitbucket.org/okfn/ckan/raw/ckan-1.4/pip-requirements.txt
+
+4. Update all the modules::
+
+   $ pip -E pyenv install -r pip-requirements.txt
+
+5. Upgrade the database::
+
+   $ paster --plugin ckan db upgrade --config {config.ini}
+
+6. Restart apache (so modpython has the latest code)::
+
+   $ sudo /etc/init.d/apache2 restart
+
+7. You could manually try CKAN works in a browser, or better still run the smoke tests found in ckanext/blackbox. To do this, install ckanext and run ckanext from another machine - see ckanext README.txt for instructions: https://bitbucket.org/okfn/ckanext and then run::
+
+   $ python blackbox/smoke.py blackbox/ckan.net.profile.json
+


--- a/doc/plugins.rst	Thu Jun 16 17:50:05 2011 +0100
+++ b/doc/plugins.rst	Fri Jun 17 19:19:25 2011 +0100
@@ -555,6 +555,28 @@
 together with their tests. Over time we hope to move more functionality out
 into CKAN extensions.
 
+Ordering of extensions
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. caution ::
+ 
+  The order that extensions are initially loaded is **different** to the order that their plugins are run.
+
+The order that extensions are initially loaded is as follows:
+
+1. System plugins (in setup.py under ``ckan.system_plugins``).
+
+2. In order of the plugins specified in the config file: ``plugins =``.
+
+3. If more than one module has a plug-in with the same name specified in the config, then all those are loaded, in the order the modules appear in ``sys.path``.
+
+The order that a plugins are run in, for example the order that IRoutes extensions have their ``before_map`` method run, is alphabetical by the plugin class.
+
+e.g. here is the order for these four extensions: ``<Plugin DguInventoryPlugin>``, ``<Plugin FormApiPlugin>``, ``<Plugin StatsPlugin>``, ``<Plugin WalesThemePlugin>``
+
+(This alphabetical ordering is done by ``pyutilib.component.core:ExtensionPoint.extensions()``)
+
+
 Plugin API documentation
 ~~~~~~~~~~~~~~~~~~~~~~~~
 


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pip-requirements-test.txt	Fri Jun 17 19:19:25 2011 +0100
@@ -0,0 +1,3 @@
+# These are packages that required when running ckan tests
+
+-e hg+https://bitbucket.org/okfn/ckanclient#egg=ckanclient


--- a/pip-requirements.txt	Thu Jun 16 17:50:05 2011 +0100
+++ b/pip-requirements.txt	Fri Jun 17 19:19:25 2011 +0100
@@ -5,11 +5,11 @@
 # 
 #     pip install --ignore-installed -r pip-requirements.txt
 
--e hg+https://bitbucket.org/okfn/ckan@default#egg=ckan
+-e hg+https://bitbucket.org/okfn/ckan@release-v1.4.1#egg=ckan
 # CKAN dependencies
--r https://bitbucket.org/okfn/ckan/raw/default/requires/lucid_conflict.txt
--r https://bitbucket.org/okfn/ckan/raw/default/requires/lucid_present.txt
--r https://bitbucket.org/okfn/ckan/raw/default/requires/lucid_missing.txt
+-r https://bitbucket.org/okfn/ckan/raw/release-v1.4.1/requires/lucid_conflict.txt
+-r https://bitbucket.org/okfn/ckan/raw/release-v1.4.1/requires/lucid_present.txt
+-r https://bitbucket.org/okfn/ckan/raw/release-v1.4.1/requires/lucid_missing.txt
 
 # NOTE: Developers, please do not edit this file. Changes should go in the
 #       appropriate files in the `requires' directory.


--- a/requires/lucid_missing.txt	Thu Jun 16 17:50:05 2011 +0100
+++ b/requires/lucid_missing.txt	Fri Jun 17 19:19:25 2011 +0100
@@ -7,12 +7,16 @@
 -e hg+https://bitbucket.org/okfn/licenses@0eed4a13296b#egg=licenses
 # vdm>=0.9,<0.9.99
 -e hg+https://bitbucket.org/okfn/vdm@vdm-0.9#egg=vdm
-# ckanclient>=0.1,<0.7.99
--e hg+https://bitbucket.org/okfn/ckanclient@6756586299cc#egg=ckanclient
 # markupsafe==0.9.2 required by webhelpers==1.2 required by formalchemy with SQLAlchemy 0.6
 -e git+https://github.com/mitsuhiko/markupsafe.git@0.9.2#egg=markupsafe
 # autoneg>=0.5
 -e git+https://github.com/wwaites/autoneg.git@b4c727b164f411cc9d60#egg=autoneg
+# flup>=0.5
+-e hg+http://hg.saddi.com/flup@301a58656bfb#egg=flup
+# All the conflicting dependencies from the lucid_conflict.txt file
+-e hg+https://bitbucket.org/okfn/ckan-deps@6287665a1965#egg=ckan-deps
+# FormAlchemy
+-e git+https://github.com/FormAlchemy/formalchemy.git@1.3.9#egg=formalchemy
 
 # NOTE: Developers, our build script for the Debian packages relies on the 
 #       requirements above being specified as editable resources with their


--- a/requires/lucid_present.txt	Thu Jun 16 17:50:05 2011 +0100
+++ b/requires/lucid_present.txt	Fri Jun 17 19:19:25 2011 +0100
@@ -10,8 +10,6 @@
 lxml==2.2.4
 sphinx==0.6.4
 Pylons==0.9.7
-# We use 1.3.6 but that isn't available, any of these should be fine
-FormAlchemy==1.3.5
 repoze.who==1.0.18
 tempita==0.4
 zope.interface==3.5.3


--- a/test-core.ini	Thu Jun 16 17:50:05 2011 +0100
+++ b/test-core.ini	Fri Jun 17 19:19:25 2011 +0100
@@ -29,7 +29,7 @@
 ckan.site_url = http://test.ckan.net
 package_new_return_url = http://localhost/package/<NAME>?test=new
 package_edit_return_url = http://localhost/package/<NAME>?test=edit
-
+rdf_packages = http://test.com/package/
 ckan.extra_resource_fields = alt_url
 
 # disable this so we can test all types of indexing


http://bitbucket.org/okfn/ckan/changeset/669215024721/
changeset:   669215024721
branch:      feature-1141-moderated-edits-ajax
user:        kindly
date:        2011-06-18 00:52:39
summary:     [merge] branch
affected #:  1 file (0 bytes)

Repository URL: https://bitbucket.org/okfn/ckan/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.




More information about the ckan-changes mailing list