[ckan-changes] commit/ckan: 4 new changesets
Bitbucket
commits-noreply at bitbucket.org
Mon Jul 25 13:46:15 UTC 2011
4 new changesets in ckan:
http://bitbucket.org/okfn/ckan/changeset/a0c9bbc76709/
changeset: a0c9bbc76709
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-22 14:57:30
summary: [logic] Move user index and read
affected #: 9 files (5.9 KB)
--- a/ckan/controllers/user.py Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/controllers/user.py Fri Jul 22 13:57:30 2011 +0100
@@ -1,13 +1,14 @@
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
+import ckan.logic.action.get as get
+
log = logging.getLogger(__name__)
def login_form():
@@ -15,7 +16,7 @@
class UserController(BaseController):
- def index(self, id=None):
+ def index(self):
LIMIT = 20
if not self.authorizer.am_authorized(c, model.Action.USER_READ, model.System):
@@ -25,25 +26,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 +45,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':c.userobj}
+
+ user_dict = get.user_show(context,data_dict)
+ if not user_dict:
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):
--- a/ckan/lib/dictization/model_dictize.py Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py Fri Jul 22 13:57:30 2011 +0100
@@ -166,6 +166,17 @@
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
--- a/ckan/lib/helpers.py Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/lib/helpers.py Fri Jul 22 13:57:30 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 *
@@ -209,3 +210,5 @@
else:
return ''
+def time_ago_in_words_from_str(date_str, format='%Y-%m-%dT%H:%M:%S.%f',granularity='month'):
+ return date.time_ago_in_words(datetime.strptime(date_str, format), granularity=granularity)
--- a/ckan/logic/action/get.py Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/logic/action/get.py Fri Jul 22 13:57:30 2011 +0100
@@ -1,5 +1,5 @@
from sqlalchemy.sql import select
-from sqlalchemy import or_
+from sqlalchemy import or_, func, desc
from ckan.logic import NotFound, check_access
from ckan.plugins import (PluginImplementations,
@@ -11,7 +11,8 @@
from ckan.lib.dictization.model_dictize import (package_dictize,
resource_list_dictize,
group_dictize,
- tag_dictize)
+ tag_dictize,
+ user_dictize)
from ckan.lib.dictization.model_dictize import (package_to_api1,
package_to_api2,
@@ -81,7 +82,8 @@
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):
@@ -146,6 +148,37 @@
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):
@@ -243,9 +276,8 @@
model = context['model']
api = context.get('api_version') or '1'
id = data_dict['id']
- #ref_package_by = 'id' if api == '2' else 'name'
- tag = model.Tag.get(id) #TODO tags
+ tag = model.Tag.get(id)
context['tag'] = tag
if tag is None:
@@ -259,10 +291,36 @@
tag_dict['packages'] = extended_packages
return tag_dict
- package_list = [getattr(pkgtag.package, ref_package_by)
- for pkgtag in obj.package_tags]
- return package_list
+def user_show(context, data_dict):
+ '''Shows user details'''
+ model = context['model']
+
+ id = data_dict.get('id',None)
+ provided_user = data_dict.get('user',None)
+ if id:
+ user = model.User.get(id)
+ context['user'] = user
+ elif provided_user:
+ context['user'] = user = provided_user
+ else:
+ return None
+
+ 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):
--- a/ckan/model/__init__.py Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/model/__init__.py Fri Jul 22 13:57:30 2011 +0100
@@ -226,7 +226,7 @@
def strftimestamp(t):
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)),
@@ -237,4 +237,8 @@
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
--- a/ckan/templates/_util.html Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/templates/_util.html Fri Jul 22 13:57:30 2011 +0100
@@ -384,4 +384,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/user/list.html Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/templates/user/list.html Fri Jul 22 13:57:30 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>
--- a/ckan/templates/user/read.html Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/templates/user/read.html Fri Jul 22 13:57:30 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/tests/functional/api/test_action.py Thu Jul 21 18:28:19 2011 +0100
+++ b/ckan/tests/functional/api/test_action.py Fri Jul 22 13:57:30 2011 +0100
@@ -77,4 +77,30 @@
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
+
http://bitbucket.org/okfn/ckan/changeset/92fafb868234/
changeset: 92fafb868234
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-22 16:29:19
summary: [logic] Add tests for tag actions
affected #: 2 files (1006 bytes)
--- a/ckan/logic/action/get.py Fri Jul 22 13:57:30 2011 +0100
+++ b/ckan/logic/action/get.py Fri Jul 22 15:29:19 2011 +0100
@@ -126,6 +126,7 @@
return licences
def tag_list(context, data_dict):
+ '''Lists tags by name'''
model = context['model']
user = context['user']
@@ -273,6 +274,8 @@
def tag_show(context, data_dict):
+ '''Shows tag details'''
+
model = context['model']
api = context.get('api_version') or '1'
id = data_dict['id']
--- a/ckan/tests/functional/api/test_action.py Fri Jul 22 13:57:30 2011 +0100
+++ b/ckan/tests/functional/api/test_action.py Fri Jul 22 15:29:19 2011 +0100
@@ -104,3 +104,21 @@
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': 'Lists tags by name',
+ 'success': True,
+ 'result': ['russian', 'tolstoy']}
+
+ 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()
http://bitbucket.org/okfn/ckan/changeset/b8162453f650/
changeset: b8162453f650
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-25 13:05:07
summary: Fix typo
affected #: 1 file (1 byte)
--- a/ckan/controllers/api.py Fri Jul 22 15:29:19 2011 +0100
+++ b/ckan/controllers/api.py Mon Jul 25 12:05:07 2011 +0100
@@ -181,7 +181,7 @@
return self._finish(403, 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))
http://bitbucket.org/okfn/ckan/changeset/4f1ca1eb2630/
changeset: 4f1ca1eb2630
branch: feature-1229-db-out-of-controllers
user: amercader
date: 2011-07-25 15:45:56
summary: [logic,forms] Move user create. The forms have also been migrated, using the new
schema and validators functions. TODO: edit user.
affected #: 11 files (12.1 KB)
--- a/ckan/controllers/user.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/controllers/user.py Mon Jul 25 14:45:56 2011 +0100
@@ -6,8 +6,14 @@
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_form_schema
import ckan.logic.action.get as get
+import ckan.logic.action.create as create
log = logging.getLogger(__name__)
@@ -16,6 +22,21 @@
class UserController(BaseController):
+ ## hooks for subclasses
+ user_form = 'user/new_user_form.html'
+
+ def _form_to_db_schema(self):
+ return user_form_schema()
+
+ def _db_to_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
@@ -51,8 +72,11 @@
data_dict = {'id':id,
'user':c.userobj}
-
- user_dict = get.user_show(context,data_dict)
+ 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)
@@ -68,52 +92,65 @@
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')
- 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'))))
+ def register(self, data=None, errors=None, error_summary=None):
+ return self.new(data, errors, error_summary)
- return render('user/register.html')
+ def new(self, data=None, errors=None, error_summary=None):
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author,
+ 'schema': self._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.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 login(self):
return render('user/login.html')
def logged_in(self):
if c.user:
- userobj = model.User.by_name(c.user)
- response.set_cookie("ckan_user", userobj.name)
- response.set_cookie("ckan_display_name", userobj.display_name)
- response.set_cookie("ckan_apikey", userobj.apikey)
- h.flash_success(_("Welcome back, %s") % userobj.display_name)
+ 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.')
--- a/ckan/lib/dictization/model_save.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/lib/dictization/model_save.py Mon Jul 25 14:45:56 2011 +0100
@@ -297,6 +297,19 @@
return group
+def user_dict_save(user_dict, context):
+
+ model = context['model']
+ session = context['session']
+ user = context.get('userobj')
+
+ 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/logic/action/create.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/logic/action/create.py Mon Jul 25 14:45:56 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,
@@ -194,6 +196,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 Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/logic/action/get.py Mon Jul 25 14:45:56 2011 +0100
@@ -304,6 +304,8 @@
if id:
user = model.User.get(id)
context['user'] = user
+ if user is None:
+ raise NotFound
elif provided_user:
context['user'] = user = provided_user
else:
@@ -311,7 +313,6 @@
user_dict = user_dictize(user,context)
-
revisions_q = model.Session.query(model.Revision
).filter_by(author=user.name)
--- a/ckan/logic/schema.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/logic/schema.py Mon Jul 25 14:45:56 2011 +0100
@@ -20,7 +20,12 @@
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)
from formencode.validators import OneOf
import ckan.model
@@ -185,4 +190,27 @@
'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, unicode],
+ 'created': [ignore],
+ 'openid': [ignore],
+ 'apikey': [ignore],
+ 'reset_key': [ignore],
+ }
+ return schema
+
+def user_form_schema():
+ schema = default_user_schema()
+
+ schema['password1'] = [unicode,user_both_passwords_entered,user_password_validator,user_passwords_match]
+ schema['password2'] = [unicode]
+
+ return schema
+
--- a/ckan/logic/validators.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/logic/validators.py Mon Jul 25 14:45:56 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,52 @@
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 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'))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/user/new.html Mon Jul 25 14:45:56 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 Mon Jul 25 14:45:56 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/register.html Mon Jul 25 12:05:07 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/test_action.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/tests/functional/api/test_action.py Mon Jul 25 14:45:56 2011 +0100
@@ -6,10 +6,22 @@
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
+
+
@classmethod
def setup_class(self):
CreateTestData.create()
+ self.sysadmin_user = model.User.get('testsysadmin')
+
@classmethod
def teardown_class(self):
model.repo.rebuild_db()
@@ -120,5 +132,71 @@
result = res_obj['result']
assert result['name'] == 'russian'
assert 'id' in result
- assert 'packages' in result and len(result['packages']) == 3
+ 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
+
+ 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
+ }
--- a/ckan/tests/functional/test_user.py Mon Jul 25 12:05:07 2011 +0100
+++ b/ckan/tests/functional/test_user.py Mon Jul 25 14:45:56 2011 +0100
@@ -185,12 +185,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['login'] = username
+ from pprint import pprint
+ 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')
@@ -203,7 +204,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))
@@ -223,12 +223,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')
@@ -241,7 +241,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))
@@ -258,13 +257,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
@@ -275,15 +274,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
@@ -294,15 +293,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
@@ -313,14 +312,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
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