[ckan-changes] commit/ckan: 5 new changesets
Bitbucket
commits-noreply at bitbucket.org
Thu Jul 28 12:16:28 UTC 2011
5 new changesets in ckan:
http://bitbucket.org/okfn/ckan/changeset/c7588fc1b8f8/
changeset: c7588fc1b8f8
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-27 17:36:03
summary: Put datetime conversion on a helper function
affected #: 3 files (199 bytes)
--- a/ckan/controllers/group.py Wed Jul 27 13:14:38 2011 +0100
+++ b/ckan/controllers/group.py Wed Jul 27 16:36:03 2011 +0100
@@ -441,7 +441,7 @@
language=unicode(get_lang()),
)
for revision_dict in c.group_revisions:
- revision_date = datetime.datetime.strptime(revision_dict['timestamp'], '%Y-%m-%dT%H:%M:%S.%f')
+ revision_date = h.date_str_to_datetime(revision_dict['timestamp'])
try:
dayHorizon = int(request.params.get('days'))
except:
--- a/ckan/controllers/package.py Wed Jul 27 13:14:38 2011 +0100
+++ b/ckan/controllers/package.py Wed Jul 27 16:36:03 2011 +0100
@@ -288,7 +288,7 @@
language=unicode(get_lang()),
)
for revision_dict in c.pkg_revisions:
- revision_date = datetime.datetime.strptime(revision_dict['timestamp'], '%Y-%m-%dT%H:%M:%S.%f')
+ revision_date = h.date_str_to_datetime(revision_dict['timestamp'])
try:
dayHorizon = int(request.params.get('days'))
except:
--- a/ckan/lib/helpers.py Wed Jul 27 13:14:38 2011 +0100
+++ b/ckan/lib/helpers.py Wed Jul 27 16:36:03 2011 +0100
@@ -29,6 +29,7 @@
except ImportError:
import simplejson as json
+ISO_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
class Message(object):
"""A message returned by ``Flash.pop_messages()``.
@@ -210,5 +211,8 @@
else:
return ''
-def time_ago_in_words_from_str(date_str, format='%Y-%m-%dT%H:%M:%S.%f',granularity='month'):
+def date_str_to_datetime(date_str, format=ISO_DATE_FORMAT):
+ return datetime.strptime(date_str, format)
+
+def time_ago_in_words_from_str(date_str, format=ISO_DATE_FORMAT, granularity='month'):
return date.time_ago_in_words(datetime.strptime(date_str, format), granularity=granularity)
http://bitbucket.org/okfn/ckan/changeset/b42c18503c84/
changeset: b42c18503c84
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-27 17:37:00
summary: Fix typo
affected #: 3 files (0 bytes)
--- a/ckan/controllers/package.py Wed Jul 27 16:36:03 2011 +0100
+++ b/ckan/controllers/package.py Wed Jul 27 16:37:00 2011 +0100
@@ -77,7 +77,7 @@
raise DataError(data_dict)
def _setup_template_variables(self, context, data_dict):
- c.groups = get.group_list_availible(context, data_dict)
+ c.groups = get.group_list_available(context, data_dict)
c.groups_authz = get.group_list_authz(context, data_dict)
c.licences = [('', '')] + model.Package.get_license_options()
c.is_sysadmin = Authorizer().is_sysadmin(c.user)
--- a/ckan/lib/navl/dictization_functions.py Wed Jul 27 16:36:03 2011 +0100
+++ b/ckan/lib/navl/dictization_functions.py Wed Jul 27 16:37:00 2011 +0100
@@ -81,7 +81,7 @@
def make_full_schema(data, schema):
'''make schema by getting all valid combinations and making sure that all keys
- are availible'''
+ are available'''
flattented_schema = flatten_schema(schema)
--- a/ckan/logic/action/get.py Wed Jul 27 16:36:03 2011 +0100
+++ b/ckan/logic/action/get.py Wed Jul 27 16:37:00 2011 +0100
@@ -119,7 +119,7 @@
groups = set(query.all())
return dict((group.id, group.name) for group in groups)
-def group_list_availible(context, data_dict):
+def group_list_available(context, data_dict):
model = context['model']
user = context['user']
pkg = context.get('package')
http://bitbucket.org/okfn/ckan/changeset/1f9081f6dd6c/
changeset: 1f9081f6dd6c
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-28 13:21:47
summary: [merge] from default
affected #: 46 files (33.7 KB)
--- a/CHANGELOG.txt Wed Jul 27 16:37:00 2011 +0100
+++ b/CHANGELOG.txt Thu Jul 28 12:21:47 2011 +0100
@@ -5,7 +5,19 @@
=================
Major:
* Packages revisions can be marked as 'moderated' (#1141)
+ * Password reset facility (#1186/#1198)
+
+Minor:
* Viewing of a package at any revision (#1141)
+ * API POSTs can be of Content-Type "application/json" as alternative to existing "application/x-www-form-urlencoded" (#1206)
+ * Caching of static files (#1223)
+
+Bug fixes:
+ * When you removed last row of resource table, you could't add it again - since 1.0 (#1215)
+ * Exception if you had any Groups and migrated between CKAN v1.0.2 to v1.2 (migration 29) - since v1.0.2 (#1205)
+ * API Package edit requests returned the Package in a different format to usual - since 1.4 (#1214)
+ * API error responses were not all JSON format and didn't have correct Content-Type (#1214)
+ * API package delete doesn't require a Content-Length header (#1214)
v1.4.1 2011-06-27
--- a/README.txt Wed Jul 27 16:37:00 2011 +0100
+++ b/README.txt Thu Jul 28 12:21:47 2011 +0100
@@ -371,6 +371,11 @@
python -c "import pylons"
+* `OperationalError: (OperationalError) no such function: plainto_tsquery ...`
+
+ This error usually results from running a test which involves search functionality, which requires using a PostgreSQL database, but another (such as SQLite) is configured. The particular test is either missing a `@search_related` decorator or there is a mixup with the test configuration files leading to the wrong database being used.
+
+
Testing extensions
------------------
--- a/ckan/ckan_nose_plugin.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/ckan_nose_plugin.py Thu Jul 28 12:21:47 2011 +0100
@@ -4,8 +4,7 @@
import sys
import pkg_resources
from paste.deploy import loadapp
-
-pylonsapp = None
+from pylons import config
class CkanNose(Plugin):
settings = None
@@ -29,6 +28,15 @@
# the db is destroyed after every test when you Session.Remove().
model.repo.init_db()
+ ## This is to make sure the configuration is run again.
+ ## Plugins use configure to make their own tables and they
+ ## may need to be recreated to make tests work.
+ from ckan.plugins import PluginImplementations
+ from ckan.plugins.interfaces import IConfigurable
+ for plugin in PluginImplementations(IConfigurable):
+ plugin.configure(config)
+
+
def options(self, parser, env):
parser.add_option(
'--ckan',
--- a/ckan/controllers/api.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/controllers/api.py Thu Jul 28 12:21:47 2011 +0100
@@ -11,6 +11,7 @@
from ckan.plugins import PluginImplementations, IGroupController
from ckan.lib.munge import munge_title_to_name
from ckan.lib.navl.dictization_functions import DataError
+from ckan.logic import get_action
import ckan.logic.action.get as get
import ckan.logic.action.create as create
import ckan.logic.action.update as update
@@ -129,32 +130,9 @@
response_data = {}
response_data['version'] = ver or '1'
return self._finish_ok(response_data)
-
- @classmethod
- def create_actions(cls):
- if cls._actions:
- return
- for name, action in get.__dict__.iteritems():
- if not name.startswith('_') and callable(action):
- cls._actions[name] = action
- for name, action in update.__dict__.iteritems():
- if not name.startswith('_') and callable(action):
- cls._actions[name] = action
- for name, action in create.__dict__.iteritems():
- if not name.startswith('_') and callable(action):
- cls._actions[name] = action
-
- def get_action(self, action):
- self.create_actions()
- return self._actions[action]
-
- @classmethod
- def register_action(cls, name, function):
- cls.create_actions()
- cls._actions[name] = function
def action(self, logic_function):
- function = self.get_action(logic_function)
+ function = get_action(logic_function)
context = {'model': model, 'session': model.Session, 'user': c.user}
model.Session()._context = context
@@ -259,10 +237,10 @@
def create(self, ver=None, register=None, subregister=None, id=None, id2=None):
action_map = {
- ('package', 'relationships'): create.package_relationship_create,
- 'group': create.group_create_rest,
- 'package': create.package_create_rest,
- 'rating': create.rating_create,
+ ('package', 'relationships'): get_action('package_relationship_create'),
+ 'group': get_action('group_create_rest'),
+ 'package': get_action('package_create_rest'),
+ 'rating': get_action('rating_create'),
}
for type in model.PackageRelationship.get_all_types():
@@ -309,9 +287,9 @@
def update(self, ver=None, register=None, subregister=None, id=None, id2=None):
action_map = {
- ('package', 'relationships'): update.package_relationship_update,
- 'package': update.package_update_rest,
- 'group': update.group_update_rest,
+ ('package', 'relationships'): get_action('package_relationship_update'),
+ 'package': get_action('package_update_rest'),
+ 'group': get_action('group_update_rest'),
}
for type in model.PackageRelationship.get_all_types():
action_map[('package', type)] = update.package_relationship_update
--- a/ckan/controllers/home.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/controllers/home.py Thu Jul 28 12:21:47 2011 +0100
@@ -43,7 +43,8 @@
query = query_for(model.Package)
query.run(query='*:*', facet_by=g.facets,
- limit=0, offset=0, username=c.user)
+ limit=0, offset=0, username=c.user,
+ order_by=None)
c.facets = query.facets
c.fields = []
c.package_count = query.count
@@ -74,7 +75,7 @@
abort(400, _('Invalid language specified'))
h.flash_notice(_("Language has been set to: English"))
else:
- h.flash_notice(_("No language given!"))
+ abort(400, _("No language given!"))
return_to = get_redirect()
if not return_to:
# no need for error, just don't redirect
--- a/ckan/controllers/package.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/controllers/package.py Thu Jul 28 12:21:47 2011 +0100
@@ -15,6 +15,7 @@
import ckan.logic.action.create as create
import ckan.logic.action.update as update
import ckan.logic.action.get as get
+from ckan.logic import get_action
from ckan.logic.schema import package_form_schema
from ckan.lib.base import request, c, BaseController, model, abort, h, g, render
from ckan.lib.base import etag_cache, response, redirect, gettext
@@ -173,24 +174,30 @@
# revision may have more than one package in it.
return str(hash((pkg.id, pkg.latest_related_revision.id, c.user, pkg.get_average_rating())))
- def _clear_pkg_cache(self, pkg):
- read_cache = cache.get_cache('package/read.html', type='dbm')
- read_cache.remove_value(self._pkg_cache_key(pkg))
-
@proxy_cache()
def read(self, id):
context = {'model': model, 'session': model.Session,
'user': c.user or c.author, 'extras_as_string': True,
'schema': self._form_to_db_schema()}
data_dict = {'id': id}
+
+ # interpret @<revision_id> or @<date> suffix
split = id.split('@')
if len(split) == 2:
- data_dict['id'], revision = split
- try:
- date = datetime.datetime(*map(int, re.split('[^\d]', revision)))
- context['revision_date'] = date
- except ValueError:
- context['revision_id'] = revision
+ data_dict['id'], revision_ref = split
+ if model.is_id(revision_ref):
+ context['revision_id'] = revision_ref
+ else:
+ try:
+ date = model.strptimestamp(revision_ref)
+ context['revision_date'] = date
+ except TypeError, e:
+ abort(400, _('Invalid revision format: %r') % e.args)
+ except ValueError, e:
+ abort(400, _('Invalid revision format: %r') % e.args)
+ elif len(split) > 2:
+ abort(400, _('Invalid revision format: %r') % 'Too many "@" symbols')
+
#check if package exists
try:
c.pkg_dict = get.package_show(context, data_dict)
@@ -437,7 +444,7 @@
tuplize_dict(parse_params(request.POST))))
self._check_data_dict(data_dict)
context['message'] = data_dict.get('log_message', '')
- pkg = create.package_create(context, data_dict)
+ pkg = get_action('package_create')(context, data_dict)
if context['preview']:
PackageSaver().render_package(pkg, context)
@@ -468,7 +475,7 @@
if not context['moderated']:
context['pending'] = False
data_dict['id'] = id
- pkg = update.package_update(context, data_dict)
+ pkg = get_action('package_update')(context, data_dict)
if request.params.get('save', '') == 'Approve':
update.make_latest_pending_package_active(context, data_dict)
c.pkg = context['package']
@@ -522,6 +529,7 @@
pkg = model.Package.get(id)
if pkg is None:
abort(404, gettext('Package not found'))
+ c.pkg = pkg # needed to add in the tab bar to the top of the auth page
c.pkgname = pkg.name
c.pkgtitle = pkg.title
--- a/ckan/controllers/user.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/controllers/user.py Thu Jul 28 12:21:47 2011 +0100
@@ -216,6 +216,8 @@
def login(self):
+ if 'error' in request.params:
+ h.flash_error(request.params['error'])
return render('user/login.html')
def logged_in(self):
--- a/ckan/lib/dictization/model_dictize.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py Thu Jul 28 12:21:47 2011 +0100
@@ -78,7 +78,11 @@
return resource
def _execute_with_revision(q, rev_table, context):
+ '''
+ Raises NotFound if the context['revision_id'] does not exist.
+ Returns [] if there are no results.
+ '''
model = context['model']
meta = model.meta
session = model.Session
@@ -87,8 +91,11 @@
pending = context.get('pending')
if revision_id:
- revision_date = session.query(context['model'].Revision).filter_by(
- id=revision_id).one().timestamp
+ revision = session.query(context['model'].Revision).filter_by(
+ id=revision_id).first()
+ if not revision:
+ raise NotFound
+ revision_date = revision.timestamp
if revision_date:
q = q.where(rev_table.c.revision_timestamp <= revision_date)
--- a/ckan/lib/dictization/model_save.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/lib/dictization/model_save.py Thu Jul 28 12:21:47 2011 +0100
@@ -5,17 +5,15 @@
##package saving
def resource_dict_save(res_dict, context):
-
model = context["model"]
session = context["session"]
- obj = None
-
+ # try to get resource object directly from context, then by ID
+ # if not found, create a new resource object
id = res_dict.get("id")
-
- if id:
+ obj = context.get("resource")
+ if (not obj) and id:
obj = session.query(model.Resource).get(id)
-
if not obj:
obj = model.Resource()
@@ -30,14 +28,17 @@
if key in fields:
setattr(obj, key, value)
else:
+ # resources save extras directly onto the object, instead
+ # of in a separate extras field like packages and groups
obj.extras[key] = value
if context.get('pending'):
if session.is_modified(obj, include_collections=False):
- obj.state = 'pending'
+ obj.state = u'pending'
+ else:
+ obj.state = u'active'
session.add(obj)
-
return obj
def package_resource_list_save(res_dicts, package, context):
@@ -126,7 +127,6 @@
return result_dict
def package_tag_list_save(tag_dicts, package, context):
-
allow_partial_update = context.get("allow_partial_update", False)
if not tag_dicts and allow_partial_update:
--- a/ckan/lib/helpers.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/lib/helpers.py Thu Jul 28 12:21:47 2011 +0100
@@ -204,10 +204,22 @@
def render_datetime(datetime_):
- '''Render a datetime object as a string in a reasonable way (Y-m-d H:m).
+ '''Render a datetime object or timestamp string as a pretty string
+ (Y-m-d H:m).
+ If timestamp is badly formatted, then a blank string is returned.
'''
- if datetime_:
- return datetime_.strftime('%Y-%m-%d %H:%M')
+ from ckan import model
+ date_format = '%Y-%m-%d %H:%M'
+ if isinstance(datetime_, datetime):
+ return datetime_.strftime(date_format)
+ elif isinstance(datetime_, basestring):
+ try:
+ datetime_ = model.strptimestamp(datetime_)
+ except TypeError:
+ return ''
+ except ValueError:
+ return ''
+ return datetime_.strftime(date_format)
else:
return ''
--- a/ckan/lib/mailer.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/lib/mailer.py Thu Jul 28 12:21:47 2011 +0100
@@ -17,13 +17,16 @@
class MailerException(Exception):
pass
+def add_msg_niceties(recipient_name, body, sender_name, sender_url):
+ return _(u"Dear %s,") % recipient_name \
+ + u"\r\n\r\n%s\r\n\r\n" % body \
+ + u"--\r\n%s (%s)" % (sender_name, sender_url)
+
def _mail_recipient(recipient_name, recipient_email,
sender_name, sender_url, subject,
body, headers={}):
mail_from = config.get('ckan.mail_from')
- body = _(u"Dear %s,") % recipient_name \
- + u"\r\n\r\n%s\r\n\r\n" % body \
- + u"--\r\n%s (%s)" % (sender_name, sender_url)
+ body = add_msg_niceties(recipient_name, body, sender_name, sender_url)
msg = MIMEText(body.encode('utf-8'), 'plain', 'utf-8')
for k, v in headers.items(): msg[k] = v
subject = Header(subject.encode('utf-8'), 'utf-8')
@@ -34,7 +37,9 @@
msg['Date'] = Utils.formatdate(time())
msg['X-Mailer'] = "CKAN %s" % __version__
try:
- server = smtplib.SMTP(config.get('smtp_server', 'localhost'))
+ server = smtplib.SMTP(
+ config.get('test_smtp_server',
+ config.get('smtp_server', 'localhost')))
#server.set_debuglevel(1)
server.sendmail(mail_from, [recipient_email], msg.as_string())
server.quit()
@@ -48,15 +53,12 @@
g.site_title, g.site_url, subject, body, headers=headers)
def mail_user(recipient, subject, body, headers={}):
- if (recipient.email is None) and len(recipient.email):
+ if (recipient.email is None) or not len(recipient.email):
raise MailerException(_("No recipient email address available!"))
mail_recipient(recipient.display_name, recipient.email, subject,
body, headers=headers)
-def make_key():
- return uuid.uuid4().hex[:10]
-
RESET_LINK_MESSAGE = _(
'''You have requested your password on %(site_title)s to be reset.
@@ -65,16 +67,30 @@
%(reset_link)s
''')
-def send_reset_link(user):
- user.reset_key = make_key()
- model.Session.add(user)
- model.Session.commit()
+def make_key():
+ return uuid.uuid4().hex[:10]
+
+def create_reset_key(user):
+ user.reset_key = unicode(make_key())
+ model.repo.commit_and_remove()
+
+def get_reset_link(user):
+ return urljoin(g.site_url,
+ url_for(controller='user',
+ action='perform_reset',
+ id=user.id,
+ key=user.reset_key))
+
+def get_reset_link_body(user):
d = {
- 'reset_link': urljoin(g.site_url, url_for(controller='user',
- action='perform_reset', id=user.id, key=user.reset_key)),
+ 'reset_link': get_reset_link(user),
'site_title': g.site_title
}
- body = RESET_LINK_MESSAGE % d
+ return RESET_LINK_MESSAGE % d
+
+def send_reset_link(user):
+ create_reset_key(user)
+ body = get_reset_link_body(user)
mail_user(user, _('Reset your password'), body)
def verify_reset_link(user, key):
--- a/ckan/lib/package_saver.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/lib/package_saver.py Thu Jul 28 12:21:47 2011 +0100
@@ -62,6 +62,11 @@
if isinstance(v, (list, tuple)):
v = ", ".join(map(unicode, v))
c.pkg_extras.append((k, v))
+ if context.get('revision_id') or context.get('revision_date'):
+ # request was for a specific revision id or date
+ c.pkg_revision_id = c.pkg_dict[u'revision_id']
+ c.pkg_revision_timestamp = c.pkg_dict[u'revision_timestamp']
+ c.pkg_revision_not_latest = c.pkg_dict[u'revision_id'] != c.pkg.revision.id
@classmethod
def _preview_pkg(cls, fs, log_message=None, author=None, client=None):
--- a/ckan/logic/__init__.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/logic/__init__.py Thu Jul 28 12:21:47 2011 +0100
@@ -1,6 +1,8 @@
import logging
import ckan.authz
from ckan.lib.navl.dictization_functions import flatten_dict
+from ckan.plugins import PluginImplementations
+from ckan.plugins.interfaces import IActions
class ActionError(Exception):
def __init__(self, extra_msg=None):
@@ -90,4 +92,42 @@
log.debug("No valid API key provided.")
raise NotAuthorized
log.debug("Access OK.")
- return True
+ return True
+
+_actions = {}
+
+def get_action(action):
+ if _actions:
+ return _actions.get(action)
+ # Otherwise look in all the plugins to resolve all possible
+ # First get the default ones in the ckan/logic/action directory
+ # Rather than writing them out in full will use __import__
+ # to load anything from ckan.logic.action that looks like it might
+ # be an action
+ for action_module_name in ['get', 'create', 'update']:
+ module_path = 'ckan.logic.action.'+action_module_name
+ module = __import__(module_path)
+ for part in module_path.split('.')[1:]:
+ module = getattr(module, part)
+ for k, v in module.__dict__.items():
+ if not k.startswith('_'):
+ _actions[k] = v
+ # Then overwrite them with any specific ones in the plugins:
+ resolved_action_plugins = {}
+ fetched_actions = {}
+ for plugin in PluginImplementations(IActions):
+ for name, auth_function in plugin.get_actions().items():
+ if name in resolved_action_plugins:
+ raise Exception(
+ 'The action %r is already implemented in %r' % (
+ name,
+ resolved_action_plugins[name]
+ )
+ )
+ log.debug('Auth function %r was inserted', plugin.name)
+ resolved_action_plugins[name] = plugin.name
+ fetched_actions[name] = auth_function
+ # Use the updated ones in preference to the originals.
+ _actions.update(fetched_actions)
+ return _actions.get(action)
+
--- a/ckan/logic/action/create.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/logic/action/create.py Thu Jul 28 12:21:47 2011 +0100
@@ -79,6 +79,25 @@
else:
return data
+def package_create_validate(context, data_dict):
+ model = context['model']
+ user = context['user']
+ preview = context.get('preview', False)
+ schema = context.get('schema') or default_create_package_schema()
+ model.Session.remove()
+ model.Session()._context = context
+
+ check_access(model.System(), model.Action.PACKAGE_CREATE, context)
+ check_group_auth(context, data_dict)
+
+ data, errors = validate(data_dict, schema, context)
+
+ if errors:
+ model.Session.rollback()
+ raise ValidationError(errors, package_error_summary(errors))
+ else:
+ return data
+
def resource_create(context, data_dict):
model = context['model']
user = context['user']
--- a/ckan/logic/action/get.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/logic/action/get.py Thu Jul 28 12:21:47 2011 +0100
@@ -2,6 +2,7 @@
from sqlalchemy import or_, func, desc
from ckan.logic import NotFound, check_access
+from ckan.model import Session
from ckan.plugins import (PluginImplementations,
IGroupController,
IPackageController)
@@ -30,7 +31,9 @@
api = context.get("api_version", '1')
ref_package_by = 'id' if api == '2' else 'name'
- query = ckan.authz.Authorizer().authorized_query(user, model.Package)
+ query = Session.query(model.PackageRevision)
+ query = query.filter(model.PackageRevision.state=='active')
+ query = query.filter(model.PackageRevision.current==True)
packages = query.all()
return [getattr(p, ref_package_by) for p in packages]
@@ -39,7 +42,7 @@
user = context["user"]
limit = data_dict.get("limit")
- q = ckan.authz.Authorizer().authorized_query(user, model.PackageRevision)
+ q = Session.query(model.PackageRevision)
q = q.filter(model.PackageRevision.state=='active')
q = q.filter(model.PackageRevision.current==True)
@@ -59,8 +62,12 @@
result_dict["resources"] = resource_list_dictize(result, context)
license_id = result_dict['license_id']
if license_id:
- isopen = model.Package.get_license_register()[license_id].isopen()
- result_dict['isopen'] = isopen
+ try:
+ isopen = model.Package.get_license_register()[license_id].isopen()
+ result_dict['isopen'] = isopen
+ except KeyError:
+ # TODO: create a log message this error?
+ result_dict['isopen'] = False
else:
result_dict['isopen'] = False
package_list.append(result_dict)
--- a/ckan/logic/action/update.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/logic/action/update.py Thu Jul 28 12:21:47 2011 +0100
@@ -9,6 +9,7 @@
from ckan.lib.dictization.model_dictize import (package_dictize,
package_to_api1,
package_to_api2,
+ resource_dictize,
group_dictize,
group_to_api1,
group_to_api2,
@@ -16,11 +17,13 @@
from ckan.lib.dictization.model_save import (group_api_to_dict,
package_api_to_dict,
group_dict_save,
+ user_dict_save,
package_dict_save,
- user_dict_save)
+ resource_dict_save)
from ckan.logic.schema import (default_update_group_schema,
default_update_package_schema,
- default_update_user_schema)
+ default_update_user_schema,
+ default_update_resource_schema)
from ckan.lib.navl.dictization_functions import validate
log = logging.getLogger(__name__)
@@ -42,6 +45,18 @@
error_summary[_(prettify(key))] = error[0]
return error_summary
+def resource_error_summary(error_dict):
+
+ error_summary = {}
+ for key, error in error_dict.iteritems():
+ if key == 'extras':
+ error_summary[_('Extras')] = _('Missing Value')
+ elif key == 'extras_validation':
+ error_summary[_('Extras')] = error[0]
+ else:
+ error_summary[_(prettify(key))] = error[0]
+ return error_summary
+
def group_error_summary(error_dict):
error_summary = {}
@@ -149,6 +164,50 @@
session.remove()
+def resource_update(context, data_dict):
+ model = context['model']
+ session = context['session']
+ user = context['user']
+ id = context["id"]
+ schema = context.get('schema') or default_update_resource_schema()
+ model.Session.remove()
+
+ resource = model.Resource.get(id)
+ context["resource"] = resource
+
+ if not resource:
+ raise NotFound(_('Resource was not found.'))
+ context["id"] = resource.id
+
+ # TODO: can check_access be used against a resource?
+ query = session.query(model.Package
+ ).join(model.ResourceGroup
+ ).join(model.Resource
+ ).filter(model.ResourceGroup.id == resource.resource_group_id)
+ pkg = query.first()
+ if not pkg:
+ raise NotFound(_('No package found for this resource, cannot check auth.'))
+
+ check_access(pkg, model.Action.EDIT, context)
+
+ data, errors = validate(data_dict, schema, context)
+
+ if errors:
+ model.Session.rollback()
+ raise ValidationError(errors, resource_error_summary(errors))
+
+ rev = model.repo.new_revision()
+ rev.author = user
+ if 'message' in context:
+ rev.message = context['message']
+ else:
+ rev.message = _(u'REST API: Update object %s') % data.get("name")
+
+ resource = resource_dict_save(data, context)
+ model.repo.commit()
+ return resource_dictize(resource, context)
+
+
def package_update(context, data_dict):
model = context['model']
user = context['user']
@@ -193,6 +252,31 @@
return package_dictize(pkg, context)
return data
+def package_update_validate(context, data_dict):
+ model = context['model']
+ user = context['user']
+
+ id = data_dict["id"]
+ preview = context.get('preview', False)
+ schema = context.get('schema') or default_update_package_schema()
+ model.Session.remove()
+ model.Session()._context = context
+
+ pkg = model.Package.get(id)
+ context["package"] = pkg
+
+ if pkg is None:
+ raise NotFound(_('Package was not found.'))
+ data_dict["id"] = pkg.id
+
+ check_access(pkg, model.Action.EDIT, context)
+ data, errors = validate(data_dict, schema, context)
+
+ if errors:
+ model.Session.rollback()
+ raise ValidationError(errors, package_error_summary(errors))
+ return data
+
def _update_package_relationship(relationship, comment, context):
model = context['model']
@@ -294,7 +378,7 @@
raise NotFound('User was not found.')
if not (ckan.authz.Authorizer().is_sysadmin(unicode(user)) or user == user_obj.name) and \
- not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj['reset_key']):
+ not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key):
raise NotAuthorized( _('User %s not authorized to edit %s') % (str(user), id))
data, errors = validate(data_dict, schema, context)
--- a/ckan/logic/schema.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/logic/schema.py Thu Jul 28 12:21:47 2011 +0100
@@ -50,6 +50,10 @@
return schema
+def default_update_resource_schema():
+ schema = default_resource_schema()
+ return schema
+
def default_tags_schema():
schema = {
--- a/ckan/migration/versions/039_add_expired_id_and_dates.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/migration/versions/039_add_expired_id_and_dates.py Thu Jul 28 12:21:47 2011 +0100
@@ -28,7 +28,7 @@
insert into package_revision (id,name,title,url,notes,license_id,revision_id,version,author,author_email,maintainer,maintainer_email,state,continuity_id) select id,name,title,url,notes,license_id, '%(id)s',version,author,author_email,maintainer,maintainer_email,state, id from package where package.id not in (select id from package_revision);
-''' % dict(id=id, timestamp=datetime.datetime.now().isoformat())
+''' % dict(id=id, timestamp=datetime.datetime.utcnow().isoformat())
update_schema = '''
--- a/ckan/model/__init__.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/model/__init__.py Thu Jul 28 12:21:47 2011 +0100
@@ -220,10 +220,20 @@
Revision.user = property(_get_revision_user)
def strptimestamp(s):
+ '''Convert a string of an ISO date into a datetime.datetime object.
+
+ raises TypeError if the number of numbers in the string is not between 3
+ and 7 (see datetime constructor).
+ raises ValueError if any of the numbers are out of range.
+ '''
+
import datetime, re
return datetime.datetime(*map(int, re.split('[^\d]', s)))
def strftimestamp(t):
+ '''Takes a datetime.datetime and returns it as an ISO string. For
+ a pretty printed string, use ckan.lib.helpers.render_datetime.
+ '''
return t.isoformat()
def revision_as_dict(revision, include_packages=True, include_groups=True,ref_package_by='name'):
@@ -242,3 +252,8 @@
for grp in revision.groups]
return revision_dict
+
+def is_id(id_string):
+ '''Tells the client if the string looks like a revision id or not'''
+ import re
+ return bool(re.match('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', id_string))
--- a/ckan/model/resource.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/model/resource.py Thu Jul 28 12:21:47 2011 +0100
@@ -81,6 +81,15 @@
if self.resource_group and not core_columns_only:
_dict["package_id"] = self.resource_group.package_id
return _dict
+
+ @classmethod
+ def get(cls, reference):
+ '''Returns a resource object referenced by its id.'''
+ query = Session.query(ResourceRevision).filter(ResourceRevision.id==reference)
+ query = query.filter(and_(
+ ResourceRevision.state == u'active', ResourceRevision.current == True
+ ))
+ return query.first()
@classmethod
def get_columns(cls, extra_columns=True):
--- a/ckan/model/tag.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/model/tag.py Thu Jul 28 12:21:47 2011 +0100
@@ -1,4 +1,5 @@
from sqlalchemy.orm import eagerload_all
+from sqlalchemy import and_
import vdm.sqlalchemy
from types import make_uuid
@@ -60,15 +61,21 @@
@classmethod
def all(cls):
q = Session.query(cls)
- q = q.distinct().join(cls.package_tags)
- q = q.filter(PackageTag.state == 'active')
+ q = q.distinct().join(PackageTagRevision)
+ q = q.filter(and_(
+ PackageTagRevision.state == 'active', PackageTagRevision.current == True
+ ))
return q
@property
def packages_ordered(self):
- ## make sure packages are active
- packages = [package for package in self.packages
- if package.state == State.ACTIVE]
+ q = Session.query(Package)
+ q = q.join(PackageTagRevision)
+ q = q.filter(PackageTagRevision.tag_id == self.id)
+ q = q.filter(and_(
+ PackageTagRevision.state == 'active', PackageTagRevision.current == True
+ ))
+ packages = [p for p in q]
ourcmp = lambda pkg1, pkg2: cmp(pkg1.name, pkg2.name)
return sorted(packages, cmp=ourcmp)
--- a/ckan/plugins/interfaces.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/plugins/interfaces.py Thu Jul 28 12:21:47 2011 +0100
@@ -10,7 +10,8 @@
'IMiddleware',
'IDomainObjectModification', 'IGroupController',
'IPackageController', 'IPluginObserver',
- 'IConfigurable', 'IConfigurer', 'IAuthorizer'
+ 'IConfigurable', 'IConfigurer', 'IAuthorizer',
+ 'IActions'
]
from inspect import isclass
@@ -300,4 +301,12 @@
other Authorizers to run; True will shortcircuit and return.
"""
-
+class IActions(Interface):
+ """
+ Allow adding of actions to the logic layer.
+ """
+ def get_actions(self):
+ """
+ Should return a dict, the keys being the name of the logic
+ function and the values being the functions themselves.
+ """
--- a/ckan/public/css/ckan.css Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/public/css/ckan.css Thu Jul 28 12:21:47 2011 +0100
@@ -357,6 +357,13 @@
display: inline-block;
margin-top: 0;
margin-right: 10px;
+ /*
+ * IE 6 & 7 don't support inline-block, but we can use the hasLayout
+ * magical property.
+ * http://blog.mozilla.com/webdev/2009/02/20/cross-browser-inline-block/
+ */
+ zoom: 1;
+ *display: inline;
}
#footer-widget-area .widget-container .textwidget {
@@ -377,6 +384,13 @@
margin: 0 1em 0 0;
padding: 0;
display: inline-block;
+ /*
+ * IE 6 & 7 don't support inline-block, but we can use the hasLayout
+ * magical property.
+ * http://blog.mozilla.com/webdev/2009/02/20/cross-browser-inline-block/
+ */
+ zoom: 1;
+ *display: inline;
}
#footer-widget-area #fourth {
@@ -943,6 +957,20 @@
float: none;
}
+#revision.widget-container
+{
+ background: #f9f2ce;
+ color: #333;
+ margin: 0 0 1em 0;
+ padding: 10px;
+ border: 1px solid #ebd897;
+ border-left: none;
+ border-top: none;
+ border-radius: 0.5em;
+ -moz-border-radius: 0.5em;
+ -webkit-border-radius: 0.5em;
+}
+
/* ===================== */
/* = User Listing = */
/* ===================== */
Binary file ckan/public/images/icons/page_stack.png has changed
--- a/ckan/templates/_util.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/_util.html Thu Jul 28 12:21:47 2011 +0100
@@ -269,8 +269,8 @@
</py:for></table>
-<!-- Copy and paste of above table. Only difference when created was the h.linked_user for the -->
-<!-- table rows. How to combine the two? -->
+<!--! Copy and paste of above table. Only difference when created was the h.linked_user for the -->
+<!--! table rows. How to combine the two? --><table py:def="authz_form_group_table(id, roles, users, user_role_dict)"><tr><th>User Group</th>
@@ -326,7 +326,7 @@
</tr></table>
- <!-- again, copy-and-paste of above, this time to attach different autocompletion -->
+ <!--! again, copy-and-paste of above, this time to attach different autocompletion --><table py:def="authz_add_group_table(roles)"><tr><th>User Group</th>
--- a/ckan/templates/layout_base.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/layout_base.html Thu Jul 28 12:21:47 2011 +0100
@@ -30,7 +30,7 @@
<![endif]--><script type="text/javascript" src="${g.site_url}/language.js"></script>
- <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script><script type="text/javascript" src="http://assets.okfn.org/ext/jquery.cookie/jquery.cookie.min.js"></script><script type="text/javascript" src="http://assets.okfn.org/ext/jquery.placeholder/jquery.placeholder.js"></script><script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.11/jquery-ui.min.js"></script>
--- a/ckan/templates/package/history.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/package/history.html Thu Jul 28 12:21:47 2011 +0100
@@ -39,7 +39,7 @@
<table><tr>
- <th></th><th>Revision</th><th>Timestamp</th><th>Author</th><th>Log Message</th>
+ <th></th><th>Revision ID</th><th>Package with timestamp</th><th>Author</th><th>Log Message</th></tr><py:for each="index, rev in enumerate(c.pkg_revisions)"><tr>
@@ -48,9 +48,10 @@
${h.radio("selected2", rev.id, checked=(index == len(c.pkg_revisions)-1))}
</td><td>
- <a href="${h.url_for(controller='revision',action='read',id=rev['id'])}">${rev['id']}</a>
+ <a href="${h.url_for(controller='revision',action='read',id=rev['id'])}">${rev['id'][:4]}…</a></td>
- <td>${rev['timestamp']}</td>
+ <td>
+ <a href="${h.url_for(controller='package',action='read',id='%s@%s' % (c.pkg_dict['name'], rev['timestamp']))}" title="${'Read package as of %s' % rev['timestamp']}">${h.render_datetime(rev['timestamp'])}</a></td><td>${h.linked_user(rev['author'])}</td><td>${rev['message']}</td></tr>
--- a/ckan/templates/package/layout.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/package/layout.html Thu Jul 28 12:21:47 2011 +0100
@@ -12,7 +12,7 @@
<li py:if="h.am_authorized(c, actions.EDIT, c.pkg)">
${h.subnav_link(c, h.icon('package_edit') + _('Edit'), controller='package', action='edit', id=c.pkg.name)}
</li>
- <li>${h.subnav_link(c, h.icon('page_white_stack') + _('History'), controller='package', action='history', id=c.pkg.name)}</li>
+ <li>${h.subnav_link(c, h.icon('page_stack') + _('History'), controller='package', action='history', id=c.pkg.name)}</li><li py:if="h.am_authorized(c, actions.EDIT_PERMISSIONS, c.pkg)">
${h.subnav_link(c, h.icon('lock') + _('Authorization'), controller='package', action='authz', id=c.pkg.name)}
</li>
--- a/ckan/templates/package/read.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/package/read.html Thu Jul 28 12:21:47 2011 +0100
@@ -93,6 +93,13 @@
</py:match><div py:match="content">
+ <py:if test="c.pkg_revision_id">
+ <div id="revision" class="widget-container">
+ <p py:if="c.pkg_revision_not_latest">This is an old revision of this package, as edited <!--!by ${h.linked_user(rev.author)}-->at ${h.render_datetime(c.pkg_revision_timestamp)}. It may differ significantly from the <a href="${url(controller='package', action='read', id=c.pkg.name)}">current revision</a>.</p>
+ <p py:if="not c.pkg_revision_not_latest">This is the current revision of this package, as edited <!--!by ${h.linked_user(rev.author)}-->at ${h.render_datetime(c.pkg_revision_timestamp)}.</p>
+ </div>
+ </py:if>
+
<xi:include href="read_core.html" /></div>
--- a/ckan/templates/package/search.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/package/search.html Thu Jul 28 12:21:47 2011 +0100
@@ -53,10 +53,10 @@
<p i18n:msg="item_count"><strong>There was an error while searching.</strong>
Please try another search term.</p></py:if>
- <py:if test="c.q">
+ <py:if test="request.params"><h4 i18n:msg="item_count"><strong>${c.page.item_count}</strong> packages found</h4></py:if>
- <py:if test="c.page.item_count == 0 and c.q">
+ <py:if test="c.page.item_count == 0 and request.params"><p i18n:msg="">Would you like to <a href="${h.url_for(action='new', id=None)}">create a new package?</a></p></py:if>
${package_list_from_dict(c.page.items)}
--- a/ckan/templates/user/request_reset.html Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/templates/user/request_reset.html Thu Jul 28 12:21:47 2011 +0100
@@ -14,7 +14,7 @@
Request a password reset
</h2>
- <form id="user-edit" action="" method="post" class="simple-form"
+ <form id="user-password-reset" action="" method="post" class="simple-form"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
>
@@ -25,7 +25,7 @@
</fieldset><div>
- ${h.submit('save', _('Reset password'))}
+ ${h.submit('reset', _('Reset password'))}
</div></form></div>
--- a/ckan/tests/functional/test_home.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/tests/functional/test_home.py Thu Jul 28 12:21:47 2011 +0100
@@ -5,10 +5,11 @@
import ckan.model as model
from ckan.tests import *
+from ckan.tests.html_check import HtmlCheckMethods
from ckan.tests.pylons_controller import PylonsTestCase
from ckan.tests import search_related
-class TestHomeController(TestController, PylonsTestCase):
+class TestHomeController(TestController, PylonsTestCase, HtmlCheckMethods):
@classmethod
def setup_class(cls):
PylonsTestCase.setup_class()
@@ -19,7 +20,6 @@
def teardown_class(self):
model.repo.rebuild_db()
- @search_related
def test_home_page(self):
offset = url_for('home')
res = self.app.get(offset)
@@ -48,7 +48,6 @@
res = self.app.get(offset)
res.click('Search', index=0)
- @search_related
def test_tags_link(self):
offset = url_for('home')
res = self.app.get(offset)
@@ -77,14 +76,12 @@
assert 'Search - ' in results_page, results_page
assert '>0<' in results_page, results_page
- @search_related
def test_template_footer_end(self):
offset = url_for('home')
res = self.app.get(offset)
assert '<strong>TEST TEMPLATE_FOOTER_END TEST</strong>'
# DISABLED because this is not on home page anymore
- @search_related
def _test_register_new_package(self):
offset = url_for('home')
res = self.app.get(offset)
@@ -94,7 +91,6 @@
assert 'Register a New Package' in results_page, results_page
assert '<input id="Package--title" name="Package--title" size="40" type="text" value="test title">' in results_page, results_page
- @search_related
def test_locale_change(self):
offset = url_for('home')
res = self.app.get(offset)
@@ -105,7 +101,18 @@
finally:
res = res.click('English')
- @search_related
+ def test_locale_change_invalid(self):
+ offset = url_for(controller='home', action='locale', locale='')
+ res = self.app.get(offset, status=400)
+ main_res = self.main_div(res)
+ assert 'Invalid language specified' in main_res, main_res
+
+ def test_locale_change_blank(self):
+ offset = url_for(controller='home', action='locale')
+ res = self.app.get(offset, status=400)
+ main_res = self.main_div(res)
+ assert 'No language given!' in main_res, main_res
+
def test_locale_change_with_false_hash(self):
offset = url_for('home')
res = self.app.get(offset)
--- a/ckan/tests/functional/test_package.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/tests/functional/test_package.py Thu Jul 28 12:21:47 2011 +0100
@@ -1,4 +1,5 @@
import cgi
+import datetime
from paste.fixture import AppError
from pylons import config
@@ -427,6 +428,178 @@
assert plugin.calls['read'] == 1, plugin.calls
plugins.unload(plugin)
+
+class TestReadAtRevision(FunctionalTestCase, HtmlCheckMethods):
+
+ @classmethod
+ def setup_class(cls):
+ cls.before = datetime.datetime(2010, 1, 1)
+ cls.date1 = datetime.datetime(2011, 1, 1)
+ cls.date2 = datetime.datetime(2011, 1, 2)
+ cls.date3 = datetime.datetime(2011, 1, 3)
+ cls.today = datetime.datetime.now()
+ cls.pkg_name = u'testpkg'
+
+ # create package
+ rev = model.repo.new_revision()
+ rev.timestamp = cls.date1
+ pkg = model.Package(name=cls.pkg_name, title=u'title1')
+ model.Session.add(pkg)
+ model.setup_default_user_roles(pkg)
+ model.repo.commit_and_remove()
+
+ # edit package
+ rev = model.repo.new_revision()
+ rev.timestamp = cls.date2
+ pkg = model.Package.by_name(cls.pkg_name)
+ pkg.title = u'title2'
+ pkg.add_tag_by_name(u'tag2')
+ pkg.extras = {'key2': u'value2'}
+ model.repo.commit_and_remove()
+
+ # edit package again
+ rev = model.repo.new_revision()
+ rev.timestamp = cls.date3
+ pkg = model.Package.by_name(cls.pkg_name)
+ pkg.title = u'title3'
+ pkg.add_tag_by_name(u'tag3')
+ pkg.extras['key2'] = u'value3'
+ model.repo.commit_and_remove()
+
+ cls.offset = url_for(controller='package',
+ action='read',
+ id=cls.pkg_name)
+ pkg = model.Package.by_name(cls.pkg_name)
+ cls.revision_ids = [rev[0].id for rev in pkg.all_related_revisions[::-1]]
+ # revision order is reversed to be chronological
+
+ @classmethod
+ def teardown_class(cls):
+ model.repo.rebuild_db()
+
+ def test_read_normally(self):
+ res = self.app.get(self.offset, status=200)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'PKG', pkg_html
+ assert 'title3' in pkg_html
+ assert 'key2' in pkg_html
+ assert 'value3' in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' in side_html
+ assert 'tag2' in side_html
+
+ def test_read_date1(self):
+ offset = self.offset + self.date1.strftime('@%Y-%m-%d')
+ res = self.app.get(offset, status=200)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'PKG', pkg_html
+ assert 'title1' in pkg_html
+ assert 'key2' not in pkg_html
+ assert 'value3' not in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' not in side_html
+ assert 'tag2' not in side_html
+
+ def test_read_date2(self):
+ date2_plus_3h = self.date2 + datetime.timedelta(hours=3)
+ offset = self.offset + date2_plus_3h.strftime('@%Y-%m-%d')
+ res = self.app.get(offset, status=200)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'PKG', pkg_html
+ assert 'title2' in pkg_html
+ assert 'key2' in pkg_html
+ assert 'value2' in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' not in side_html
+ assert 'tag2' in side_html
+
+ def test_read_date3(self):
+ offset = self.offset + self.date3.strftime('@%Y-%m-%d-%H-%M')
+ res = self.app.get(offset, status=200)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'PKG', pkg_html
+ assert 'title3' in pkg_html
+ assert 'key2' in pkg_html
+ assert 'value3' in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' in side_html
+ assert 'tag2' in side_html
+
+ def test_read_date_before_created(self):
+ offset = self.offset + self.before.strftime('@%Y-%m-%d')
+ res = self.app.get(offset, status=404)
+
+ def test_read_date_invalid(self):
+ res = self.app.get(self.offset + self.date3.strftime('@%Y-%m'),
+ status=400)
+ res = self.app.get(self.offset + self.date3.strftime('@%Y'),
+ status=400)
+ res = self.app.get(self.offset + self.date3.strftime('@%Y@%m'),
+ status=400)
+
+ def test_read_revision1(self):
+ offset = self.offset + '@%s' % self.revision_ids[0]
+ res = self.app.get(offset, status=200)
+ main_html = self.main_div(res)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'MAIN', main_html
+ assert 'This is an old revision of this package' in main_html
+ assert 'at 2011-01-01 00:00' in main_html
+ self.check_named_element(main_html, 'a', 'href="/package/%s"' % self.pkg_name)
+ print 'PKG', pkg_html
+ assert 'title1' in pkg_html
+ assert 'key2' not in pkg_html
+ assert 'value3' not in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' not in side_html
+ assert 'tag2' not in side_html
+
+ def test_read_revision2(self):
+ offset = self.offset + '@%s' % self.revision_ids[1]
+ res = self.app.get(offset, status=200)
+ main_html = self.main_div(res)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'MAIN', main_html
+ assert 'This is an old revision of this package' in main_html
+ assert 'at 2011-01-02 00:00' in main_html
+ self.check_named_element(main_html, 'a', 'href="/package/%s"' % self.pkg_name)
+ print 'PKG', pkg_html
+ assert 'title2' in pkg_html
+ assert 'key2' in pkg_html
+ assert 'value2' in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' not in side_html
+ assert 'tag2' in side_html
+
+ def test_read_revision3(self):
+ offset = self.offset + '@%s' % self.revision_ids[2]
+ res = self.app.get(offset, status=200)
+ main_html = self.main_div(res)
+ pkg_html = self.named_div('package', res)
+ side_html = self.named_div('primary', res)
+ print 'MAIN', main_html
+ assert 'This is an old revision of this package' not in main_html
+ assert 'This is the current revision of this package' in main_html
+ assert 'at 2011-01-03 00:00' in main_html
+ self.check_named_element(main_html, 'a', 'href="/package/%s"' % self.pkg_name)
+ print 'PKG', pkg_html
+ assert 'title3' in pkg_html
+ assert 'key2' in pkg_html
+ assert 'value3' in pkg_html
+ print 'SIDE', side_html
+ assert 'tag3' in side_html
+ assert 'tag2' in side_html
+
+ def test_read_bad_revision(self):
+ # this revision doesn't exist in the db
+ offset = self.offset + '@ccab6798-1f4b-4a22-bcf5-462703aa4594'
+ res = self.app.get(offset, status=404)
class TestEdit(TestPackageForm):
editpkg_name = u'editpkgtest'
@@ -1277,36 +1450,39 @@
class TestRevisions(TestPackageBase):
@classmethod
- def setup_class(self):
+ def setup_class(cls):
model.Session.remove()
model.repo.init_db()
- self.name = u'revisiontest1'
+ cls.name = u'revisiontest1'
# create pkg
- self.notes = [u'Written by Puccini', u'Written by Rossini', u'Not written at all', u'Written again', u'Written off']
+ cls.notes = [u'Written by Puccini', u'Written by Rossini', u'Not written at all', u'Written again', u'Written off']
rev = model.repo.new_revision()
- self.pkg1 = model.Package(name=self.name)
- self.pkg1.notes = self.notes[0]
- model.Session.add(self.pkg1)
- model.setup_default_user_roles(self.pkg1)
+ cls.pkg1 = model.Package(name=cls.name)
+ cls.pkg1.notes = cls.notes[0]
+ model.Session.add(cls.pkg1)
+ model.setup_default_user_roles(cls.pkg1)
model.repo.commit_and_remove()
# edit pkg
for i in range(5)[1:]:
rev = model.repo.new_revision()
- pkg1 = model.Package.by_name(self.name)
- pkg1.notes = self.notes[i]
+ pkg1 = model.Package.by_name(cls.name)
+ pkg1.notes = cls.notes[i]
model.repo.commit_and_remove()
- self.pkg1 = model.Package.by_name(self.name)
+ cls.pkg1 = model.Package.by_name(cls.name)
+ cls.revision_ids = [rev[0].id for rev in cls.pkg1.all_related_revisions]
+ # revision ids are newest first
+ cls.revision_timestamps = [rev[0].timestamp for rev in cls.pkg1.all_related_revisions]
+ cls.offset = url_for(controller='package', action='history', id=cls.pkg1.name)
@classmethod
- def teardown_class(self):
+ def teardown_class(cls):
model.repo.rebuild_db()
def test_0_read_history(self):
- offset = url_for(controller='package', action='history', id=self.pkg1.name)
- res = self.app.get(offset)
+ res = self.app.get(self.offset)
main_res = self.main_div(res)
assert self.pkg1.name in main_res, main_res
assert 'radio' in main_res, main_res
@@ -1318,8 +1494,7 @@
assert last_radio_checked_html in main_res, '%s %s' % (last_radio_checked_html, main_res)
def test_1_do_diff(self):
- offset = url_for(controller='package', action='history', id=self.pkg1.name)
- res = self.app.get(offset)
+ res = self.app.get(self.offset)
form = res.forms['package-revisions']
res = form.submit()
res = res.follow()
@@ -1330,13 +1505,26 @@
assert '<tr><td>notes</td><td><pre>- Written by Puccini\n+ Written off</pre></td></tr>' in main_res, main_res
def test_2_atom_feed(self):
- offset = url_for(controller='package', action='history', id=self.pkg1.name)
- offset = "%s?format=atom" % offset
+ offset = "%s?format=atom" % self.offset
res = self.app.get(offset)
assert '<feed' in res, res
assert 'xmlns="http://www.w3.org/2005/Atom"' in res, res
assert '</feed>' in res, res
+ def test_3_history_revision_link(self):
+ res = self.app.get(self.offset)
+ res = res.click('%s' % self.revision_ids[2][:4])
+ main_res = self.main_div(res)
+ assert 'Revision: %s' % self.revision_ids[2] in main_res
+
+ def test_4_history_revision_package_link(self):
+ res = self.app.get(self.offset)
+ url = str(self.revision_timestamps[1])[-6:]
+ res = res.click(href=url)
+ main_html = self.main_div(res)
+ assert 'This is an old revision of this package' in main_html
+ assert 'at %s' % str(self.revision_timestamps[1])[:6] in main_html
+
class TestMarkdownHtmlWhitelist(TestPackageForm):
--- a/ckan/tests/functional/test_user.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/tests/functional/test_user.py Thu Jul 28 12:21:47 2011 +0100
@@ -4,12 +4,17 @@
from pprint import pprint
from ckan.tests import search_related, CreateTestData
from ckan.tests.html_check import HtmlCheckMethods
+from ckan.tests.pylons_controller import PylonsTestCase
+from ckan.tests.mock_mail_server import SmtpServerHarness
import ckan.model as model
from base import FunctionalTestCase
+from ckan.lib.mailer import get_reset_link, create_reset_key
-class TestUserController(FunctionalTestCase, HtmlCheckMethods):
+class TestUserController(FunctionalTestCase, HtmlCheckMethods, PylonsTestCase, SmtpServerHarness):
@classmethod
def setup_class(self):
+ PylonsTestCase.setup_class()
+ SmtpServerHarness.setup_class()
CreateTestData.create()
# make 3 changes, authored by annafan
@@ -27,6 +32,7 @@
@classmethod
def teardown_class(self):
+ SmtpServerHarness.teardown_class()
model.repo.rebuild_db()
def teardown(self):
@@ -432,6 +438,12 @@
assert 'looks like spam' in main_res, main_res
assert 'Edit User: ' in main_res, main_res
+ def test_login_openid_error(self):
+ # comes back as a params like this:
+ # e.g. /user/login?error=Error%20in%20discovery:%20Error%20fetching%20XRDS%20document:%20(6,%20%22Couldn't%20resolve%20host%20'mysite.myopenid.com'%22)
+ res = self.app.get("/user/login?error=Error%20in%20discovery:%20Error%20fetching%20XRDS%20document:%20(6,%20%22Couldn't%20resolve%20host%20'mysite.myopenid.com'%22")
+ main_res = self.main_div(res)
+ assert "Couldn't resolve host" in main_res, main_res
############
# Disabled
@@ -481,3 +493,93 @@
# but for some reason this does not work ...
return res
+ def test_request_reset_user_password_link_user_incorrect(self):
+ offset = url_for(controller='user',
+ action='request_reset')
+ res = self.app.get(offset)
+ fv = res.forms['user-password-reset']
+ fv['user'] = 'unknown'
+ res = fv.submit()
+ main_res = self.main_div(res)
+ assert 'No such user: unknown' in main_res, main_res # error
+
+ def test_request_reset_user_password_using_search(self):
+ CreateTestData.create_user(name='larry1', email='kittens at john.com')
+ offset = url_for(controller='user',
+ action='request_reset')
+ res = self.app.get(offset)
+ fv = res.forms['user-password-reset']
+ fv['user'] = 'kittens'
+ res = fv.submit()
+ assert_equal(res.status, 302)
+ assert_equal(res.header_dict['Location'], 'http://localhost/')
+
+ CreateTestData.create_user(name='larry2', fullname='kittens')
+ res = self.app.get(offset)
+ fv = res.forms['user-password-reset']
+ fv['user'] = 'kittens'
+ res = fv.submit()
+ main_res = self.main_div(res)
+ assert '"kittens" matched several users' in main_res, main_res
+ assert 'larry1' not in main_res, main_res
+ assert 'larry2' not in main_res, main_res
+
+ res = self.app.get(offset)
+ fv = res.forms['user-password-reset']
+ fv['user'] = ''
+ res = fv.submit()
+ main_res = self.main_div(res)
+ assert 'No such user:' in main_res, main_res
+
+ res = self.app.get(offset)
+ fv = res.forms['user-password-reset']
+ fv['user'] = 'l'
+ res = fv.submit()
+ main_res = self.main_div(res)
+ assert 'No such user:' in main_res, main_res
+
+ def test_reset_user_password_link(self):
+ # Set password
+ CreateTestData.create_user(name='bob', email='bob at bob.net', password='test1')
+
+ # Set password to something new
+ model.User.by_name(u'bob').password = 'test2'
+ model.repo.commit_and_remove()
+ test2_encoded = model.User.by_name(u'bob').password
+ assert test2_encoded != 'test2'
+ assert model.User.by_name(u'bob').password == test2_encoded
+
+ # Click link from reset password email
+ create_reset_key(model.User.by_name(u'bob'))
+ reset_password_link = get_reset_link(model.User.by_name(u'bob'))
+ offset = reset_password_link.replace('http://test.ckan.net', '')
+ print offset
+ res = self.app.get(offset)
+
+ # Reset password form
+ fv = res.forms['user-reset']
+ fv['password1'] = 'test1'
+ fv['password2'] = 'test1'
+ res = fv.submit('save', status=302)
+
+ # Check a new password is stored
+ assert model.User.by_name(u'bob').password != test2_encoded
+
+ def test_perform_reset_user_password_link_key_incorrect(self):
+ CreateTestData.create_user(name='jack', password='test1')
+ # Make up a key - i.e. trying to hack this
+ user = model.User.by_name(u'jack')
+ offset = url_for(controller='user',
+ action='perform_reset',
+ id=user.id,
+ key='randomness') # i.e. incorrect
+ res = self.app.get(offset, status=403) # error
+
+ def test_perform_reset_user_password_link_user_incorrect(self):
+ # Make up a key - i.e. trying to hack this
+ user = model.User.by_name(u'jack')
+ offset = url_for(controller='user',
+ action='perform_reset',
+ id='randomness', # i.e. incorrect
+ key='randomness')
+ res = self.app.get(offset, status=404)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/lib/test_mailer.py Thu Jul 28 12:21:47 2011 +0100
@@ -0,0 +1,112 @@
+import time
+from nose.tools import assert_equal, assert_raises
+from pylons import config
+from email.mime.text import MIMEText
+
+from ckan import model
+from ckan.tests.pylons_controller import PylonsTestCase
+from ckan.tests.mock_mail_server import SmtpServerHarness
+from ckan.lib.mailer import mail_recipient, mail_user, send_reset_link, add_msg_niceties, MailerException, get_reset_link_body, get_reset_link
+from ckan.lib.create_test_data import CreateTestData
+from ckan.lib.base import g
+
+class TestMailer(SmtpServerHarness, PylonsTestCase):
+ @classmethod
+ def setup_class(cls):
+ CreateTestData.create_user(name='bob', email='bob at bob.net')
+ CreateTestData.create_user(name='mary') #NB No email addr provided
+ SmtpServerHarness.setup_class()
+ PylonsTestCase.setup_class()
+
+ @classmethod
+ def teardown_class(cls):
+ SmtpServerHarness.teardown_class()
+ model.repo.rebuild_db()
+
+ def setup(self):
+ self.clear_smtp_messages()
+
+ def mime_encode(self, msg, recipient_name):
+ sender_name = g.site_title
+ sender_url = g.site_url
+ body = add_msg_niceties(recipient_name, msg, sender_name, sender_url)
+ encoded_body = MIMEText(body.encode('utf-8'), 'plain', 'utf-8').get_payload().strip()
+ return encoded_body
+
+ def test_mail_recipient(self):
+ msgs = self.get_smtp_messages()
+ assert_equal(msgs, [])
+
+ # send email
+ test_email = {'recipient_name': 'Bob',
+ 'recipient_email':'bob at bob.net',
+ 'subject': 'Meeting',
+ 'body': 'The meeting is cancelled.',
+ 'headers': {'header1': 'value1'}}
+ mail_recipient(**test_email)
+ time.sleep(0.1)
+
+ # check it went to the mock smtp server
+ msgs = self.get_smtp_messages()
+ assert_equal(len(msgs), 1)
+ msg = msgs[0]
+ assert_equal(msg[1], config['ckan.mail_from'])
+ assert_equal(msg[2], [test_email['recipient_email']])
+ assert test_email['headers'].keys()[0] in msg[3], msg[3]
+ assert test_email['headers'].values()[0] in msg[3], msg[3]
+ assert test_email['subject'] in msg[3], msg[3]
+ expected_body = self.mime_encode(test_email['body'],
+ test_email['recipient_name'])
+ assert expected_body in msg[3], '%r not in %r' % (expected_body, msg[3])
+
+ def test_mail_user(self):
+ msgs = self.get_smtp_messages()
+ assert_equal(msgs, [])
+
+ # send email
+ test_email = {'recipient': model.User.by_name(u'bob'),
+ 'subject': 'Meeting',
+ 'body': 'The meeting is cancelled.',
+ 'headers': {'header1': 'value1'}}
+ mail_user(**test_email)
+ time.sleep(0.1)
+
+ # check it went to the mock smtp server
+ msgs = self.get_smtp_messages()
+ assert_equal(len(msgs), 1)
+ msg = msgs[0]
+ assert_equal(msg[1], config['ckan.mail_from'])
+ assert_equal(msg[2], [model.User.by_name(u'bob').email])
+ assert test_email['headers'].keys()[0] in msg[3], msg[3]
+ assert test_email['headers'].values()[0] in msg[3], msg[3]
+ assert test_email['subject'] in msg[3], msg[3]
+ expected_body = self.mime_encode(test_email['body'],
+ 'bob')
+ assert expected_body in msg[3], '%r not in %r' % (expected_body, msg[3])
+
+ def test_mail_user_without_email(self):
+ # send email
+ test_email = {'recipient': model.User.by_name(u'mary'),
+ 'subject': 'Meeting',
+ 'body': 'The meeting is cancelled.',
+ 'headers': {'header1': 'value1'}}
+ assert_raises(MailerException, mail_user, **test_email)
+
+ def test_send_reset_email(self):
+ # send email
+ send_reset_link(model.User.by_name(u'bob'))
+ time.sleep(0.1)
+
+ # check it went to the mock smtp server
+ msgs = self.get_smtp_messages()
+ assert_equal(len(msgs), 1)
+ msg = msgs[0]
+ assert_equal(msg[1], config['ckan.mail_from'])
+ assert_equal(msg[2], [model.User.by_name(u'bob').email])
+ assert 'Reset' in msg[3], msg[3]
+ test_msg = get_reset_link_body(model.User.by_name(u'bob'))
+ expected_body = self.mime_encode(test_msg,
+ u'bob')
+ assert expected_body in msg[3], '%r not in %r' % (expected_body, msg[3])
+
+ # reset link tested in user functional test
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/misc/test_mock_mail_server.py Thu Jul 28 12:21:47 2011 +0100
@@ -0,0 +1,33 @@
+import time
+from nose.tools import assert_equal
+from pylons import config
+from email.mime.text import MIMEText
+
+from ckan.tests.pylons_controller import PylonsTestCase
+from ckan.tests.mock_mail_server import SmtpServerHarness
+from ckan.lib.mailer import mail_recipient
+
+class TestMockMailServer(SmtpServerHarness, PylonsTestCase):
+ @classmethod
+ def setup_class(cls):
+ SmtpServerHarness.setup_class()
+ PylonsTestCase.setup_class()
+
+ @classmethod
+ def teardown_class(cls):
+ SmtpServerHarness.teardown_class()
+
+ def test_basic(self):
+ msgs = self.get_smtp_messages()
+ assert_equal(msgs, [])
+
+ test_email = {'recipient_name': 'Bob',
+ 'recipient_email':'bob at bob.net',
+ 'subject': 'Meeting',
+ 'body': 'The meeting is cancelled.',
+ 'headers': {'header1': 'value1'}}
+ mail_recipient(**test_email)
+ time.sleep(0.1)
+
+ msgs = self.get_smtp_messages()
+ assert_equal(len(msgs), 1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/mock_mail_server.py Thu Jul 28 12:21:47 2011 +0100
@@ -0,0 +1,82 @@
+import threading
+import asyncore
+import socket
+from smtpd import SMTPServer
+
+from pylons import config
+
+from ckan.lib.mailer import _mail_recipient
+
+class MockSmtpServer(SMTPServer):
+ '''A mock SMTP server that operates in an asyncore loop'''
+ def __init__(self, host, port):
+ self.msgs = []
+ SMTPServer.__init__(self, (host, port), None)
+
+ def process_message(self, peer, mailfrom, rcpttos, data):
+ self.msgs.append((peer, mailfrom, rcpttos, data))
+
+ def get_smtp_messages(self):
+ return self.msgs
+
+ def clear_smtp_messages(self):
+ self.msgs = []
+
+class MockSmtpServerThread(threading.Thread):
+ '''Runs the mock SMTP server in a thread'''
+ def __init__(self, host, port):
+ self.assert_port_free(host, port)
+ # init thread
+ self._stop_event = threading.Event()
+ self.thread_name = self.__class__
+ threading.Thread.__init__(self, name=self.thread_name)
+ # init smtp server
+ self.server = MockSmtpServer(host, port)
+
+ def assert_port_free(self, host, port):
+ test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
+ test_socket.getsockopt(socket.SOL_SOCKET,
+ socket.SO_REUSEADDR) | 1 )
+ test_socket.bind((host, port))
+ test_socket.close()
+
+ def run(self):
+ while not self._stop_event.isSet():
+ asyncore.loop(timeout=0.01, count=1)
+
+ def stop(self, timeout=None):
+ self._stop_event.set()
+ threading.Thread.join(self, timeout)
+ self.server.close()
+
+ def get_smtp_messages(self):
+ return self.server.get_smtp_messages()
+
+ def clear_smtp_messages(self):
+ return self.server.clear_smtp_messages()
+
+class SmtpServerHarness(object):
+ '''Derive from this class to run MockSMTP - a test harness that
+ records what email messages are requested to be sent by it.'''
+
+ @classmethod
+ def setup_class(cls):
+ smtp_server = config.get('test_smtp_server') or config['smtp_server']
+ if ':' in smtp_server:
+ host, port = smtp_server.split(':')
+ else:
+ host, port = smtp_server, 25
+ cls.smtp_thread = MockSmtpServerThread(host, int(port))
+ cls.smtp_thread.start()
+
+ @classmethod
+ def teardown_class(cls):
+ cls.smtp_thread.stop()
+
+ def get_smtp_messages(self):
+ return self.smtp_thread.get_smtp_messages()
+
+ def clear_smtp_messages(self):
+ return self.smtp_thread.clear_smtp_messages()
+
--- a/ckan/tests/test_mailer.py Wed Jul 27 16:37:00 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-"""
-"""
-from pylons import config
-from ckan.lib.mailer import _mail_recipient
-from ckan.tests import *
-
-from smtpd import SMTPServer
-
-class TestMailer(TestController):
-
- def setup(self):
- config['smtp_server'] = 'localhost:667511'
- config['ckan.mail_from'] = 'info at ckan.net'
- class TestSMTPServer(SMTPServer):
- def process_message(zelf, peer, mailfrom, rcpttos, data):
- print "FOO"
- return self.process_message(peer, mailfrom, rcpttos, data)
- self.server = TestSMTPServer(('localhost', 6675), None)
-
- def test_mail_recipient(self):
- # def tests(s, peer, mailfrom, rcpttos, data):
- # assert 'info at ckan.net' in mailfrom
- # assert 'foo at bar.com' in recpttos
- # assert 'i am a banana' in data
- # #self.process_message = tests
- # _mail_recipient('fooman', 'foo at localhost',
- # 'banaman', 'http://banana.com',
- # 'i am a banana', 'this is a test')
- pass
--- a/ckan/tests/wsgi_ckanclient.py Wed Jul 27 16:37:00 2011 +0100
+++ b/ckan/tests/wsgi_ckanclient.py Thu Jul 28 12:21:47 2011 +0100
@@ -2,7 +2,12 @@
import paste.fixture
-from ckanclient import CkanClient, Request, CkanApiError
+from ckanclient import CkanClient, CkanApiError
+try:
+ from ckanclient import ApiRequest
+except ImportError:
+ # older versions of ckanclient
+ from ckanclient import Request as ApiRequest
__all__ = ['WsgiCkanClient', 'ClientError']
@@ -27,7 +32,7 @@
if data != None:
data = urllib.urlencode({data: 1})
# Don't use request beyond getting the method
- req = Request(location, data, headers, method=method)
+ req = ApiRequest(location, data, headers, method=method)
# Make header values ascii strings
for key, value in headers.items():
--- a/fabfile.py Wed Jul 27 16:37:00 2011 +0100
+++ b/fabfile.py Thu Jul 28 12:21:47 2011 +0100
@@ -167,7 +167,7 @@
db_user=None,
db_pass='',
db_host='localhost',
- user=None
+ user='okfn'
):
'''Configurable configuration: fab -d gives full info.
@@ -401,7 +401,7 @@
with cd(env.instance_path):
assert exists(env.config_ini_filename), "Can't find config file: %s/%s" % (env.instance_path, env.config_ini_filename)
db_details = _get_db_config()
- assert db_details['db_type'] == 'postgres'
+ assert db_details['db_type'] in ('postgres', 'postgresql')
port_option = '-p %s' % db_details['db_port'] if db_details['db_port'] else ''
run('export PGPASSWORD=%s&&pg_dump -U %s -h %s %s %s > %s' % (db_details['db_pass'], db_details['db_user'], db_details['db_host'], port_option, db_details['db_name'], pg_dump_filepath), shell=False)
assert exists(pg_dump_filepath)
--- a/requires/lucid_present.txt Wed Jul 27 16:37:00 2011 +0100
+++ b/requires/lucid_present.txt Thu Jul 28 12:21:47 2011 +0100
@@ -10,9 +10,9 @@
psycopg2==2.0.13
lxml==2.2.4
sphinx==0.6.4
-# Specifying not to use later webob because of incompatibility
+# Specifying particular version of WebOb because later version has incompatibility
# with pylons 0.9.7 (change to imports of Multidict)
-webob<=1.0.8
+webob==1.0.8
Pylons==0.9.7
repoze.who==1.0.18
tempita==0.4
--- a/setup.py Wed Jul 27 16:37:00 2011 +0100
+++ b/setup.py Thu Jul 28 12:21:47 2011 +0100
@@ -65,6 +65,9 @@
rights = ckan.lib.authztool:RightsCommand
roles = ckan.lib.authztool:RolesCommand
+ [console_scripts]
+ ckan-admin = bin.ckan_admin:Command
+
[paste.paster_create_template]
ckanext=ckan.pastertemplates:CkanextTemplate
--- a/test-core.ini Wed Jul 27 16:37:00 2011 +0100
+++ b/test-core.ini Thu Jul 28 12:21:47 2011 +0100
@@ -47,3 +47,7 @@
# use <strong> so we can check that html is *not* escaped
ckan.template_footer_end = <strong>TEST TEMPLATE_FOOTER_END TEST</strong>
+
+# mailer
+test_smtp_server = localhost:6675
+ckan.mail_from = info at test.ckan.net
http://bitbucket.org/okfn/ckan/changeset/1317fd88220d/
changeset: 1317fd88220d
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-28 13:50:46
summary: Fix issues in the user controller
affected #: 2 files (757 bytes)
--- a/ckan/controllers/user.py Thu Jul 28 12:21:47 2011 +0100
+++ b/ckan/controllers/user.py Thu Jul 28 12:50:46 2011 +0100
@@ -84,9 +84,6 @@
try:
user_dict = get.user_show(context,data_dict)
except NotFound:
- abort(404, _('User not found'))
-
- if not user_dict:
h.redirect_to(controller='user', action='login', id=None)
c.user_dict = user_dict
@@ -253,22 +250,38 @@
'user': c.user}
data_dict = {'id':id}
-
+ user_obj = None
try:
user_dict = get.user_show(context,data_dict)
user_obj = context['user_obj']
+ except NotFound:
+ # Try searching the user
+ del data_dict['id']
+ data_dict['q'] = id
- if user_dict is None:
+ if id and len(id) > 2:
+ user_list = get.user_list(context,data_dict)
+ if len(user_list) == 1:
+ # This is ugly, but we need the user object for the mailer,
+ # and user_list does not return them
+ del data_dict['q']
+ data_dict['id'] = user_list[0]['id']
+ user_dict = get.user_show(context,data_dict)
+ user_obj = context['user_obj']
+ elif len(user_list) > 1:
+ h.flash_error(_('"%s" matched several users') % (id))
+ else:
+ h.flash_error(_('No such user: %s') % id)
+ else:
h.flash_error(_('No such user: %s') % id)
+
+ if user_obj:
try:
mailer.send_reset_link(user_obj)
h.flash_success(_('Please check your inbox for a reset code.'))
redirect('/')
except mailer.MailerException, e:
h.flash_error(_('Could not send reset link: %s') % unicode(e))
-
- except NotFound:
- h.flash_error(_('No such user: %s') % id)
return render('user/request_reset.html')
def perform_reset(self, id):
--- a/ckan/logic/action/get.py Thu Jul 28 12:21:47 2011 +0100
+++ b/ckan/logic/action/get.py Thu Jul 28 12:50:46 2011 +0100
@@ -358,7 +358,7 @@
elif provided_user:
context['user_obj'] = user = provided_user
else:
- return None
+ raise NotFound
user_dict = user_dictize(user,context)
http://bitbucket.org/okfn/ckan/changeset/b8c4dbf865d4/
changeset: b8c4dbf865d4
user: amercader
date: 2011-07-28 13:55:51
summary: [merge] from feature-1220-db-out-of-controllers
affected #: 35 files (42.5 KB)
--- a/ckan/controllers/api.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/controllers/api.py Thu Jul 28 12:55:51 2011 +0100
@@ -157,9 +157,14 @@
'message': _('Access denied')}
return_dict['success'] = False
return self._finish(403, return_dict, content_type='json')
+ except NotFound:
+ return_dict['error'] = {'__type': 'Not Found Error',
+ 'message': _('Not found')}
+ return_dict['success'] = False
+ return self._finish(404, return_dict, content_type='json')
except ValidationError, e:
error_dict = e.error_dict
- error_dict['__type'] = 'Validtion Error'
+ error_dict['__type'] = 'Validation Error'
return_dict['error'] = error_dict
return_dict['success'] = False
log.error('Validation error: %r' % str(e.error_dict))
@@ -198,7 +203,7 @@
action_map = {
'revision': get.revision_show,
'group': get.group_show_rest,
- 'tag': get.tag_show,
+ 'tag': get.tag_show_rest,
'package': get.package_show_rest,
('package', 'relationships'): get.package_relationships_list,
}
@@ -447,12 +452,18 @@
return params
def tag_counts(self, ver=None):
- log.debug('tag counts')
- tags = model.Session.query(model.Tag).all()
+ c.q = request.params.get('q', '')
+
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'all_fields': True}
+
+ tag_list = get.tag_list(context, data_dict)
results = []
- for tag in tags:
- tag_count = len(tag.package_tags)
- results.append((tag.name, tag_count))
+ for tag in tag_list:
+ tag_count = len(tag['packages'])
+ results.append((tag['name'], tag_count))
return self._finish_ok(results)
def throughput(self, ver=None):
@@ -478,21 +489,15 @@
def user_autocomplete(self):
q = request.params.get('q', '')
limit = request.params.get('limit', 20)
- try:
- limit = int(limit)
- except:
- limit = 20
- limit = min(50, limit)
-
- query = model.User.search(q)
- def convert_to_dict(user):
- out = {}
- for k in ['id', 'name', 'fullname']:
- out[k] = getattr(user, k)
- return out
- query = query.limit(limit)
- out = map(convert_to_dict, query.all())
- return out
+ user_list = []
+ if q:
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'q':q,'limit':limit}
+
+ user_list = get.user_autocomplete(context,data_dict)
+ return user_list
@jsonpify
@@ -528,26 +533,22 @@
return self._finish_ok(response_data)
def tag_autocomplete(self):
- incomplete = request.params.get('incomplete', '')
- if incomplete:
- query = query_for('tag', backend='sql')
- query.run(query=incomplete,
- return_objects=True,
- limit=10,
- username=c.user)
- tagNames = [t.name for t in query.results]
- else:
- tagNames = []
+ q = request.params.get('incomplete', '')
+ limit = request.params.get('limit', 10)
+ tag_names = []
+ if q:
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'q':q,'limit':limit}
+
+ tag_names = get.tag_autocomplete(context,data_dict)
+
resultSet = {
- "ResultSet": {
- "Result": []
+ 'ResultSet': {
+ 'Result': [{'Name': tag} for tag in tag_names]
}
}
- for tagName in tagNames[:10]:
- result = {
- "Name": tagName
- }
- resultSet["ResultSet"]["Result"].append(result)
return self._finish_ok(resultSet)
--- a/ckan/controllers/group.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/controllers/group.py Thu Jul 28 12:55:51 2011 +0100
@@ -1,4 +1,5 @@
import genshi
+import datetime
from sqlalchemy.orm import eagerload_all
from ckan.lib.base import BaseController, c, model, request, render, h
@@ -39,21 +40,21 @@
c, model.Action.CHANGE_STATE, group)
## end hooks
-
- def __init__(self):
- BaseController.__init__(self)
- self.extensions = PluginImplementations(IGroupController)
def index(self):
-
+
if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
abort(401, _('Not authorized to see this page'))
-
- 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())
+
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'all_fields': True}
+
+ results = get.group_list(context,data_dict)
+
c.page = Page(
- collection=query,
+ collection=results,
page=request.params.get('page', 1),
items_per_page=20
)
@@ -61,17 +62,21 @@
def read(self, id):
- c.group = model.Group.get(id)
- if c.group is None:
- abort(404)
- auth_for_read = self.authorizer.am_authorized(c, model.Action.READ, c.group)
- if not auth_for_read:
- abort(401, _('Not authorized to read %s') % id.encode('utf8'))
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'schema': self._form_to_db_schema()}
+ data_dict = {'id': id}
+ try:
+ c.group_dict = get.group_show(context, data_dict)
+ c.group = context['group']
+ except NotFound:
+ abort(404, _('Group not found'))
+ except NotAuthorized:
+ abort(401, _('Unauthorized to read group %s') % id)
- import ckan.misc
- format = ckan.misc.MarkdownFormat()
- desc_formatted = format.to_html(c.group.description)
- try:
+ try:
+
+ desc_formatted = ckan.misc.MarkdownFormat().to_html(c.group.description)
desc_formatted = genshi.HTML(desc_formatted)
except genshi.ParseError, e:
log.error('Could not print group description: %r Error: %r', c.group.description, e)
@@ -84,8 +89,7 @@
page=request.params.get('page', 1),
items_per_page=50
)
- for extension in self.extensions:
- extension.read(c.group)
+
return render('group/read.html')
def new(self, data=None, errors=None, error_summary=None):
@@ -394,12 +398,6 @@
c.authz_groups_role_dict = authz_groups_role_dict
return render('group/authz.html')
-
-
-
-
-
-
def history(self, id):
if 'diff' in request.params or 'selected1' in request.params:
@@ -416,10 +414,19 @@
params['diff_entity'] = 'group'
h.redirect_to(controller='revision', action='diff', **params)
- c.group = model.Group.get(id)
- if not c.group:
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'schema': self._form_to_db_schema()}
+ data_dict = {'id': id}
+ try:
+ c.group_dict = get.group_show(context, data_dict)
+ c.group_revisions = get.group_revision_list(context, data_dict)
+ #TODO: remove
+ # Still necessary for the authz check in group/layout.html
+ c.group = context['group']
+ except NotFound:
abort(404, _('Group not found'))
- if not self.authorizer.am_authorized(c, model.Action.READ, c.group):
+ except NotAuthorized:
abort(401, _('User %r not authorized to edit %r') % (c.user, id))
format = request.params.get('format', '')
@@ -428,31 +435,29 @@
from webhelpers.feedgenerator import Atom1Feed
feed = Atom1Feed(
title=_(u'CKAN Group Revision History'),
- link=h.url_for(controller='group', action='read', id=c.group.name),
+ link=h.url_for(controller='group', action='read', id=c.group_dict['name']),
description=_(u'Recent changes to CKAN Group: ') +
- c.group.display_name,
+ c.group_dict['display_name'],
language=unicode(get_lang()),
)
- for revision, obj_rev in c.group.all_related_revisions:
+ for revision_dict in c.group_revisions:
+ revision_date = h.date_str_to_datetime(revision_dict['timestamp'])
try:
dayHorizon = int(request.params.get('days'))
except:
dayHorizon = 30
- try:
- dayAge = (datetime.now() - revision.timestamp).days
- except:
- dayAge = 0
+ dayAge = (datetime.datetime.now() - revision_date).days
if dayAge >= dayHorizon:
break
- if revision.message:
- item_title = u'%s' % revision.message.split('\n')[0]
+ if revision_dict['message']:
+ item_title = u'%s' % revision_dict['message'].split('\n')[0]
else:
- item_title = u'%s' % revision.id
- item_link = h.url_for(controller='revision', action='read', id=revision.id)
+ item_title = u'%s' % revision_dict['id']
+ item_link = h.url_for(controller='revision', action='read', id=revision_dict['id'])
item_description = _('Log message: ')
- item_description += '%s' % (revision.message or '')
- item_author_name = revision.author
- item_pubdate = revision.timestamp
+ item_description += '%s' % (revision_dict['message'] or '')
+ item_author_name = revision_dict['author']
+ item_pubdate = revision_date
feed.add_item(
title=item_title,
link=item_link,
@@ -462,7 +467,6 @@
)
feed.content_type = 'application/atom+xml'
return feed.writeString('utf-8')
- c.group_revisions = c.group.all_related_revisions
return render('group/history.html')
def _render_edit_form(self, fs):
--- a/ckan/controllers/package.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/controllers/package.py Thu Jul 28 12:55:51 2011 +0100
@@ -6,7 +6,6 @@
import re
from sqlalchemy.orm import eagerload_all
-from sqlalchemy import or_
import genshi
from pylons import config, cache
from pylons.i18n import get_lang, _
@@ -21,7 +20,7 @@
from ckan.lib.base import request, c, BaseController, model, abort, h, g, render
from ckan.lib.base import etag_cache, response, redirect, gettext
from ckan.authz import Authorizer
-from ckan.lib.search import query_for, SearchError
+from ckan.lib.search import SearchError
from ckan.lib.cache import proxy_cache
from ckan.lib.package_saver import PackageSaver, ValidationException
from ckan.lib.navl.dictization_functions import DataError, unflatten, validate
@@ -79,7 +78,7 @@
raise DataError(data_dict)
def _setup_template_variables(self, context, data_dict):
- c.groups = get.group_list_availible(context, data_dict)
+ c.groups = get.group_list_available(context, data_dict)
c.groups_authz = get.group_list_authz(context, data_dict)
c.licences = [('', '')] + model.Package.get_license_options()
c.is_sysadmin = Authorizer().is_sysadmin(c.user)
@@ -108,7 +107,6 @@
except ValueError, e:
abort(400, ('"page" parameter must be an integer'))
limit = 20
- query = query_for(model.Package)
# most search operations should reset the page counter:
params_nopage = [(k, v) for k,v in request.params.items() if k != 'page']
@@ -139,25 +137,30 @@
and len(value) and not param.startswith('_'):
c.fields.append((param, value))
- query.run(query=q,
- fields=c.fields,
- facet_by=g.facets,
- limit=limit,
- offset=(page-1)*limit,
- return_objects=True,
- filter_by_openness=c.open_only,
- filter_by_downloadable=c.downloadable_only,
- username=c.user)
-
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'q':q,
+ 'fields':c.fields,
+ 'facet_by':g.facets,
+ 'limit':limit,
+ 'offset':(page-1)*limit,
+ 'return_objects':True,
+ 'filter_by_openness':c.open_only,
+ 'filter_by_downloadable':c.downloadable_only,
+ }
+
+ query = get.package_search(context,data_dict)
+
c.page = h.Page(
- collection=query.results,
+ collection=query['results'],
page=page,
url=pager_url,
- item_count=query.count,
+ item_count=query['count'],
items_per_page=limit
)
- c.facets = query.facets
- c.page.items = query.results
+ c.facets = query['facets']
+ c.page.items = query['results']
except SearchError, se:
c.query_error = True
c.facets = {}
@@ -265,39 +268,50 @@
params['diff_entity'] = 'package'
h.redirect_to(controller='revision', action='diff', **params)
- c.pkg = model.Package.get(id)
- if not c.pkg:
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'extras_as_string': True,}
+ data_dict = {'id':id}
+ try:
+ c.pkg_dict = get.package_show(context, data_dict)
+ c.pkg_revisions = get.package_revision_list(context, data_dict)
+ #TODO: remove
+ # Still necessary for the authz check in group/layout.html
+ c.pkg = context['package']
+
+ except NotAuthorized:
+ abort(401, _('Unauthorized to read package %s') % '')
+ except NotFound:
abort(404, _('Package not found'))
+
format = request.params.get('format', '')
if format == 'atom':
# Generate and return Atom 1.0 document.
from webhelpers.feedgenerator import Atom1Feed
feed = Atom1Feed(
title=_(u'CKAN Package Revision History'),
- link=h.url_for(controller='revision', action='read', id=c.pkg.name),
- description=_(u'Recent changes to CKAN Package: ') + (c.pkg.title or ''),
+ link=h.url_for(controller='revision', action='read', id=c.pkg_dict['name']),
+ description=_(u'Recent changes to CKAN Package: ') + (c.pkg_dict['title'] or ''),
language=unicode(get_lang()),
)
- for revision, obj_rev in c.pkg.all_related_revisions:
+ for revision_dict in c.pkg_revisions:
+ revision_date = h.date_str_to_datetime(revision_dict['timestamp'])
try:
dayHorizon = int(request.params.get('days'))
except:
dayHorizon = 30
- try:
- dayAge = (datetime.now() - revision.timestamp).days
- except:
- dayAge = 0
+ dayAge = (datetime.datetime.now() - revision_date).days
if dayAge >= dayHorizon:
break
- if revision.message:
- item_title = u'%s' % revision.message.split('\n')[0]
+ if revision_dict['message']:
+ item_title = u'%s' % revision_dict['message'].split('\n')[0]
else:
- item_title = u'%s' % revision.id
- item_link = h.url_for(controller='revision', action='read', id=revision.id)
+ item_title = u'%s' % revision_dict['id']
+ item_link = h.url_for(controller='revision', action='read', id=revision_dict['id'])
item_description = _('Log message: ')
- item_description += '%s' % (revision.message or '')
- item_author_name = revision.author
- item_pubdate = revision.timestamp
+ item_description += '%s' % (revision_dict['message'] or '')
+ item_author_name = revision_dict['author']
+ item_pubdate = revision_date
feed.add_item(
title=item_title,
link=item_link,
@@ -307,7 +321,6 @@
)
feed.content_type = 'application/atom+xml'
return feed.writeString('utf-8')
- c.pkg_revisions = c.pkg.all_related_revisions
return render('package/history.html')
def new(self, data=None, errors=None, error_summary=None):
@@ -395,21 +408,31 @@
def history_ajax(self, id):
- pkg = model.Package.get(id)
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'extras_as_string': True,}
+ data_dict = {'id':id}
+ try:
+ pkg_revisions = get.package_revision_list(context, data_dict)
+ except NotAuthorized:
+ abort(401, _('Unauthorized to read package %s') % '')
+ except NotFound:
+ abort(404, _('Package not found'))
+
+
data = []
approved = False
- for num, (revision, revision_obj) in enumerate(pkg.all_related_revisions):
- if not approved and revision.approved_timestamp:
+ for num, revision in enumerate(pkg_revisions):
+ if not approved and revision['approved_timestamp']:
current_approved, approved = True, True
else:
current_approved = False
- data.append({'revision_id': revision.id,
- 'message': revision.message,
- 'timestamp': format_datetime(revision.timestamp,
- locale=(get_lang() or ['en'])[0]),
- 'author': revision.author,
- 'approved': bool(revision.approved_timestamp),
+ data.append({'revision_id': revision['id'],
+ 'message': revision['message'],
+ 'timestamp': revision['timestamp'],
+ 'author': revision['author'],
+ 'approved': bool(revision['approved_timestamp']),
'current_approved': current_approved})
response.headers['Content-Type'] = 'application/json;charset=utf-8'
@@ -726,38 +749,24 @@
return render('package/authz.html')
-
-
-
- def rate(self, id):
- package_name = id
- package = model.Package.get(package_name)
- if package is None:
- abort(404, gettext('Package Not Found'))
- #self._clear_pkg_cache(package)
- rating = request.params.get('rating', '')
- if rating:
- try:
- ckan.rating.set_my_rating(c, package, rating)
- except ckan.rating.RatingValueException, e:
- abort(400, gettext('Rating value invalid'))
- h.redirect_to(controller='package', action='read', id=package_name, rating=str(rating))
-
def autocomplete(self):
q = unicode(request.params.get('q', ''))
if not len(q):
return ''
+
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'q':q}
+
+ packages = get.package_autocomplete(context,data_dict)
+
pkg_list = []
- like_q = u"%s%%" % q
- pkg_query = ckan.authz.Authorizer().authorized_query(c.user, model.Package)
- pkg_query = pkg_query.filter(or_(model.Package.name.ilike(like_q),
- model.Package.title.ilike(like_q)))
- pkg_query = pkg_query.limit(10)
- for pkg in pkg_query:
- if pkg.name.lower().startswith(q.lower()):
- pkg_list.append('%s|%s' % (pkg.name, pkg.name))
+ for pkg in packages:
+ if pkg['name'].lower().startswith(q.lower()):
+ pkg_list.append('%s|%s' % (pkg['name'], pkg['name']))
else:
- pkg_list.append('%s (%s)|%s' % (pkg.title.replace('|', ' '), pkg.name, pkg.name))
+ pkg_list.append('%s (%s)|%s' % (pkg['title'].replace('|', ' '), pkg['name'], pkg['name']))
return '\n'.join(pkg_list)
def _render_edit_form(self, fs, params={}, clear_session=False):
--- a/ckan/controllers/tag.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/controllers/tag.py Thu Jul 28 12:55:51 2011 +0100
@@ -7,6 +7,9 @@
from ckan.lib.cache import proxy_cache
from ckan.lib.helpers import AlphaPage, Page
+from ckan.logic import NotFound, NotAuthorized
+import ckan.logic.action.get as get
+
LIMIT = 25
class TagController(BaseController):
@@ -18,26 +21,32 @@
def index(self):
c.q = request.params.get('q', '')
-
+
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {}
+
if c.q:
page = int(request.params.get('page', 1))
- query = query_for('tag', backend='sql')
- query.run(query=c.q,
- limit=LIMIT,
- offset=(page-1)*LIMIT,
- return_objects=True,
- username=c.user)
+ data_dict['q'] = c.q
+ data_dict['limit'] = LIMIT
+ data_dict['offset'] = (page-1)*LIMIT
+ data_dict['return_objects'] = True
+
+ results = get.tag_list(context,data_dict)
+
+ if c.q:
c.page = h.Page(
- collection=query.results,
+ collection=results,
page=page,
- item_count=query.count,
+ item_count=len(results),
items_per_page=LIMIT
)
- c.page.items = query.results
+ c.page.items = results
else:
- query = model.Tag.all()
c.page = AlphaPage(
- collection=query,
+ collection=results,
page=request.params.get('page', 'A'),
alpha_attribute='name',
other_text=_('Other'),
@@ -47,10 +56,14 @@
@proxy_cache()
def read(self, id):
- query = model.Session.query(model.Tag)
- query = query.filter(model.Tag.name==id)
- c.tag = query.first()
- if c.tag is None:
- abort(404)
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author}
+
+ data_dict = {'id':id}
+ try:
+ c.tag = get.tag_show(context,data_dict)
+ except NotFound:
+ abort(404, _('Tag not found'))
+
return render('tag/read.html')
--- a/ckan/controllers/user.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/controllers/user.py Thu Jul 28 12:55:51 2011 +0100
@@ -1,12 +1,20 @@
import logging
import genshi
-from sqlalchemy import or_, func, desc
from urllib import quote
import ckan.misc
from ckan.lib.base import *
from ckan.lib import mailer
+from ckan.authz import Authorizer
+from ckan.lib.navl.dictization_functions import DataError, unflatten
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic import tuplize_dict, clean_dict, parse_params
+from ckan.logic.schema import user_new_form_schema, user_edit_form_schema
+
+import ckan.logic.action.get as get
+import ckan.logic.action.create as create
+import ckan.logic.action.update as update
log = logging.getLogger(__name__)
@@ -15,7 +23,30 @@
class UserController(BaseController):
- def index(self, id=None):
+ ## hooks for subclasses
+ new_user_form = 'user/new_user_form.html'
+ edit_user_form = 'user/edit_user_form.html'
+
+ def _new_form_to_db_schema(self):
+ return user_new_form_schema()
+
+ def _db_to_new_form_schema(self):
+ '''This is an interface to manipulate data from the database
+ into a format suitable for the form (optional)'''
+
+ def _edit_form_to_db_schema(self):
+ return user_edit_form_schema()
+
+ def _db_to_edit_form_schema(self):
+ '''This is an interface to manipulate data from the database
+ into a format suitable for the form (optional)'''
+
+ def _setup_template_variables(self, context):
+ c.is_sysadmin = Authorizer().is_sysadmin(c.user)
+
+ ## end hooks
+
+ def index(self):
LIMIT = 20
if not self.authorizer.am_authorized(c, model.Action.USER_READ, model.System):
@@ -25,25 +56,18 @@
c.q = request.params.get('q', '')
c.order_by = request.params.get('order_by', 'name')
- query = model.Session.query(model.User, func.count(model.User.id))
- if c.q:
- query = model.User.search(c.q, query)
+ context = {'model': model,
+ 'user': c.user or c.author}
- if c.order_by == 'edits':
- query = query.join((model.Revision, or_(
- model.Revision.author==model.User.name,
- model.Revision.author==model.User.openid
- )))
- query = query.group_by(model.User)
- query = query.order_by(desc(func.count(model.User.id)))
- else:
- query = query.group_by(model.User)
- query = query.order_by(model.User.name)
+ data_dict = {'q':c.q,
+ 'order_by':c.order_by}
+
+ users_list = get.user_list(context,data_dict)
c.page = h.Page(
- collection=query,
+ collection=users_list,
page=page,
- item_count=query.count(),
+ item_count=len(users_list),
items_per_page=LIMIT
)
return render('user/list.html')
@@ -51,21 +75,21 @@
def read(self, id=None):
if not self.authorizer.am_authorized(c, model.Action.USER_READ, model.System):
abort(401, _('Not authorized to see this page'))
- if id:
- user = model.User.get(id)
- else:
- user = c.userobj
- if not user:
+
+ context = {'model': model,
+ 'user': c.user or c.author}
+
+ data_dict = {'id':id,
+ 'user_obj':c.userobj}
+ try:
+ user_dict = get.user_show(context,data_dict)
+ except NotFound:
h.redirect_to(controller='user', action='login', id=None)
- c.read_user = user.display_name
- c.is_myself = user.name == c.user
- c.api_key = user.apikey
- c.about_formatted = self._format_about(user.about)
- revisions_q = model.Session.query(model.Revision
- ).filter_by(author=user.name)
- c.num_edits = user.number_of_edits()
- c.num_pkg_admin = user.number_administered_packages()
- c.activity = revisions_q.limit(20).all()
+
+ c.user_dict = user_dict
+ c.is_myself = user_dict['name'] == c.user
+ c.about_formatted = self._format_about(user_dict['about'])
+
return render('user/read.html')
def me(self):
@@ -74,41 +98,119 @@
user_ref = c.userobj.get_reference_preferred_for_uri()
h.redirect_to(controller='user', action='read', id=user_ref)
- def register(self):
- if not self.authorizer.am_authorized(c, model.Action.USER_CREATE, model.System):
- abort(401, _('Not authorized to see this page'))
- if request.method == 'POST':
- try:
- c.login = request.params.getone('login')
- c.fullname = request.params.getone('fullname')
+ def register(self, data=None, errors=None, error_summary=None):
+ return self.new(data, errors, error_summary)
+
+ def new(self, data=None, errors=None, error_summary=None):
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'schema': self._new_form_to_db_schema(),
+ 'save': 'save' in request.params}
+
+ auth_for_create = Authorizer().am_authorized(c, model.Action.USER_CREATE, model.System())
+ if not auth_for_create:
+ abort(401, _('Unauthorized to create a user'))
+
+ if context['save'] and not data:
+ return self._save_new(context)
+
+ data = data or {}
+ errors = errors or {}
+ error_summary = error_summary or {}
+ vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+
+ self._setup_template_variables(context)
+ c.form = render(self.new_user_form, extra_vars=vars)
+ return render('user/new.html')
+
+ def _save_new(self, context):
+ try:
+ data_dict = clean_dict(unflatten(
+ tuplize_dict(parse_params(request.params))))
+ context['message'] = data_dict.get('log_message', '')
+ user = create.user_create(context, data_dict)
+ h.redirect_to(controller='user', action='read', id=user['name'])
+ except NotAuthorized:
+ abort(401, _('Unauthorized to create user %s') % '')
+ except NotFound, e:
+ abort(404, _('User not found'))
+ except DataError:
+ abort(400, _(u'Integrity Error'))
+ except ValidationError, e:
+ errors = e.error_dict
+ error_summary = e.error_summary
+ return self.new(data_dict, errors, error_summary)
+
+ def edit(self, id, data=None, errors=None, error_summary=None):
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'preview': 'preview' in request.params,
+ 'save': 'save' in request.params,
+ 'schema': self._edit_form_to_db_schema(),
+ }
+ data_dict = {'id': id}
+
+ if (context['save'] or context['preview']) and not data:
+ return self._save_edit(id, context)
+
+ try:
+ old_data = get.user_show(context, data_dict)
+
+ schema = self._db_to_edit_form_schema()
+ if schema:
+ old_data, errors = validate(old_data, schema)
+
+ c.display_name = old_data.get('display_name')
+ c.user_name = old_data.get('name')
+
+ data = data or old_data
+
+ except NotAuthorized:
+ abort(401, _('Unauthorized to edit user %s') % '')
+
+ user_obj = context.get('user_obj')
+
+ if not (ckan.authz.Authorizer().is_sysadmin(unicode(c.user)) or c.user == user_obj.name):
+ abort(401, _('User %s not authorized to edit %s') % (str(c.user), id))
+
+ errors = errors or {}
+ vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+
+ self._setup_template_variables(context)
+
+ c.form = render(self.edit_user_form, extra_vars=vars)
+
+ return render('user/edit.html')
+
+ def _save_edit(self, id, context):
+ try:
+ data_dict = clean_dict(unflatten(
+ tuplize_dict(parse_params(request.params))))
+ context['message'] = data_dict.get('log_message', '')
+ data_dict['id'] = id
+ user = update.user_update(context, data_dict)
+
+ if context['preview']:
+ about = request.params.getone('about')
+ c.preview = self._format_about(about)
+ c.user_about = about
+ c.full_name = request.params.get('fullname','')
c.email = request.params.getone('email')
- except KeyError, e:
- abort(401, _('Missing parameter: %r') % e)
- 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 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:
- h.flash_error(ve)
- return render('user/register.html')
- user = model.User(name=c.login, fullname=c.fullname,
- email=c.email, password=password)
- model.Session.add(user)
- model.Session.commit()
- model.Session.remove()
- h.redirect_to('/login_generic?login=%s&password=%s' % (str(c.login), quote(password.encode('utf-8'))))
- return render('user/register.html')
+ return self.edit(id, data_dict)
+
+ h.redirect_to(controller='user', action='read', id=user['id'])
+ except NotAuthorized:
+ abort(401, _('Unauthorized to edit user %s') % id)
+ except NotFound, e:
+ abort(404, _('User not found'))
+ except DataError:
+ abort(400, _(u'Integrity Error'))
+ except ValidationError, e:
+ errors = e.error_dict
+ error_summary = e.error_summary
+ return self.edit(id, data_dict, errors, error_summary)
+
def login(self):
if 'error' in request.params:
@@ -116,11 +218,18 @@
return render('user/login.html')
def logged_in(self):
- if c.userobj:
- response.set_cookie("ckan_user", c.userobj.name)
- response.set_cookie("ckan_display_name", c.userobj.display_name)
- response.set_cookie("ckan_apikey", c.userobj.apikey)
- h.flash_success(_("Welcome back, %s") % c.userobj.display_name)
+ if c.user:
+ context = {'model': model,
+ 'user': c.user}
+
+ data_dict = {'id':c.user}
+
+ user_dict = get.user_show(context,data_dict)
+
+ response.set_cookie("ckan_user", user_dict['name'])
+ response.set_cookie("ckan_display_name", user_dict['display_name'])
+ response.set_cookie("ckan_apikey", user_dict['apikey'])
+ h.flash_success(_("Welcome back, %s") % user_dict['display_name'])
h.redirect_to(controller='user', action='me', id=None)
else:
h.flash_error('Login failed. Bad username or password.')
@@ -132,98 +241,84 @@
response.delete_cookie("ckan_display_name")
response.delete_cookie("ckan_apikey")
return render('user/logout.html')
-
- def edit(self, id=None):
- if id is not None:
- user = model.User.get(id)
- else:
- user = c.userobj
- if user is None:
- abort(404)
- currentuser = c.userobj
- if not (ckan.authz.Authorizer().is_sysadmin(unicode(c.user)) or user == currentuser):
- abort(401)
- c.userobj = user
- if not 'save' in request.params and not 'preview' in request.params:
- c.user_about = user.about
- c.user_fullname = user.fullname
- c.user_email = user.email
- elif 'preview' in request.params:
- about = request.params.getone('about')
- c.preview = self._format_about(about)
- c.user_about = about
- c.user_fullname = request.params.getone('fullname')
- c.user_email = request.params.getone('email')
- elif 'save' in request.params:
- try:
- 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:
- password = self._get_form_password()
- if password:
- user.password = password
- except ValueError, ve:
- h.flash_error(ve)
- return render('user/edit.html')
- except Exception, inst:
- model.Session.rollback()
- raise
- else:
- model.Session.commit()
- h.flash_notice(_("Your account has been updated."))
- response.set_cookie("ckan_display_name", user.display_name)
- h.redirect_to(controller='user', action='read', id=user.id)
-
- return render('user/edit.html')
def request_reset(self):
if request.method == 'POST':
id = request.params.get('user')
- user = model.User.get(id)
- if user is None and id and len(id)>2:
- q = model.User.search(id)
- if q.count() == 1:
- user = q.one()
- elif q.count() > 1:
- users = ' '.join([user.name for user in q])
- h.flash_error(_('"%s" matched several users') % (id))
- return render("user/request_reset.html")
- if user is None:
- h.flash_error(_('No such user: %s') % id)
- return render("user/request_reset.html")
+
+ context = {'model': model,
+ 'user': c.user}
+
+ data_dict = {'id':id}
+ user_obj = None
try:
- mailer.send_reset_link(user)
- h.flash_success(_('Please check your inbox for a reset code.'))
- redirect('/')
- except mailer.MailerException, e:
- h.flash_error(_('Could not send reset link: %s') % unicode(e))
+ user_dict = get.user_show(context,data_dict)
+ user_obj = context['user_obj']
+ except NotFound:
+ # Try searching the user
+ del data_dict['id']
+ data_dict['q'] = id
+
+ if id and len(id) > 2:
+ user_list = get.user_list(context,data_dict)
+ if len(user_list) == 1:
+ # This is ugly, but we need the user object for the mailer,
+ # and user_list does not return them
+ del data_dict['q']
+ data_dict['id'] = user_list[0]['id']
+ user_dict = get.user_show(context,data_dict)
+ user_obj = context['user_obj']
+ elif len(user_list) > 1:
+ h.flash_error(_('"%s" matched several users') % (id))
+ else:
+ h.flash_error(_('No such user: %s') % id)
+ else:
+ h.flash_error(_('No such user: %s') % id)
+
+ if user_obj:
+ try:
+ mailer.send_reset_link(user_obj)
+ h.flash_success(_('Please check your inbox for a reset code.'))
+ redirect('/')
+ except mailer.MailerException, e:
+ h.flash_error(_('Could not send reset link: %s') % unicode(e))
return render('user/request_reset.html')
def perform_reset(self, id):
- user = model.User.get(id)
- if user is None:
- abort(404)
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user}
+
+ data_dict = {'id':id}
+
+ try:
+ user_dict = get.user_show(context,data_dict)
+ user_obj = context['user_obj']
+ except NotFound, e:
+ abort(404, _('User not found'))
+
c.reset_key = request.params.get('key')
- if not mailer.verify_reset_link(user, c.reset_key):
- msg = _('Invalid reset key. Please try again.')
- h.flash_error(msg)
- abort(403, msg.encode('utf8'))
+ if not mailer.verify_reset_link(user_obj, c.reset_key):
+ h.flash_error(_('Invalid reset key. Please try again.'))
+ abort(403)
+
if request.method == 'POST':
try:
- user.password = self._get_form_password()
- model.Session.add(user)
- model.Session.commit()
+ context['reset_password'] = True
+ new_password = self._get_form_password()
+ user_dict['password'] = new_password
+ user_dict['reset_key'] = c.reset_key
+ user = update.user_update(context, user_dict)
+
h.flash_success(_("Your password has been reset."))
redirect('/')
+ except NotAuthorized:
+ h.flash_error(_('Unauthorized to edit user %s') % id)
+ except NotFound, e:
+ h.flash_error(_('User not found'))
+ except DataError:
+ h.flash_error(_(u'Integrity Error'))
+ except ValidationError, e:
+ h.flash_error(u'%r'% e.error_dict)
except ValueError, ve:
h.flash_error(unicode(ve))
return render('user/perform_reset.html')
--- a/ckan/lib/alphabet_paginate.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/lib/alphabet_paginate.py Thu Jul 28 12:55:51 2011 +0100
@@ -13,7 +13,7 @@
${c.page.pager()}
'''
from itertools import dropwhile
-
+import re
from sqlalchemy import __version__ as sqav
from sqlalchemy.orm.query import Query
from pylons.i18n import _
@@ -92,7 +92,18 @@
# regexp search
query = query.filter(attribute.op('~')(u'^[^a-zA-Z].*'))
query.order_by(attribute)
- return query.all()
+ return query.all()
+ elif isinstance(self.collection,list):
+ if self.item_count >= self.paging_threshold:
+ if self.page != self.other_text:
+ items = [x for x in self.collection if x[0:1].lower() == self.page.lower()]
+ else:
+ # regexp search
+ items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x)]
+ items.sort()
+ else:
+ items = self.collection
+ return items
else:
raise NotImplementedError
@@ -100,5 +111,7 @@
def item_count(self):
if isinstance(self.collection, Query):
return self.collection.count()
+ elif isinstance(self.collection,list):
+ return len(self.collection)
else:
raise NotImplementedError
--- a/ckan/lib/dictization/model_dictize.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py Thu Jul 28 12:55:51 2011 +0100
@@ -11,7 +11,7 @@
## package save
-def group_list_dictize(obj_list, context, sort_key=lambda x:x):
+def group_list_dictize(obj_list, context, sort_key=lambda x:x['display_name']):
active = context.get('active', True)
@@ -23,6 +23,10 @@
if active and obj.state not in ('active', 'pending'):
continue
+ group_dict['display_name'] = obj.display_name
+
+ group_dict['packages'] = len(obj.packages)
+
result_list.append(group_dict)
return sorted(result_list, key=sort_key)
@@ -155,15 +159,37 @@
def group_dictize(group, context):
result_dict = table_dictize(group, context)
+
+ result_dict['display_name'] = group.display_name
- result_dict["extras"] = extras_dict_dictize(
+ result_dict['extras'] = extras_dict_dictize(
group._extras, context)
- result_dict["packages"] = obj_list_dictize(
+ result_dict['packages'] = obj_list_dictize(
group.packages, context)
return result_dict
+def tag_dictize(tag, context):
+
+ result_dict = table_dictize(tag, context)
+
+ result_dict["packages"] = obj_list_dictize(
+ tag.packages_ordered, context)
+
+ return result_dict
+
+def user_dictize(user, context):
+
+ result_dict = table_dictize(user, context)
+
+ del result_dict['password']
+
+ result_dict['display_name'] = user.display_name
+ result_dict['number_of_edits'] = user.number_of_edits()
+ result_dict['number_administered_packages'] = user.number_administered_packages()
+
+ return result_dict
## conversion to api
@@ -183,6 +209,15 @@
dictized["packages"] = sorted([package["id"] for package in dictized["packages"]])
return dictized
+def tag_to_api1(tag, context):
+
+ dictized = tag_dictize(tag, context)
+ return sorted([package["name"] for package in dictized["packages"]])
+
+def tag_to_api2(tag, context):
+
+ dictized = tag_dictize(tag, context)
+ return sorted([package["id"] for package in dictized["packages"]])
def resource_dict_to_api(res_dict, package_id, context):
res_dict.pop("revision_id")
--- a/ckan/lib/dictization/model_save.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/lib/dictization/model_save.py Thu Jul 28 12:55:51 2011 +0100
@@ -297,6 +297,19 @@
return group
+def user_dict_save(user_dict, context):
+
+ model = context['model']
+ session = context['session']
+ user = context.get('user_obj')
+
+ User = model.User
+ if user:
+ user_dict['id'] = user.id
+
+ user = table_dict_save(user_dict, User, context)
+
+ return user
def package_api_to_dict(api1_dict, context):
--- a/ckan/lib/helpers.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/lib/helpers.py Thu Jul 28 12:55:51 2011 +0100
@@ -5,6 +5,7 @@
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to templates as 'h'.
"""
+from datetime import datetime
from webhelpers.html import escape, HTML, literal, url_escape
from webhelpers.html.tools import mail_to
from webhelpers.html.tags import *
@@ -16,7 +17,6 @@
from routes import url_for, redirect_to
from alphabet_paginate import AlphaPage
from lxml.html import fromstring
-import datetime
from ckan.i18n import get_available_locales
try:
@@ -29,6 +29,7 @@
except ImportError:
import simplejson as json
+ISO_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
class Message(object):
"""A message returned by ``Flash.pop_messages()``.
@@ -209,7 +210,7 @@
'''
from ckan import model
date_format = '%Y-%m-%d %H:%M'
- if isinstance(datetime_, datetime.datetime):
+ if isinstance(datetime_, datetime):
return datetime_.strftime(date_format)
elif isinstance(datetime_, basestring):
try:
@@ -222,3 +223,8 @@
else:
return ''
+def date_str_to_datetime(date_str, format=ISO_DATE_FORMAT):
+ return datetime.strptime(date_str, format)
+
+def time_ago_in_words_from_str(date_str, format=ISO_DATE_FORMAT, granularity='month'):
+ return date.time_ago_in_words(datetime.strptime(date_str, format), granularity=granularity)
--- a/ckan/lib/navl/dictization_functions.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/lib/navl/dictization_functions.py Thu Jul 28 12:55:51 2011 +0100
@@ -81,7 +81,7 @@
def make_full_schema(data, schema):
'''make schema by getting all valid combinations and making sure that all keys
- are availible'''
+ are available'''
flattented_schema = flatten_schema(schema)
--- a/ckan/logic/action/create.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/logic/action/create.py Thu Jul 28 12:55:51 2011 +0100
@@ -14,15 +14,17 @@
from ckan.lib.dictization.model_save import (group_api_to_dict,
group_dict_save,
package_api_to_dict,
- package_dict_save)
+ package_dict_save,
+ user_dict_save)
from ckan.lib.dictization.model_dictize import (group_dictize,
- package_dictize)
+ package_dictize,
+ user_dictize)
from ckan.logic.schema import default_create_package_schema, default_resource_schema
-from ckan.logic.schema import default_group_schema
+from ckan.logic.schema import default_group_schema, default_user_schema
from ckan.lib.navl.dictization_functions import validate
from ckan.logic.action.update import (_update_package_relationship,
package_error_summary,
@@ -213,6 +215,36 @@
'rating count': len(package.ratings)}
return ret_dict
+def user_create(context, data_dict):
+ '''Creates a new user'''
+
+ model = context['model']
+ user = context['user']
+ schema = context.get('schema') or default_user_schema()
+
+ check_access(model.System(), model.Action.USER_CREATE, context)
+
+ data, errors = validate(data_dict, schema, context)
+
+ if errors:
+ model.Session.rollback()
+ raise ValidationError(errors, group_error_summary(errors))
+
+ rev = model.repo.new_revision()
+ rev.author = user
+
+ if 'message' in context:
+ rev.message = context['message']
+ else:
+ rev.message = _(u'REST API: Create user %s') % data.get('name')
+
+ user = user_dict_save(data, context)
+
+ model.repo.commit()
+ context['user'] = user
+ context['id'] = user.id
+ log.debug('Created user %s' % str(user.name))
+ return user_dictize(user, context)
## Modifications for rest api
--- a/ckan/logic/action/get.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/logic/action/get.py Thu Jul 28 12:55:51 2011 +0100
@@ -1,4 +1,6 @@
from sqlalchemy.sql import select
+from sqlalchemy import or_, func, desc
+
from ckan.logic import NotFound, check_access
from ckan.model import Session
from ckan.plugins import (PluginImplementations,
@@ -7,13 +9,20 @@
import ckan.authz
from ckan.lib.dictization import table_dictize
-from ckan.lib.dictization.model_dictize import group_to_api1, group_to_api2
+from ckan.lib.dictization.model_dictize import (package_dictize,
+ resource_list_dictize,
+ group_dictize,
+ group_list_dictize,
+ tag_dictize,
+ user_dictize)
+
from ckan.lib.dictization.model_dictize import (package_to_api1,
package_to_api2,
- package_dictize,
- resource_list_dictize,
- group_dictize)
-
+ group_to_api1,
+ group_to_api2,
+ tag_to_api1,
+ tag_to_api2)
+from ckan.lib.search import query_for
def package_list(context, data_dict):
'''Lists the package by name'''
@@ -81,18 +90,32 @@
revision_dicts = []
for revision, object_revisions in pkg.all_related_revisions:
revision_dicts.append(model.revision_as_dict(revision,
- include_packages=False))
+ include_packages=False,
+ include_groups=False))
return revision_dicts
def group_list(context, data_dict):
- model = context["model"]
- user = context["user"]
+ '''Returns a list of groups'''
+
+ model = context['model']
+ user = context['user']
api = context.get('api_version') or '1'
ref_group_by = 'id' if api == '2' else 'name';
+ all_fields = data_dict.get('all_fields',None)
+
query = ckan.authz.Authorizer().authorized_query(user, model.Group)
- groups = query.all()
- return [getattr(p, ref_group_by) for p in groups]
+ query = query.order_by(model.Group.name.asc())
+ query = query.order_by(model.Group.title.asc())
+
+ groups = query.all()
+
+ if not all_fields:
+ group_list = [getattr(p, ref_group_by) for p in groups]
+ else:
+ group_list = group_list_dictize(groups,context)
+
+ return group_list
def group_list_authz(context, data_dict):
model = context['model']
@@ -103,7 +126,7 @@
groups = set(query.all())
return dict((group.id, group.name) for group in groups)
-def group_list_availible(context, data_dict):
+def group_list_available(context, data_dict):
model = context['model']
user = context['user']
pkg = context.get('package')
@@ -116,6 +139,21 @@
return [(group.id, group.name) for group in groups]
+def group_revision_list(context, data_dict):
+ model = context['model']
+ id = data_dict['id']
+ group = model.Group.get(id)
+ if group is None:
+ raise NotFound
+ check_access(group, model.Action.READ, context)
+
+ revision_dicts = []
+ for revision, object_revisions in group.all_related_revisions:
+ revision_dicts.append(model.revision_as_dict(revision,
+ include_packages=False,
+ include_groups=False))
+ return revision_dicts
+
def licence_list(context, data_dict):
model = context["model"]
license_register = model.Package.get_license_register()
@@ -124,11 +162,71 @@
return licences
def tag_list(context, data_dict):
- model = context["model"]
- tags = model.Session.query(model.Tag).all() #TODO
- tag_list = [tag.name for tag in tags]
+ '''Returns a list of tags'''
+
+ model = context['model']
+ user = context['user']
+
+ all_fields = data_dict.get('all_fields',None)
+
+ q = data_dict.get('q','')
+ if q:
+ limit = data_dict.get('limit',25)
+ offset = data_dict.get('offset',0)
+ return_objects = data_dict.get('return_objects',True)
+
+ query = query_for(model.Tag, backend='sql')
+ query.run(query=q,
+ limit=limit,
+ offset=offset,
+ return_objects=return_objects,
+ username=user)
+ tags = query.results
+ else:
+ tags = model.Session.query(model.Tag).all()
+
+ tag_list = []
+ if all_fields:
+ for tag in tags:
+ result_dict = tag_dictize(tag, context)
+ tag_list.append(result_dict)
+ else:
+ tag_list = [tag.name for tag in tags]
+
return tag_list
+def user_list(context, data_dict):
+ '''Lists the current users'''
+ model = context['model']
+ user = context['user']
+
+ q = data_dict.get('q','')
+ order_by = data_dict.get('order_by','name')
+
+ query = model.Session.query(model.User, func.count(model.User.id))
+ if q:
+ query = model.User.search(q, query)
+
+ if order_by == 'edits':
+ query = query.join((model.Revision, or_(
+ model.Revision.author==model.User.name,
+ model.Revision.author==model.User.openid
+ )))
+ query = query.group_by(model.User)
+ query = query.order_by(desc(func.count(model.User.id)))
+ else:
+ query = query.group_by(model.User)
+ query = query.order_by(model.User.name)
+
+ users_list = []
+
+ for user in query.all():
+ result_dict = user_dictize(user[0], context)
+ del result_dict['apikey']
+ users_list.append(result_dict)
+
+ return users_list
+
def package_relationships_list(context, data_dict):
##TODO needs to work with dictization layer
@@ -201,6 +299,8 @@
return rev_dict
def group_show(context, data_dict):
+ '''Shows group details'''
+
model = context['model']
id = data_dict['id']
api = context.get('api_version') or '1'
@@ -211,6 +311,7 @@
if group is None:
raise NotFound
+
check_access(group, model.Action.READ, context)
group_dict = group_dictize(group, context)
@@ -222,17 +323,57 @@
def tag_show(context, data_dict):
+ '''Shows tag details'''
+
model = context['model']
api = context.get('api_version') or '1'
id = data_dict['id']
- ref_package_by = 'id' if api == '2' else 'name'
- obj = model.Tag.get(id) #TODO tags
- if obj is None:
+
+ tag = model.Tag.get(id)
+ context['tag'] = tag
+
+ if tag is None:
raise NotFound
- package_list = [getattr(pkgtag.package, ref_package_by)
- for pkgtag in obj.package_tags]
- return package_list
+ tag_dict = tag_dictize(tag,context)
+ extended_packages = []
+ for package in tag_dict['packages']:
+ extended_packages.append(_extend_package_dict(package,context))
+
+ tag_dict['packages'] = extended_packages
+
+ return tag_dict
+
+def user_show(context, data_dict):
+ '''Shows user details'''
+ model = context['model']
+
+ id = data_dict.get('id',None)
+ provided_user = data_dict.get('user_obj',None)
+ if id:
+ user = model.User.get(id)
+ context['user_obj'] = user
+ if user is None:
+ raise NotFound
+ elif provided_user:
+ context['user_obj'] = user = provided_user
+ else:
+ raise NotFound
+
+ user_dict = user_dictize(user,context)
+
+ revisions_q = model.Session.query(model.Revision
+ ).filter_by(author=user.name)
+
+ revisions_list = []
+ for revision in revisions_q.limit(20).all():
+ revision_dict = revision_show(context,{'id':revision.id})
+ revision_dict['state'] = revision.state
+ revisions_list.append(revision_dict)
+
+ user_dict['activity'] = revisions_list
+
+ return user_dict
def package_show_rest(context, data_dict):
@@ -261,3 +402,141 @@
return group_dict
+def tag_show_rest(context, data_dict):
+
+ tag_show(context, data_dict)
+ api = context.get('api_version') or '1'
+ tag = context['tag']
+
+ if api == '2':
+ tag_dict = tag_to_api2(tag, context)
+ else:
+ tag_dict = tag_to_api1(tag, context)
+
+ return tag_dict
+
+def package_autocomplete(context, data_dict):
+ '''Returns packages containing the provided string'''
+ model = context['model']
+ session = context['session']
+ user = context['user']
+ q = data_dict['q']
+
+ like_q = u"%s%%" % q
+
+ #TODO: Auth
+ pkg_query = ckan.authz.Authorizer().authorized_query(user, model.Package)
+ pkg_query = session.query(model.Package) \
+ .filter(or_(model.Package.name.ilike(like_q),
+ model.Package.title.ilike(like_q)))
+ pkg_query = pkg_query.limit(10)
+
+ pkg_list = []
+ for package in pkg_query:
+ result_dict = table_dictize(package, context)
+ pkg_list.append(result_dict)
+
+ return pkg_list
+
+def tag_autocomplete(context, data_dict):
+ '''Returns tags containing the provided string'''
+ model = context['model']
+ session = context['session']
+ user = context['user']
+
+ q = data_dict.get('q',None)
+ if not q:
+ return []
+
+ limit = data_dict.get('limit',10)
+
+ like_q = u"%s%%" % q
+
+ query = query_for('tag', backend='sql')
+ query.run(query=like_q,
+ return_objects=True,
+ limit=10,
+ username=user)
+
+ return [tag.name for tag in query.results]
+
+def user_autocomplete(context, data_dict):
+ '''Returns users containing the provided string'''
+ model = context['model']
+ session = context['session']
+ user = context['user']
+ q = data_dict.get('q',None)
+ if not q:
+ return []
+
+ limit = data_dict.get('limit',20)
+
+ query = model.User.search(q).limit(limit)
+
+ user_list = []
+ for user in query.all():
+ result_dict = {}
+ for k in ['id', 'name', 'fullname']:
+ result_dict[k] = getattr(user,k)
+
+ user_list.append(result_dict)
+
+ return user_list
+
+def package_search(context, data_dict):
+ model = context['model']
+ session = context['session']
+ user = context['user']
+
+ q=data_dict.get('q','')
+ fields=data_dict.get('fields',[])
+ facet_by=data_dict.get('facet_by',[])
+ limit=data_dict.get('limit',20)
+ offset=data_dict.get('offset',0)
+ return_objects=data_dict.get('return_objects',False)
+ filter_by_openness=data_dict.get('filter_by_openness',False)
+ filter_by_downloadable=data_dict.get('filter_by_downloadable',False)
+
+ query = query_for(model.Package)
+ query.run(query=q,
+ fields=fields,
+ facet_by=facet_by,
+ limit=limit,
+ offset=offset,
+ return_objects=return_objects,
+ filter_by_openness=filter_by_openness,
+ filter_by_downloadable=filter_by_downloadable,
+ username=user)
+
+ results = []
+ for package in query.results:
+ result_dict = table_dictize(package, context)
+ result_dict = _extend_package_dict(result_dict,context)
+
+ results.append(result_dict)
+
+ return {
+ 'count': query.count,
+ 'facets': query.facets,
+ 'results': results
+ }
+
+def _extend_package_dict(package_dict,context):
+ model = context['model']
+
+ resources = model.Session.query(model.Resource)\
+ .join(model.ResourceGroup)\
+ .filter(model.ResourceGroup.package_id == package_dict['id'])\
+ .all()
+ if resources:
+ package_dict['resources'] = resource_list_dictize(resources, context)
+ else:
+ package_dict['resources'] = []
+ license_id = package_dict['license_id']
+ if license_id:
+ isopen = model.Package.get_license_register()[license_id].isopen()
+ package_dict['isopen'] = isopen
+ else:
+ package_dict['isopen'] = False
+
+ return package_dict
--- a/ckan/logic/action/update.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/logic/action/update.py Thu Jul 28 12:55:51 2011 +0100
@@ -12,14 +12,17 @@
resource_dictize,
group_dictize,
group_to_api1,
- group_to_api2)
+ group_to_api2,
+ user_dictize)
from ckan.lib.dictization.model_save import (group_api_to_dict,
package_api_to_dict,
group_dict_save,
+ user_dict_save,
package_dict_save,
resource_dict_save)
from ckan.logic.schema import (default_update_group_schema,
default_update_package_schema,
+ default_update_user_schema,
default_update_resource_schema)
from ckan.lib.navl.dictization_functions import validate
log = logging.getLogger(__name__)
@@ -360,6 +363,45 @@
return group_dictize(group, context)
+def user_update(context, data_dict):
+ '''Updates the user's details'''
+
+ model = context['model']
+ user = context['user']
+ preview = context.get('preview', False)
+ schema = context.get('schema') or default_update_user_schema()
+ id = data_dict['id']
+
+ user_obj = model.User.get(id)
+ context['user_obj'] = user_obj
+ if user_obj is None:
+ raise NotFound('User was not found.')
+
+ if not (ckan.authz.Authorizer().is_sysadmin(unicode(user)) or user == user_obj.name) and \
+ not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key):
+ raise NotAuthorized( _('User %s not authorized to edit %s') % (str(user), id))
+
+ data, errors = validate(data_dict, schema, context)
+ if errors:
+ model.Session.rollback()
+ raise ValidationError(errors, group_error_summary(errors))
+
+ if not preview:
+ rev = model.repo.new_revision()
+ rev.author = user
+ if 'message' in context:
+ rev.message = context['message']
+ else:
+ rev.message = _(u'REST API: Update user %s') % data.get('name')
+
+ user = user_dict_save(data, context)
+
+ if not preview:
+ model.repo.commit()
+ return user_dictize(user, context)
+
+ return data
+
## Modifications for rest api
def package_update_rest(context, data_dict):
--- a/ckan/logic/schema.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/logic/schema.py Thu Jul 28 12:55:51 2011 +0100
@@ -20,7 +20,13 @@
duplicate_extras_key,
ignore_not_admin,
no_http,
- tag_not_uppercase)
+ tag_not_uppercase,
+ user_name_validator,
+ user_password_validator,
+ user_both_passwords_entered,
+ user_passwords_match,
+ user_password_not_empty,
+ user_about_validator)
from formencode.validators import OneOf
import ckan.model
@@ -189,4 +195,44 @@
'state': [ignore],
}
+def default_user_schema():
+ schema = {
+ 'id': [ignore_missing, unicode],
+ 'name': [not_empty, unicode, user_name_validator],
+ 'fullname': [ignore_missing, unicode],
+ 'password': [user_password_validator, user_password_not_empty, ignore_missing, unicode],
+ 'email': [ignore_missing, unicode],
+ 'about': [ignore_missing, user_about_validator, unicode],
+ 'created': [ignore],
+ 'openid': [ignore],
+ 'apikey': [ignore],
+ 'reset_key': [ignore],
+ }
+ return schema
+
+def user_new_form_schema():
+ schema = default_user_schema()
+
+ schema['password1'] = [unicode,user_both_passwords_entered,user_password_validator,user_passwords_match]
+ schema['password2'] = [unicode]
+
+ return schema
+
+def user_edit_form_schema():
+ schema = default_user_schema()
+
+ schema['name'] = [ignore_missing]
+ schema['password'] = [ignore_missing]
+ schema['password1'] = [ignore_missing,unicode,user_password_validator,user_passwords_match]
+ schema['password2'] = [ignore_missing,unicode]
+
+ return schema
+
+def default_update_user_schema():
+ schema = default_user_schema()
+
+ schema['name'] = [ignore_missing]
+ schema['password'] = [user_password_validator,ignore_missing, unicode]
+ return schema
+
--- a/ckan/logic/validators.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/logic/validators.py Thu Jul 28 12:55:51 2011 +0100
@@ -1,6 +1,6 @@
import re
from pylons.i18n import _, ungettext, N_, gettext
-from ckan.lib.navl.dictization_functions import Invalid, missing, unflatten
+from ckan.lib.navl.dictization_functions import Invalid, Missing, missing, unflatten
from ckan.authz import Authorizer
def package_id_not_changed(value, context):
@@ -168,3 +168,58 @@
data.pop(key)
+def user_name_validator(value,context):
+ model = context['model']
+
+ if not model.User.check_name_valid(value):
+ raise Invalid(
+ _('That login name is not valid. It must be at least 3 characters, restricted to alphanumerics and these symbols: %s') % '_\-'
+ )
+
+ if not model.User.check_name_available(value):
+ raise Invalid(
+ _("That login name is not available.")
+ )
+
+ return value
+
+def user_both_passwords_entered(key, data, errors, context):
+
+ password1 = data.get(('password1',),None)
+ password2 = data.get(('password2',),None)
+
+ if password1 is None or password1 == '' or \
+ password2 is None or password2 == '':
+ errors[('password',)].append(_('Please enter both passwords'))
+
+def user_password_validator(key, data, errors, context):
+ value = data[key]
+
+ if not value == '' and not isinstance(value, Missing) and not len(value) >= 4:
+ errors[('password',)].append(_('Your password must be 4 characters or longer'))
+
+def user_passwords_match(key, data, errors, context):
+
+ password1 = data.get(('password1',),None)
+ password2 = data.get(('password2',),None)
+
+ if not password1 == password2:
+ errors[key].append(_('The passwords you entered do not match'))
+ else:
+ #Set correct password
+ data[('password',)] = password1
+
+def user_password_not_empty(key, data, errors, context):
+ '''Only check if password is present if the user is created via action API.
+ If not, user_both_passwords_entered will handle the validation'''
+
+ if not ('password1',) in data and not ('password2',) in data:
+ password = data.get(('password',),None)
+ if not password:
+ errors[key].append(_('Missing value'))
+
+def user_about_validator(value,context):
+ if 'http://' in value or 'https://' in value:
+ raise Invalid(_('Edit not allowed as it looks like spam. Please avoid links in your description.'))
+
+ return value
--- a/ckan/model/__init__.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/model/__init__.py Thu Jul 28 12:55:51 2011 +0100
@@ -236,16 +236,21 @@
'''
return t.isoformat()
-def revision_as_dict(revision, include_packages=True, ref_package_by='name'):
+def revision_as_dict(revision, include_packages=True, include_groups=True,ref_package_by='name'):
revision_dict = OrderedDict((
('id', revision.id),
('timestamp', strftimestamp(revision.timestamp)),
('message', revision.message),
('author', revision.author),
+ ('approved_timestamp',revision.approved_timestamp)
))
if include_packages:
revision_dict['packages'] = [getattr(pkg, ref_package_by) \
for pkg in revision.packages]
+ if include_groups:
+ revision_dict['groups'] = [getattr(grp, ref_package_by) \
+ for grp in revision.groups]
+
return revision_dict
def is_id(id_string):
--- a/ckan/templates/_util.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/_util.html Thu Jul 28 12:55:51 2011 +0100
@@ -30,7 +30,15 @@
${h.link_to(tag['name'], h.url_for(controller='tag', action='read', id=tag['name']))}
</li></ul>
-
+
+ <!--! List of tags: pass in a list of tag name and this renders the standard
+ tag listing -->
+ <ul py:def="tag_list_from_name(tags)" class="tags clearfix">
+ <li py:for="tag in tags">
+ ${h.link_to(tag, h.url_for(controller='tag', action='read', id=tag))}
+ </li>
+ </ul>
+
<!--! List of users: pass in a collection of users and this renders the standard
user listing --><ul py:def="user_list(users)" class="users">
@@ -153,7 +161,20 @@
</tr></py:for></table>
-
+
+ <!--! List of data package groups: pass in a collection of data package groups
+ and this renders the standard group listing. Same as the above, but using dictionaries -->
+ <table py:def="group_list_from_dict(groups)" class="groups">
+ <tr><th>Title</th><th>Number of packages</th><th>Description</th></tr>
+ <py:for each="group in groups">
+ <tr>
+ <td><a href="${h.url_for(controller='group', action='read', id=group['name'])}">${group['display_name']}</a></td>
+ <td>${group['packages']}</td>
+ <td>${h.truncate(group['description'], length=80, whole_word=True)}</td>
+ </tr>
+ </py:for>
+ </table>
+
<!--! List of authorization groups: pass in a collection of authorization groups and
this renders the standard group listing --><table py:def="authorization_group_list(authorization_groups)" class="authorization_groups">
@@ -376,4 +397,54 @@
</tr></table>
+
+ <table py:def="revision_list_from_dict(revisions, allow_compare=False)">
+ <tr>
+ <th>Revision</th><th>Timestamp</th><th>Author</th><th>Entity</th><th>Log Message</th>
+ </tr>
+ <tr
+ class="state-${revision['state']}"
+ py:for="revision in revisions"
+ >
+ <td>
+ ${
+ h.link_to(revision['id'],
+ h.url_for(
+ controller='revision',
+ action='read',
+ id=revision['id'])
+ )
+ }
+ <py:if test="c.revision_change_state_allowed">
+ <div class="actions">
+ <form
+ method="POST"
+ action="${h.url_for(controller='revision',
+ action='edit',
+ id=revision['id'])}"
+ >
+ <py:if test="revision['state']!='deleted'">
+ <button type="submit" name="action" value="delete">Delete</button>
+ </py:if>
+ <py:if test="revision['state']=='deleted'">
+ <button type="submit" name="action" value="undelete">Undelete</button>
+ </py:if>
+ </form>
+ </div>
+ </py:if>
+ </td>
+ <td>${revision['timestamp']}</td>
+ <td>${h.linked_user(revision['author'])}</td>
+ <td>
+ <py:for each="pkg in revision['packages']">
+ <a href="${h.url_for(controller='package', action='read', id=pkg)}">${pkg}</a>
+ </py:for>
+ <py:for each="grp in revision['groups']">
+ <a href="${h.url_for(controller='group', action='read', id=grp)}">${grp}</a>
+ </py:for>
+ </td>
+ <td>${revision['message']}</td>
+ </tr>
+ </table>
+
</html>
--- a/ckan/templates/group/history.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/group/history.html Thu Jul 28 12:55:51 2011 +0100
@@ -2,19 +2,19 @@
xmlns:xi="http://www.w3.org/2001/XInclude"
py:strip="">
- <py:def function="page_title">${c.group.display_name} - Groups - History</py:def>
+ <py:def function="page_title">${c.group_dict['display_name']} - Groups - History</py:def><div py:match="content" class="group"><h2 class="head">
- ${c.group.display_name}
+ ${c.group_dict['display_name']}
</h2><h3>
Revisions
<p class="atom-feed-link group-history-link"><a
- href="${url(controller='group', action='history', id=c.group.name, format='atom', days=7)}"
- title="${g.site_title} - Group History - ${c.group.name}"
+ href="${url(controller='group', action='history', id=c.group_dict['name'], format='atom', days=7)}"
+ title="${g.site_title} - Group History - ${c.group_dict['name']}"
>
Subscribe »</a></p>
@@ -28,24 +28,24 @@
Error: ${c.error}
</h3>
- <input type="hidden" name="group_name" value="${c.group.name}"/>
+ <input type="hidden" name="group_name" value="${c.group_dict['name']}"/><table><tr><th></th><th>Revision</th><th>Timestamp</th><th>Author</th><th>Log Message</th></tr>
- <py:for each="index, rev in enumerate([rev for rev, obj_revs in c.group_revisions])">
+ <py:for each="index, revision_dict in enumerate(c.group_revisions)"><tr><td nowrap="nowrap">
- ${h.radio("selected1", rev.id, checked=(index == 0))}
- ${h.radio("selected2", rev.id, checked=(index == len(c.group_revisions)-1))}
+ ${h.radio("selected1", revision_dict['id'], checked=(index == 0))}
+ ${h.radio("selected2", revision_dict['id'], checked=(index == len(c.group_revisions)-1))}
</td><td>
- <a href="${h.url_for(controller='revision',action='read',id=rev.id)}">${rev.id}</a>
+ <a href="${h.url_for(controller='revision',action='read',id=revision_dict['id'])}">${revision_dict['id']}</a></td>
- <td>${rev.timestamp}</td>
- <td>${h.linked_user(rev.author)}</td>
- <td>${rev.message}</td>
+ <td>${revision_dict['timestamp']}</td>
+ <td>${h.linked_user(revision_dict['author'])}</td>
+ <td>${revision_dict['message']}</td></tr></py:for></table>
@@ -55,7 +55,7 @@
<py:def function="optional_feed"><link rel="alternate" type="application/atom+xml" title="Group History"
- href="${url(controller='group', action='history', id=c.group.name, format='atom', days=7)}" />
+ href="${url(controller='group', action='history', id=c.group_dict['name'], format='atom', days=7)}" /></py:def><xi:include href="layout.html" />
--- a/ckan/templates/group/index.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/group/index.html Thu Jul 28 12:55:51 2011 +0100
@@ -11,7 +11,7 @@
<p i18n:msg="item_count">There are <strong>${c.page.item_count}</strong> groups.</p>
${c.page.pager()}
- ${group_list(c.page.items)}
+ ${group_list_from_dict(c.page.items)}
${c.page.pager()}
<py:choose test="">
--- a/ckan/templates/package/history.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/package/history.html Thu Jul 28 12:55:51 2011 +0100
@@ -1,8 +1,9 @@
<html xmlns:py="http://genshi.edgewall.org/"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
xmlns:xi="http://www.w3.org/2001/XInclude"
py:strip="">
- <py:def function="page_title">${c.pkg.title or c.pkg.name} - Data Packages - History</py:def>
+ <py:def function="page_title">${c.pkg_dict.get('title', c.pkg_dict['name'])} - Data Packages - History</py:def><!-- Sidebar --><py:match path="primarysidebar">
@@ -10,8 +11,8 @@
<h4>Updates</h4><p class="atom-feed-link package-history-link"><a
- href="${url(controller='package', action='history', id=c.pkg.name, format='atom', days=7)}"
- title="${g.site_title} - Package History - ${c.pkg.name}">
+ href="${url(controller='package', action='history', id=c.pkg_dict['name'], format='atom', days=7)}"
+ title="${g.site_title} - Package History - ${c.pkg_dict['name']}">
Subscribe »</a></p></li>
@@ -21,7 +22,7 @@
<!-- Title --><h2 class="head">
- ${c.pkg.title} - History
+ ${c.pkg_dict.get('title', c.pkg_dict['name'])} - History
</h2><h3>Revisions</h3>
@@ -34,25 +35,25 @@
Error: ${c.error}
</h3>
- <input type="hidden" name="pkg_name" value="${c.pkg.name}"/>
+ <input type="hidden" name="pkg_name" value="${c.pkg_dict['name']}"/><table><tr><th></th><th>Revision ID</th><th>Package with timestamp</th><th>Author</th><th>Log Message</th></tr>
- <py:for each="index, rev in enumerate([rev for rev, obj_revs in c.pkg_revisions])">
+ <py:for each="index, rev in enumerate(c.pkg_revisions)"><tr><td nowrap="nowrap">
${h.radio("selected1", rev.id, checked=(index == 0))}
${h.radio("selected2", rev.id, checked=(index == len(c.pkg_revisions)-1))}
</td><td>
- <a href="${h.url_for(controller='revision',action='read',id=rev.id)}" title="${rev.id}">${rev.id[:4]}…</a>
+ <a href="${h.url_for(controller='revision',action='read',id=rev['id'])}">${rev['id'][:4]}…</a></td><td>
- <a href="${h.url_for(controller='package',action='read',id='%s@%s' % (c.pkg.name, rev.timestamp))}" title="${'Read package as of %s' % rev.timestamp}">${h.render_datetime(rev.timestamp)}</a></td>
- <td>${h.linked_user(rev.author)}</td>
- <td>${rev.message}</td>
+ <a href="${h.url_for(controller='package',action='read',id='%s@%s' % (c.pkg_dict['name'], rev['timestamp']))}" title="${'Read package as of %s' % rev['timestamp']}">${h.render_datetime(rev['timestamp'])}</a></td>
+ <td>${h.linked_user(rev['author'])}</td>
+ <td>${rev['message']}</td></tr></py:for></table>
@@ -62,7 +63,7 @@
<py:def function="optional_feed"><link rel="alternate" type="application/atom+xml" title="Package History"
- href="${url(controller='package', action='history', id=c.pkg.name, format='atom', days=7)}" />
+ href="${url(controller='package', action='history', id=c.pkg_dict['name'], format='atom', days=7)}" /></py:def><xi:include href="layout.html" />
--- a/ckan/templates/package/search.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/package/search.html Thu Jul 28 12:55:51 2011 +0100
@@ -59,7 +59,7 @@
<py:if test="c.page.item_count == 0 and request.params"><p i18n:msg="">Would you like to <a href="${h.url_for(action='new', id=None)}">create a new package?</a></p></py:if>
- ${package_list(c.page.items)}
+ ${package_list_from_dict(c.page.items)}
${c.page.pager(q=c.q)}
</div>
--- a/ckan/templates/tag/index.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/tag/index.html Thu Jul 28 12:55:51 2011 +0100
@@ -27,7 +27,7 @@
</p>
${c.page.pager(q=c.q)}
- ${tag_list(c.page.items)}
+ ${tag_list_from_name(c.page.items)}
${c.page.pager(q=c.q)}
<p py:if="c.q">
--- a/ckan/templates/tag/read.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/tag/read.html Thu Jul 28 12:55:51 2011 +0100
@@ -3,12 +3,12 @@
xmlns:xi="http://www.w3.org/2001/XInclude"
py:strip="">
- <py:def function="page_title">${c.tag.name} - Tags</py:def>
+ <py:def function="page_title">${c.tag['name']} - Tags</py:def><div py:match="content">
- <h2>Tag: ${c.tag.name}</h2>
- <p i18n:msg="package_count, tagname">There are ${len(c.tag.packages_ordered)} packages tagged with <strong>${c.tag.name}</strong>:</p>
- ${package_list(c.tag.packages_ordered)}
+ <h2>Tag: ${c.tag['name']}</h2>
+ <p i18n:msg="package_count, tagname">There are ${len(c.tag['packages'])} packages tagged with <strong>${c.tag['name']}</strong>:</p>
+ ${package_list_from_dict(c.tag['packages'])}
</div><xi:include href="layout.html" />
--- a/ckan/templates/user/edit.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/user/edit.html Thu Jul 28 12:55:51 2011 +0100
@@ -11,51 +11,27 @@
<div py:match="content"><h2>
- Edit User: ${c.userobj.display_name} (${c.userobj.name})
+ Edit User: ${c.display_name} (${c.user_name})
<a href="#preview" py:if="c.preview">(skip to preview)</a></h2>
- <form id="user-edit" action="" method="post" class="simple-form"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- >
- <fieldset>
- <legend>Base details</legend>
- <label for="fullname">Full name:</label>
- <input name="fullname" value="${c.user_fullname}" /><br/>
-
- <label for="email">E-Mail:</label>
- <input name="email" value="${c.user_email}" /><br/>
- </fieldset>
- <fieldset>
- <legend>Change your password</legend>
- <label for="password1">Password:</label>
- <input type="password" name="password1" value="" />
- <br/>
- <label for="password2">Password (repeat):</label>
- <input type="password" name="password2" value="" />
- <br/>
- </fieldset>
- <label for="about">About user:</label>
- <textarea id="about" rows="5" name="about" cols="60">${c.user_about}</textarea>
- <p class="small" i18n:msg="">You can use <a href="http://daringfireball.net/projects/markdown/syntax">Markdown formatting</a> here.</p>
-
- <div>
- <input name="preview" type="submit" value="Preview" />
- ${h.submit('save', _('Save'))}
- </div>
- </form><div id="preview" style="margin-left: 20px;" py:if="c.preview"><hr /><h2>Preview</h2>
- <h4>Full name: ${c.user_fullname}</h4>
+ <h4>Full name: ${c.full_name}</h4>
+ <h4>Email: ${c.email}</h4><div style="border: 2px dashed red; padding: 5px;">
${c.preview}
</div></div>
+
+ ${Markup(c.form)}
+
+
</div>
+
<xi:include href="layout.html" /></html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/user/edit_user_form.html Thu Jul 28 12:55:51 2011 +0100
@@ -0,0 +1,42 @@
+<form id="user-edit" action="" method="post"
+ py:attrs="{'class':'has-errors'} if errors else {}"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<div class="error-explanation" py:if="error_summary">
+<h2>Errors in form</h2>
+<p>The form contains invalid entries:</p>
+<ul>
+ <li py:for="key, error in error_summary.items()">${"%s: %s" % (key, error)}</li>
+</ul>
+</div>
+ <fieldset>
+ <legend>Base details</legend>
+ <dl>
+ <dt><label for="fullname">Full name:</label></dt>
+ <dd><input type="text" name="fullname" value="${data.get('fullname','')}" /></dd>
+
+ <dt><label for="email">E-Mail:</label></dt>
+ <dd><input type="text" name="email" value="${data.get('email','')}" /></dd>
+ </dl>
+ </fieldset>
+ <fieldset>
+ <legend>Change your password</legend>
+ <dl>
+ <dt><label for="password1">Password:</label></dt>
+ <dd><input type="password" name="password1" value="" /></dd>
+ <dt><label for="password2">Password (repeat):</label></dt>
+ <dd><input type="password" name="password2" value="" /></dd>
+ </dl>
+ </fieldset>
+ <label for="about">About user:</label>
+ <textarea id="about" rows="5" name="about" cols="60">${data.get('about','')}</textarea>
+ <p class="small" i18n:msg="">You can use <a href="http://daringfireball.net/projects/markdown/syntax">Markdown formatting</a> here.</p>
+
+ <div>
+ <input name="preview" type="submit" value="Preview" />
+ ${h.submit('save', _('Save'))}
+ </div>
+</form>
+
--- a/ckan/templates/user/list.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/user/list.html Thu Jul 28 12:55:51 2011 +0100
@@ -25,25 +25,24 @@
<hr /><ul class="userlist">
- <li py:for="(user,count) in c.page.items" class="user">
+ <li py:for="user in c.page.items" class="user"><ul><li class="username">
- ${h.linked_user(user, maxlength=20)}
+ ${h.linked_user(user['name'], maxlength=20)}
</li><li class="created">
- Member for ${h.date.time_ago_in_words(user.created,
- granularity='month')}
+ Member for ${h.time_ago_in_words_from_str(user['created'],granularity='month')}
</li><li>
- <span class="edits" title="${user.number_of_edits()} Edits">
- ${user.number_of_edits()}
+ <span class="edits" title="${user['number_of_edits']} Edits">
+ ${user['number_of_edits']}
</span><span class="administered"
- title="${user.number_administered_packages()} Administered">
+ title="${user['number_administered_packages']} Administered"><span class="badge">
●
</span>
- ${user.number_administered_packages()}
+ ${user['number_administered_packages']}
</span></li></ul>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/user/new.html Thu Jul 28 12:55:51 2011 +0100
@@ -0,0 +1,35 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip="">
+
+ <py:def function="page_title">Register - User</py:def>
+
+ <py:def function="optional_head">
+ <link rel="stylesheet" href="${g.site_url}/css/forms.css" type="text/css" media="screen, print" />
+ </py:def>
+
+ <py:match path="primarysidebar">
+ <li class="widget-container widget_text">
+ <h2>Have an OpenID?</h2>
+ <p>
+ If you have an account with Google, Yahoo or one of many other
+ OpenID providers, you can log in without signing up.
+ </p>
+ <ul>
+ <li>${h.link_to(_('Log in now'), h.url_for(action='login'))}</li>
+ </ul>
+ </li>
+ </py:match>
+
+ <div py:match="content">
+ <h2>
+ Join the community
+ </h2>
+
+ ${Markup(c.form)}
+ </div>
+
+ <xi:include href="layout.html" />
+</html>
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/user/new_user_form.html Thu Jul 28 12:55:51 2011 +0100
@@ -0,0 +1,42 @@
+<form id="user-edit" action="" method="post"
+ py:attrs="{'class':'has-errors'} if errors else {}"
+ xmlns:i18n="http://genshi.edgewall.org/i18n"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<div class="error-explanation" py:if="error_summary">
+<h2>Errors in form</h2>
+<p>The form contains invalid entries:</p>
+<ul>
+ <li py:for="key, error in error_summary.items()">${"%s: %s" % (key, error)}</li>
+</ul>
+</div>
+
+<fieldset>
+ <legend i18n:msg="site_title">Register with CKAN</legend>
+ <dl>
+ <dt><label class="field_opt" for="name">Login:</label></dt>
+ <dd><input type="text" name="name" value="${data.get('name','')}" /></dd>
+ <dd class="instructions basic">3+ chars, using only 'a-z0-9' and '-_'</dd>
+ <dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
+
+ <dt><label class="field_opt" for="fullname">Full name (optional):</label></dt>
+ <dd><input type="text" name="fullname" value="${data.get('fullname','')}" /></dd>
+ <dd class="field_error" py:if="errors.get('fullname', '')">${errors.get('fullname', '')}</dd>
+
+ <dt><label class="field_opt" for="email">E-Mail (optional):</label></dt>
+ <dd><input type="text" name="email" value="${data.get('email','')}" /></dd>
+
+ <dt><label class="field_opt" for="password1">Password:</label></dt>
+ <dd><input type="password" name="password1" value="" /></dd>
+ <dd class="field_error" py:if="errors.get('password1', '')">${errors.get('password1', '')}</dd>
+
+ <dt><label class="field_opt" for="password2">Password (repeat):</label></dt>
+ <dd><input type="password" name="password2" value="" /></dd>
+
+ </dl>
+</fieldset>
+
+ <br />
+ <input id="save" name="save" type="submit" value="Save" />
+</form>
--- a/ckan/templates/user/read.html Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/templates/user/read.html Thu Jul 28 12:55:51 2011 +0100
@@ -2,18 +2,18 @@
xmlns:xi="http://www.w3.org/2001/XInclude"
py:strip="">
- <py:def function="page_title">${c.read_user} - User</py:def>
+ <py:def function="page_title">${c.user_dict['display_name']} - User</py:def><py:def function="body_class">user-view</py:def><div py:match="content">
- <h2>${c.read_user}</h2>
+ <h2>${c.user_dict['display_name']}</h2><py:if test="c.is_myself"><h3>My Account</h3><p>You are logged in.</p><ul>
- <li>Your API key is: ${c.api_key}</li>
+ <li>Your API key is: ${c.user_dict['apikey']}</li><li><a href="${h.url_for(controller='user', action='edit')}">Edit your profile</a></li><li><a href="${h.url_for('/user/logout')}">Log out</a></li></ul>
@@ -26,14 +26,14 @@
<div class="activity"><h3>Activity</h3><ul>
- <li><strong>Number of edits:</strong> ${c.num_edits}</li>
- <li><strong>Number of packages administered:</strong> ${c.num_pkg_admin}</li>
+ <li><strong>Number of edits:</strong> ${c.user_dict['number_of_edits']}</li>
+ <li><strong>Number of packages administered:</strong> ${c.user_dict['number_administered_packages']}</li></ul></div><div class="changes"><h3>Recent changes</h3>
- ${revision_list(c.activity)}
+ ${revision_list_from_dict(c.user_dict['activity'])}
</div></div>
--- a/ckan/templates/user/register.html Wed Jul 27 17:15:34 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-<html xmlns:py="http://genshi.edgewall.org/"
- xmlns:i18n="http://genshi.edgewall.org/i18n"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- py:strip="">
-
- <py:match path="primarysidebar">
- <li class="widget-container widget_text">
- <h2>Have an OpenID?</h2>
- <p>
- If you have an account with Google, Yahoo or one of many other
- OpenID providers, you can log in without signing up.
- </p>
- <ul>
- <li>${h.link_to(_('Log in now'), h.url_for(action='login'))}</li>
- </ul>
- </li>
- </py:match>
-
- <py:def function="page_title">Register - User</py:def>
-
- <div py:match="content">
- <h2>Join the community</h2>
-
- <form action="/user/register" method="post" class="simple-form" id="register_form">
- <fieldset>
- <legend i18n:msg="site_title">Register with CKAN</legend>
-
- <label for="login">Login:</label>
- <input name="login" value="${c.login}" />
- <br/>
- <label for="fullname">Full name (optional):</label>
- <input name="fullname" value="${c.fullname}" />
- <br/>
- <label for="email">E-Mail (optional):</label>
- <input name="email" value="${c.email}" />
- <br/>
- <label for="password1">Password:</label>
- <input type="password" name="password1" value="" />
- <br/>
- <label for="password2">Password (repeat):</label>
- <input type="password" name="password2" value="" />
- <br/>
- </fieldset>
- ${h.submit('signup', _('Sign up'))}
- </form>
- </div>
- <xi:include href="layout.html" />
-</html>
--- a/ckan/tests/functional/api/model/test_package.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/tests/functional/api/model/test_package.py Thu Jul 28 12:55:51 2011 +0100
@@ -476,7 +476,7 @@
res = self.app.get(self.offset('/rest/package/%s/revisions' % 'annakarenina'))
revisions = res.json
assert len(revisions) == 1, len(revisions)
- expected_keys = set(('id', 'message', 'author', 'timestamp'))
+ expected_keys = set(('id', 'message', 'author', 'timestamp', 'approved_timestamp'))
keys = set(revisions[0].keys())
assert_equal(keys, expected_keys)
--- a/ckan/tests/functional/api/test_action.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/tests/functional/api/test_action.py Thu Jul 28 12:55:51 2011 +0100
@@ -6,10 +6,25 @@
class TestAction(WsgiAppCase):
+ STATUS_200_OK = 200
+ STATUS_201_CREATED = 201
+ STATUS_400_BAD_REQUEST = 400
+ STATUS_403_ACCESS_DENIED = 403
+ STATUS_404_NOT_FOUND = 404
+ STATUS_409_CONFLICT = 409
+
+ sysadmin_user = None
+
+ normal_user = None
+
@classmethod
def setup_class(self):
CreateTestData.create()
+ self.sysadmin_user = model.User.get('testsysadmin')
+
+ self.normal_user = model.User.get('annafan')
+
@classmethod
def teardown_class(self):
model.repo.rebuild_db()
@@ -21,7 +36,14 @@
"success": True,
"result": ["annakarenina", "warandpeace"]}
- def test_02_create_update_package(self):
+ def test_02_package_autocomplete(self):
+ postparams = '%s=1' % json.dumps({'q':'a'})
+ res = self.app.post('/api/action/package_autocomplete', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj['success'] == True
+ assert res_obj['result'][0]['name'] == 'annakarenina'
+
+ def test_03_create_update_package(self):
package = {
'author': None,
@@ -70,3 +92,298 @@
package_created.pop('revision_timestamp')
assert package_updated == package_created#, (pformat(json.loads(res.body)), pformat(package_created['result']))
+ def test_04_user_list(self):
+ postparams = '%s=1' % json.dumps({})
+ res = self.app.post('/api/action/user_list', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Lists the current users'
+ assert res_obj['success'] == True
+ assert len(res_obj['result']) == 7
+ assert res_obj['result'][0]['name'] == 'annafan'
+ assert res_obj['result'][0]['about'] == 'I love reading Annakarenina. My site: <a href="http://anna.com">anna.com</a>'
+ assert not 'apikey' in res_obj['result'][0]
+
+ def test_05_user_show(self):
+ postparams = '%s=1' % json.dumps({'id':'annafan'})
+ res = self.app.post('/api/action/user_show', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Shows user details'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['name'] == 'annafan'
+ assert result['about'] == 'I love reading Annakarenina. My site: <a href="http://anna.com">anna.com</a>'
+ assert 'apikey' in result
+ assert 'activity' in result
+ assert 'created' in result
+ assert 'display_name' in result
+ assert 'number_administered_packages' in result
+ assert 'number_of_edits' in result
+
+ def test_06_tag_list(self):
+ postparams = '%s=1' % json.dumps({})
+ res = self.app.post('/api/action/tag_list', params=postparams)
+ assert json.loads(res.body) == {'help': 'Returns a list of tags',
+ 'success': True,
+ 'result': ['russian', 'tolstoy']}
+ #Get all fields
+ postparams = '%s=1' % json.dumps({'all_fields':True})
+ res = self.app.post('/api/action/tag_list', params=postparams)
+ res_obj = json.loads(res.body)
+ pprint(res_obj)
+ assert res_obj['success'] == True
+ assert res_obj['result'][0]['name'] == 'russian'
+ assert len(res_obj['result'][0]['packages']) == 3
+ assert res_obj['result'][1]['name'] == 'tolstoy'
+ assert len(res_obj['result'][1]['packages']) == 2
+ assert 'id' in res_obj['result'][0]
+ assert 'id' in res_obj['result'][1]
+
+ def test_07_tag_show(self):
+ postparams = '%s=1' % json.dumps({'id':'russian'})
+ res = self.app.post('/api/action/tag_show', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Shows tag details'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['name'] == 'russian'
+ assert 'id' in result
+ assert 'packages' in result and len(result['packages']) == 3
+ assert [package['name'] for package in result['packages']].sort() == ['annakarenina', 'warandpeace', 'moo'].sort()
+
+ def test_08_user_create_not_authorized(self):
+ postparams = '%s=1' % json.dumps({'name':'test_create_from_action_api', 'password':'testpass'})
+ res = self.app.post('/api/action/user_create', params=postparams,
+ status=self.STATUS_403_ACCESS_DENIED)
+ res_obj = json.loads(res.body)
+ assert res_obj == {'help': 'Creates a new user',
+ 'success': False,
+ 'error': {'message': 'Access denied', '__type': 'Authorization Error'}}
+
+ def test_09_user_create(self):
+ user_dict = {'name':'test_create_from_action_api',
+ 'about': 'Just a test user',
+ 'password':'testpass'}
+
+ postparams = '%s=1' % json.dumps(user_dict)
+ res = self.app.post('/api/action/user_create', params=postparams,
+ extra_environ={'Authorization': str(self.sysadmin_user.apikey)})
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Creates a new user'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['name'] == user_dict['name']
+ assert result['about'] == user_dict['about']
+ assert 'apikey' in result
+ assert 'created' in result
+ assert 'display_name' in result
+ assert 'number_administered_packages' in result
+ assert 'number_of_edits' in result
+ assert not 'password' in result
+
+ def test_10_user_create_parameters_missing(self):
+ user_dict = {}
+
+ postparams = '%s=1' % json.dumps(user_dict)
+ res = self.app.post('/api/action/user_create', params=postparams,
+ extra_environ={'Authorization': str(self.sysadmin_user.apikey)},
+ status=self.STATUS_409_CONFLICT)
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'error': {
+ '__type': 'Validation Error',
+ 'name': ['Missing value'],
+ 'password': ['Missing value']
+ },
+ 'help': 'Creates a new user',
+ 'success': False
+ }
+
+ def test_11_user_create_wrong_password(self):
+ user_dict = {'name':'test_create_from_action_api_2',
+ 'password':'tes'} #Too short
+
+ postparams = '%s=1' % json.dumps(user_dict)
+ res = self.app.post('/api/action/user_create', params=postparams,
+ extra_environ={'Authorization': str(self.sysadmin_user.apikey)},
+ status=self.STATUS_409_CONFLICT)
+
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'error': {
+ '__type': 'Validation Error',
+ 'password': ['Your password must be 4 characters or longer']
+ },
+ 'help': 'Creates a new user',
+ 'success': False
+ }
+
+ def test_12_user_update(self):
+ normal_user_dict = {'id': self.normal_user.id,
+ 'fullname': 'Updated normal user full name',
+ 'about':'Updated normal user about'}
+
+ sysadmin_user_dict = {'id': self.sysadmin_user.id,
+ 'fullname': 'Updated sysadmin user full name',
+ 'about':'Updated sysadmin user about'}
+
+ #Normal users can update themselves
+ postparams = '%s=1' % json.dumps(normal_user_dict)
+ res = self.app.post('/api/action/user_update', params=postparams,
+ extra_environ={'Authorization': str(self.normal_user.apikey)})
+
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Updates the user\'s details'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['id'] == self.normal_user.id
+ assert result['name'] == self.normal_user.name
+ assert result['fullname'] == normal_user_dict['fullname']
+ assert result['about'] == normal_user_dict['about']
+ assert 'apikey' in result
+ assert 'created' in result
+ assert 'display_name' in result
+ assert 'number_administered_packages' in result
+ assert 'number_of_edits' in result
+ assert not 'password' in result
+
+ #Sysadmin users can update themselves
+ postparams = '%s=1' % json.dumps(sysadmin_user_dict)
+ res = self.app.post('/api/action/user_update', params=postparams,
+ extra_environ={'Authorization': str(self.sysadmin_user.apikey)})
+
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Updates the user\'s details'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['id'] == self.sysadmin_user.id
+ assert result['name'] == self.sysadmin_user.name
+ assert result['fullname'] == sysadmin_user_dict['fullname']
+ assert result['about'] == sysadmin_user_dict['about']
+
+ #Sysadmin users can update all users
+ postparams = '%s=1' % json.dumps(normal_user_dict)
+ res = self.app.post('/api/action/user_update', params=postparams,
+ extra_environ={'Authorization': str(self.sysadmin_user.apikey)})
+
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Updates the user\'s details'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['id'] == self.normal_user.id
+ assert result['name'] == self.normal_user.name
+ assert result['fullname'] == normal_user_dict['fullname']
+ assert result['about'] == normal_user_dict['about']
+
+ #Normal users can not update other users
+ postparams = '%s=1' % json.dumps(sysadmin_user_dict)
+ res = self.app.post('/api/action/user_update', params=postparams,
+ extra_environ={'Authorization': str(self.normal_user.apikey)},
+ status=self.STATUS_403_ACCESS_DENIED)
+
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'error': {
+ '__type': 'Authorization Error',
+ 'message': 'Access denied'
+ },
+ 'help': 'Updates the user\'s details',
+ 'success': False
+ }
+
+ def test_13_group_list(self):
+ postparams = '%s=1' % json.dumps({})
+ res = self.app.post('/api/action/group_list', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'result': [
+ 'david',
+ 'roger'
+ ],
+ 'help': 'Returns a list of groups',
+ 'success': True
+ }
+
+ #Get all fields
+ postparams = '%s=1' % json.dumps({'all_fields':True})
+ res = self.app.post('/api/action/group_list', params=postparams)
+ res_obj = json.loads(res.body)
+
+ assert res_obj['success'] == True
+ assert res_obj['result'][0]['name'] == 'david'
+ assert res_obj['result'][0]['display_name'] == 'Dave\'s books'
+ assert res_obj['result'][0]['packages'] == 2
+ assert res_obj['result'][1]['name'] == 'roger'
+ assert res_obj['result'][1]['packages'] == 1
+ assert 'id' in res_obj['result'][0]
+ assert 'revision_id' in res_obj['result'][0]
+ assert 'state' in res_obj['result'][0]
+
+ def test_14_group_show(self):
+ postparams = '%s=1' % json.dumps({'id':'david'})
+ res = self.app.post('/api/action/group_show', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj['help'] == 'Shows group details'
+ assert res_obj['success'] == True
+ result = res_obj['result']
+ assert result['name'] == 'david'
+ assert result['title'] == result['display_name'] == 'Dave\'s books'
+ assert result['state'] == 'active'
+ assert 'id' in result
+ assert 'revision_id' in result
+ assert len(result['packages']) == 2
+
+ #Group not found
+ postparams = '%s=1' % json.dumps({'id':'not_present_in_the_db'})
+ res = self.app.post('/api/action/group_show', params=postparams,
+ status=self.STATUS_404_NOT_FOUND)
+
+ res_obj = json.loads(res.body)
+ pprint(res_obj)
+ assert res_obj == {
+ 'error': {
+ '__type': 'Not Found Error',
+ 'message': 'Not found'
+ },
+ 'help': 'Shows group details',
+ 'success': False
+ }
+
+ def test_15_tag_autocomplete(self):
+ #Empty query
+ postparams = '%s=1' % json.dumps({})
+ res = self.app.post('/api/action/tag_autocomplete', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'help': 'Returns tags containing the provided string',
+ 'result': [],
+ 'success': True
+ }
+
+ #Normal query
+ postparams = '%s=1' % json.dumps({'q':'r'})
+ res = self.app.post('/api/action/tag_autocomplete', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'help': 'Returns tags containing the provided string',
+ 'result': ['russian'],
+ 'success': True
+ }
+
+ def test_16_user_autocomplete(self):
+ #Empty query
+ postparams = '%s=1' % json.dumps({})
+ res = self.app.post('/api/action/user_autocomplete', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj == {
+ 'help': 'Returns users containing the provided string',
+ 'result': [],
+ 'success': True
+ }
+
+ #Normal query
+ postparams = '%s=1' % json.dumps({'q':'joe'})
+ res = self.app.post('/api/action/user_autocomplete', params=postparams)
+ res_obj = json.loads(res.body)
+ assert res_obj['result'][0]['name'] == 'joeadmin'
+ assert 'id','fullname' in res_obj['result'][0]
+
--- a/ckan/tests/functional/test_package.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/tests/functional/test_package.py Thu Jul 28 12:55:51 2011 +0100
@@ -1651,4 +1651,8 @@
extra_environ={'REMOTE_USER':c.user})
anna_hash = str(PackageController._pkg_cache_key(self.anna))
self.assert_equal(res.header_dict['ETag'], anna_hash)
-
+
+ def test_package_autocomplete(self):
+ query = 'a'
+ res = self.app.get('/package/autocomplete?q=%s' % query)
+ assert res.body == "annakarenina|annakarenina\nA Wonderful Story (warandpeace)|warandpeace"
--- a/ckan/tests/functional/test_user.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/tests/functional/test_user.py Thu Jul 28 12:55:51 2011 +0100
@@ -1,6 +1,7 @@
from routes import url_for
from nose.tools import assert_equal
+from pprint import pprint
from ckan.tests import search_related, CreateTestData
from ckan.tests.html_check import HtmlCheckMethods
from ckan.tests.pylons_controller import PylonsTestCase
@@ -191,12 +192,12 @@
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 = res.forms['user-edit']
+ fv['name'] = username
fv['fullname'] = fullname
fv['password1'] = password
fv['password2'] = password
- res = fv.submit('signup')
+ res = fv.submit('save')
# view user
assert res.status == 302, self.main_div(res).encode('utf8')
@@ -209,7 +210,6 @@
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))
@@ -229,12 +229,12 @@
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 = res.forms['user-edit']
+ fv['name'] = username
fv['fullname'] = fullname.encode('utf8')
fv['password1'] = password.encode('utf8')
fv['password2'] = password.encode('utf8')
- res = fv.submit('signup')
+ res = fv.submit('save')
# view user
assert res.status == 302, self.main_div(res).encode('utf8')
@@ -247,7 +247,6 @@
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))
@@ -264,13 +263,13 @@
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 = res.forms['user-edit']
fv['password1'] = password
fv['password2'] = password
- res = fv.submit('signup')
+ res = fv.submit('save')
assert res.status == 200, res
main_res = self.main_div(res)
- assert 'Please enter a login name' in main_res, main_res
+ assert 'Name: Missing value' in main_res, main_res
def test_user_create_bad_name(self):
# create/register user
@@ -281,15 +280,15 @@
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 = res.forms['user-edit']
+ fv['name'] = username
fv['password1'] = password
fv['password2'] = password
- res = fv.submit('signup')
+ res = fv.submit('save')
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)
+ self.check_named_element(main_res, 'input', 'name="name"', 'value="%s"' % username)
def test_user_create_bad_password(self):
# create/register user
@@ -300,15 +299,15 @@
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 = res.forms['user-edit']
+ fv['name'] = username
fv['password1'] = password
fv['password2'] = password
- res = fv.submit('signup')
+ res = fv.submit('save')
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)
+ self.check_named_element(main_res, 'input', 'name="name"', 'value="%s"' % username)
def test_user_create_without_password(self):
# create/register user
@@ -319,14 +318,54 @@
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 = res.forms['user-edit']
+ fv['name'] = username
# no password
- res = fv.submit('signup')
+ res = fv.submit('save')
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)
+ assert 'Password: Please enter both passwords' in main_res, main_res
+ self.check_named_element(main_res, 'input', 'name="name"', 'value="%s"' % username)
+
+ def test_user_create_only_one_password(self):
+ # create/register user
+ username = 'testcreate4'
+ password = u'testpassword'
+ 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['user-edit']
+ fv['name'] = username
+ fv['password1'] = password
+ # Only password1
+ res = fv.submit('save')
+ assert res.status == 200, res
+ main_res = self.main_div(res)
+ assert 'Password: Please enter both passwords' in main_res, main_res
+ self.check_named_element(main_res, 'input', 'name="name"', 'value="%s"' % username)
+
+ def test_user_invalid_password(self):
+ # create/register user
+ username = 'testcreate4'
+ password = u'tes' # Too short
+ 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['user-edit']
+ fv['name'] = username
+ fv['password1'] = password
+ fv['password2'] = password
+ res = fv.submit('save')
+ assert res.status == 200, res
+ main_res = self.main_div(res)
+ assert 'Password: Your password must be 4 characters or longer' in main_res, main_res
+ self.check_named_element(main_res, 'input', 'name="name"', 'value="%s"' % username)
def test_user_edit(self):
# create user
@@ -356,8 +395,6 @@
# preview
main_res = self.main_div(res)
assert 'Edit User: testedit' in main_res, main_res
- before_preview = main_res[:main_res.find('Preview')]
- assert new_about in before_preview, before_preview
in_preview = main_res[main_res.find('Preview'):]
assert new_about in in_preview, in_preview
--- a/ckan/tests/lib/test_dictization.py Wed Jul 27 17:15:34 2011 +0100
+++ b/ckan/tests/lib/test_dictization.py Thu Jul 28 12:55:51 2011 +0100
@@ -771,6 +771,7 @@
'extras': [{'key': u'genre', 'state': u'active', 'value': u'"horror"'},
{'key': u'media', 'state': u'active', 'value': u'"dvd"'}],
'name': u'help',
+ 'display_name': u'help',
'packages': [{'author': None,
'author_email': None,
'license_id': u'other-open',
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