[ckan-changes] commit/ckan: 2 new changesets
Bitbucket
commits-noreply at bitbucket.org
Wed Jul 27 11:29:24 UTC 2011
2 new changesets in ckan:
http://bitbucket.org/okfn/ckan/changeset/ced5b6ef1a6c/
changeset: ced5b6ef1a6c
branch: feature-1211-drupal
user: kindly
date: 2011-07-27 12:24:01
summary: [merge] default
affected #: 34 files (27.0 KB)
--- a/CHANGELOG.txt Thu Jul 21 16:04:07 2011 +0100
+++ b/CHANGELOG.txt Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/README.txt Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/ckan_nose_plugin.py Wed Jul 27 11:24:01 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/home.py Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/controllers/home.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/controllers/package.py Wed Jul 27 11:24:01 2011 +0100
@@ -171,24 +171,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)
@@ -727,7 +733,7 @@
package = model.Package.get(package_name)
if package is None:
abort(404, gettext('Package Not Found'))
- self._clear_pkg_cache(package)
+ #self._clear_pkg_cache(package)
rating = request.params.get('rating', '')
if rating:
try:
--- a/ckan/controllers/user.py Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/controllers/user.py Wed Jul 27 11:24:01 2011 +0100
@@ -111,15 +111,16 @@
return render('user/register.html')
def login(self):
+ if 'error' in request.params:
+ h.flash_error(request.params['error'])
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)
+ 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)
h.redirect_to(controller='user', action='me', id=None)
else:
h.flash_error('Login failed. Bad username or password.')
@@ -136,10 +137,10 @@
if id is not None:
user = model.User.get(id)
else:
- user = model.User.by_name(c.user)
+ user = c.userobj
if user is None:
abort(404)
- currentuser = model.User.by_name(c.user)
+ currentuser = c.userobj
if not (ckan.authz.Authorizer().is_sysadmin(unicode(c.user)) or user == currentuser):
abort(401)
c.userobj = user
@@ -188,8 +189,17 @@
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")
try:
mailer.send_reset_link(user)
h.flash_success(_('Please check your inbox for a reset code.'))
@@ -204,8 +214,9 @@
abort(404)
c.reset_key = request.params.get('key')
if not mailer.verify_reset_link(user, c.reset_key):
- h.flash_error(_('Invalid reset key. Please try again.'))
- abort(403)
+ msg = _('Invalid reset key. Please try again.')
+ h.flash_error(msg)
+ abort(403, msg.encode('utf8'))
if request.method == 'POST':
try:
user.password = self._get_form_password()
--- a/ckan/lib/dictization/model_dictize.py Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py Wed Jul 27 11:24:01 2011 +0100
@@ -74,7 +74,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
@@ -83,8 +87,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/helpers.py Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/lib/helpers.py Wed Jul 27 11:24:01 2011 +0100
@@ -16,6 +16,7 @@
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:
@@ -202,10 +203,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.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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/lib/mailer.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/lib/package_saver.py Wed Jul 27 11:24:01 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/migration/versions/039_add_expired_id_and_dates.py Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/migration/versions/039_add_expired_id_and_dates.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/model/__init__.py Wed Jul 27 11:24:01 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, ref_package_by='name'):
@@ -237,3 +247,8 @@
revision_dict['packages'] = [getattr(pkg, ref_package_by) \
for pkg in revision.packages]
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/public/css/ckan.css Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/public/css/ckan.css Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/templates/_util.html Wed Jul 27 11:24:01 2011 +0100
@@ -248,8 +248,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>
@@ -305,7 +305,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/package/history.html Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/templates/package/history.html Wed Jul 27 11:24:01 2011 +0100
@@ -38,7 +38,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([rev for rev, obj_revs in c.pkg_revisions])"><tr>
@@ -47,9 +47,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)}" title="${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.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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/templates/package/layout.html Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/templates/package/read.html Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/templates/package/search.html Wed Jul 27 11:24:01 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(c.page.items)}
--- a/ckan/templates/user/request_reset.html Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/templates/user/request_reset.html Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/tests/functional/test_home.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/tests/functional/test_package.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/tests/functional/test_user.py Wed Jul 27 11:24:01 2011 +0100
@@ -3,12 +3,17 @@
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
@@ -26,6 +31,7 @@
@classmethod
def teardown_class(self):
+ SmtpServerHarness.teardown_class()
model.repo.rebuild_db()
def teardown(self):
@@ -395,6 +401,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
@@ -444,3 +456,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 Wed Jul 27 11:24:01 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 Wed Jul 27 11:24:01 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 Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/ckan/tests/wsgi_ckanclient.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/fabfile.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/requires/lucid_present.txt Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/setup.py Wed Jul 27 11:24:01 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 Thu Jul 21 16:04:07 2011 +0100
+++ b/test-core.ini Wed Jul 27 11:24:01 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/af396a1b34d0/
changeset: af396a1b34d0
user: kindly
date: 2011-07-27 12:33:48
summary: [merge] feature-1211-drupal
affected #: 15 files (9.3 KB)
--- a/ckan/config/routing.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/config/routing.py Wed Jul 27 11:33:48 2011 +0100
@@ -104,6 +104,8 @@
map.connect('/api/rest', controller='api', action='index')
+ map.connect('/api/action/{logic_function}', controller='api', action='action')
+
map.connect('/api/rest/{register}', controller='api', action='list',
requirements=dict(register=register_list_str),
conditions=dict(method=['GET'])
--- a/ckan/controllers/api.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/controllers/api.py Wed Jul 27 11:33:48 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
@@ -30,6 +31,8 @@
}
class ApiController(BaseController):
+ _actions = {}
+
def __call__(self, environ, start_response):
self._identify_user()
if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
@@ -127,10 +130,45 @@
response_data = {}
response_data['version'] = ver or '1'
return self._finish_ok(response_data)
+
+ def action(self, logic_function):
+ function = get_action(logic_function)
+
+ context = {'model': model, 'session': model.Session, 'user': c.user}
+ model.Session()._context = context
+ return_dict = {'help': function.__doc__}
+
+ try:
+ request_data = self._get_request_data()
+ except ValueError, inst:
+
+ return self._finish_bad_request(
+ gettext('JSON Error: %s') % str(inst))
+ try:
+ result = function(context, request_data)
+ return_dict['success'] = True
+ return_dict['result'] = result
+ except DataError:
+ log.error('Format incorrect: %s' % request_data)
+ #TODO make better error message
+ return self._finish(400, _(u'Integrity Error') % request_data)
+ except NotAuthorized:
+ return_dict['error'] = {'__type': 'Authorization Error',
+ 'message': _('Access denied')}
+ return_dict['success'] = False
+ return self._finish(403, return_dict, content_type='json')
+ except ValidationError, e:
+ error_dict = e.error_dict
+ error_dict['__type'] = 'Validtion Error'
+ return_dict['error'] = error_dict
+ return_dict['success'] = False
+ log.error('Validation error: %r' % str(e.error_dict))
+ return self._finish(409, return_dict, content_type='json')
+ return self._finish_ok(return_dict)
def list(self, ver=None, register=None, subregister=None, id=None):
context = {'model': model, 'session': model.Session,
- 'user': c.user, 'id': id, 'api_version': ver}
+ 'user': c.user, 'api_version': ver}
log.debug('listing: %s' % context)
action_map = {
'revision': get.revision_list,
@@ -149,7 +187,7 @@
return self._finish_bad_request(
gettext('Cannot list entity of this type: %s') % register)
try:
- return self._finish_ok(action(context))
+ return self._finish_ok(action(context, {'id': id}))
except NotFound, e:
extra_msg = e.extra_msg
return self._finish_not_found(extra_msg)
@@ -166,8 +204,9 @@
}
context = {'model': model, 'session': model.Session, 'user': c.user,
- 'id': id, 'id2': id2, 'rel': subregister,
'api_version': ver}
+ data_dict = {'id': id, 'id2': id2, 'rel': subregister}
+
for type in model.PackageRelationship.get_all_types():
action_map[('package', type)] = get.package_relationships_list
log.debug('show: %s' % context)
@@ -180,7 +219,7 @@
gettext('Cannot read entity of this type: %s') % register)
try:
- return self._finish_ok(action(context))
+ return self._finish_ok(action(context, data_dict))
except NotFound, e:
extra_msg = e.extra_msg
return self._finish_not_found(extra_msg)
@@ -193,22 +232,22 @@
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():
action_map[('package', type)] = create.package_relationship_create
-
context = {'model': model, 'session': model.Session, 'user': c.user,
- 'id': id, 'id2': id2, 'rel': subregister,
'api_version': ver}
log.debug('create: %s' % (context))
try:
request_data = self._get_request_data()
+ data_dict = {'id': id, 'id2': id2, 'rel': subregister}
+ data_dict.update(request_data)
except ValueError, inst:
return self._finish_bad_request(
gettext('JSON Error: %s') % str(inst))
@@ -221,10 +260,10 @@
gettext('Cannot create new entity of this type: %s %s') % \
(register, subregister))
try:
- response_data = action(request_data, context)
+ response_data = action(context, data_dict)
location = None
- if "id" in context:
- location = str('%s/%s' % (request.path, context.get("id")))
+ if "id" in data_dict:
+ location = str('%s/%s' % (request.path, data_dict.get("id")))
return self._finish_ok(response_data,
resource_location=location)
except NotAuthorized:
@@ -243,19 +282,20 @@
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
context = {'model': model, 'session': model.Session, 'user': c.user,
- 'id': id, 'id2': id2, 'rel': subregister,
- 'api_version': ver}
+ 'api_version': ver, 'id': id}
log.debug('update: %s' % (context))
try:
request_data = self._get_request_data()
+ data_dict = {'id': id, 'id2': id2, 'rel': subregister}
+ data_dict.update(request_data)
except ValueError, inst:
return self._finish_bad_request(
gettext('JSON Error: %s') % str(inst))
@@ -267,7 +307,7 @@
gettext('Cannot update entity of this type: %s') % \
register.encode('utf-8'))
try:
- response_data = action(request_data, context)
+ response_data = action(context, data_dict)
return self._finish_ok(response_data)
except NotAuthorized:
return self._finish_not_authz()
@@ -510,3 +550,4 @@
resultSet["ResultSet"]["Result"].append(result)
return self._finish_ok(resultSet)
+
--- a/ckan/controllers/group.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/controllers/group.py Wed Jul 27 11:33:48 2011 +0100
@@ -115,13 +115,14 @@
'user': c.user or c.author, 'extras_as_string': True,
'save': 'save' in request.params,
'schema': self._form_to_db_schema(),
- 'id': id}
+ }
+ data_dict = {'id': id}
if context['save'] and not data:
return self._save_edit(id, context)
try:
- old_data = get.group_show(context)
+ old_data = get.group_show(context, data_dict)
c.grouptitle = old_data.get('title')
c.groupname = old_data.get('name')
schema = self._db_to_form_schema()
@@ -151,7 +152,7 @@
data_dict = clean_dict(unflatten(
tuplize_dict(parse_params(request.params))))
context['message'] = data_dict.get('log_message', '')
- group = create.group_create(data_dict, context)
+ group = create.group_create(context, data_dict)
h.redirect_to(controller='group', action='read', id=group['name'])
except NotAuthorized:
abort(401, _('Unauthorized to read group %s') % '')
@@ -169,7 +170,8 @@
data_dict = clean_dict(unflatten(
tuplize_dict(parse_params(request.params))))
context['message'] = data_dict.get('log_message', '')
- group = update.group_update(data_dict, context)
+ data_dict['id'] = id
+ group = update.group_update(context, data_dict)
h.redirect_to(controller='group', action='read', id=group['name'])
except NotAuthorized:
abort(401, _('Unauthorized to read group %s') % id)
--- a/ckan/controllers/home.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/controllers/home.py Wed Jul 27 11:33:48 2011 +0100
@@ -50,7 +50,8 @@
c.package_count = query.count
c.latest_packages = current_package_list_with_resources({'model': model,
'user': c.user,
- 'limit': 5})
+ 'limit': 5},
+ {})
return render('home/index.html', cache_key=cache_key,
cache_expire=cache_expires)
--- a/ckan/controllers/package.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/controllers/package.py Wed Jul 27 11:33:48 2011 +0100
@@ -16,6 +16,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
@@ -77,9 +78,9 @@
log.info('incorrect form fields posted')
raise DataError(data_dict)
- def _setup_template_variables(self, context):
- c.groups = get.group_list_availible(context)
- c.groups_authz = get.group_list_authz(context)
+ def _setup_template_variables(self, context, data_dict):
+ c.groups = get.group_list_availible(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)
c.resource_columns = model.Resource.get_columns()
@@ -174,13 +175,13 @@
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(),
- 'id': id}
+ 'schema': self._form_to_db_schema()}
+ data_dict = {'id': id}
# interpret @<revision_id> or @<date> suffix
split = id.split('@')
if len(split) == 2:
- context['id'], revision_ref = split
+ data_dict['id'], revision_ref = split
if model.is_id(revision_ref):
context['revision_id'] = revision_ref
else:
@@ -196,7 +197,7 @@
#check if package exists
try:
- c.pkg_dict = get.package_show(context)
+ c.pkg_dict = get.package_show(context, data_dict)
c.pkg = context['package']
except NotFound:
abort(404, _('Package not found'))
@@ -227,12 +228,11 @@
def comments(self, id):
context = {'model': model, 'session': model.Session,
'user': c.user or c.author, 'extras_as_string': True,
- 'schema': self._form_to_db_schema(),
- 'id': id}
+ 'schema': self._form_to_db_schema()}
#check if package exists
try:
- c.pkg_dict = get.package_show(context)
+ c.pkg_dict = get.package_show(context, {'id':id})
c.pkg = context['package']
except NotFound:
abort(404, _('Package not found'))
@@ -329,7 +329,7 @@
error_summary = error_summary or {}
vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
- self._setup_template_variables(context)
+ self._setup_template_variables(context, {'id': id})
c.form = render(self.package_form, extra_vars=vars)
return render('package/new.html')
@@ -339,14 +339,14 @@
'user': c.user or c.author, 'extras_as_string': True,
'preview': 'preview' in request.params,
'save': 'save' in request.params,
- 'id': id, 'moderated': config.get('moderated'),
+ 'moderated': config.get('moderated'),
'pending': True,
'schema': self._form_to_db_schema()}
if (context['save'] or context['preview']) and not data:
return self._save_edit(id, context)
try:
- old_data = get.package_show(context)
+ old_data = get.package_show(context, {'id':id})
schema = self._db_to_form_schema()
if schema:
old_data, errors = validate(old_data, schema)
@@ -365,19 +365,19 @@
errors = errors or {}
vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
- self._setup_template_variables(context)
+ self._setup_template_variables(context, {'id':'id'})
c.form = render(self.package_form, extra_vars=vars)
return render('package/edit.html')
def read_ajax(self, id, revision=None):
context = {'model': model, 'session': model.Session,
'user': c.user or c.author,
- 'id': id, 'extras_as_string': True,
+ 'extras_as_string': True,
'schema': self._form_to_db_schema(),
'revision_id': revision}
try:
- data = get.package_show(context)
+ data = get.package_show(context, {'id': id})
schema = self._db_to_form_schema()
if schema:
data, errors = validate(data, schema)
@@ -395,9 +395,6 @@
def history_ajax(self, id):
- context = {'model': model, 'session': model.Session,
- 'user': c.user or c.author,
- 'id': id, 'extras_as_string': True}
pkg = model.Package.get(id)
data = []
approved = False
@@ -424,7 +421,7 @@
tuplize_dict(parse_params(request.POST))))
self._check_data_dict(data_dict)
context['message'] = data_dict.get('log_message', '')
- pkg = create.package_create(data_dict, context)
+ pkg = get_action('package_create')(context, data_dict)
if context['preview']:
PackageSaver().render_package(pkg, context)
@@ -454,9 +451,10 @@
context['message'] = data_dict.get('log_message', '')
if not context['moderated']:
context['pending'] = False
- pkg = update.package_update(data_dict, context)
+ data_dict['id'] = id
+ pkg = get_action('package_update')(context, data_dict)
if request.params.get('save', '') == 'Approve':
- update.make_latest_pending_package_active(context)
+ update.make_latest_pending_package_active(context, data_dict)
c.pkg = context['package']
c.pkg_dict = pkg
--- a/ckan/lib/dictization/model_save.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/lib/dictization/model_save.py Wed Jul 27 11:33:48 2011 +0100
@@ -105,7 +105,7 @@
extra = old_extras[key]
if extra.state == 'deleted':
continue
- state = 'pending-deleted' if context.get('pending') else 'delete'
+ state = 'pending-deleted' if context.get('pending') else 'deleted'
extra.state = state
def group_extras_save(extras_dicts, context):
@@ -126,7 +126,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/logic/__init__.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/logic/__init__.py Wed Jul 27 11:33:48 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 Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/logic/action/create.py Wed Jul 27 11:33:48 2011 +0100
@@ -30,16 +30,17 @@
check_group_auth)
log = logging.getLogger(__name__)
-def package_create(data_dict, context):
+def package_create(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(data_dict, context)
+ check_group_auth(context, data_dict)
data, errors = validate(data_dict, schema, context)
@@ -76,7 +77,26 @@
else:
return data
-def resource_create(data_dict, context):
+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']
@@ -84,14 +104,13 @@
default_resource_schema(),
context)
-
-def package_relationship_create(data_dict, context):
+def package_relationship_create(context, data_dict):
model = context['model']
user = context['user']
- id = context["id"]
- id2 = context["id2"]
- rel_type = context["rel"]
+ id = data_dict["id"]
+ id2 = data_dict["id2"]
+ rel_type = data_dict["rel"]
api = context.get('api_version') or '1'
ref_package_by = 'id' if api == '2' else 'name'
@@ -124,7 +143,7 @@
relationship_dicts = rel.as_dict(ref_package_by=ref_package_by)
return relationship_dicts
-def group_create(data_dict, context):
+def group_create(context, data_dict):
model = context['model']
user = context['user']
schema = context.get('schema') or default_group_schema()
@@ -160,7 +179,7 @@
log.debug('Created object %s' % str(group.name))
return group_dictize(group, context)
-def rating_create(data_dict, context):
+def rating_create(context, data_dict):
model = context['model']
user = context.get("user")
@@ -197,12 +216,12 @@
## Modifications for rest api
-def package_create_rest(data_dict, context):
+def package_create_rest(context, data_dict):
api = context.get('api_version') or '1'
dictized_package = package_api_to_dict(data_dict, context)
- dictized_after = package_create(dictized_package, context)
+ dictized_after = package_create(context, dictized_package)
pkg = context['package']
@@ -211,14 +230,16 @@
else:
package_dict = package_to_api2(pkg, context)
+ data_dict['id'] = pkg.id
+
return package_dict
-def group_create_rest(data_dict, context):
+def group_create_rest(context, data_dict):
api = context.get('api_version') or '1'
dictized_group = group_api_to_dict(data_dict, context)
- dictized_after = group_create(dictized_group, context)
+ dictized_after = group_create(context, dictized_group)
group = context['group']
@@ -227,5 +248,7 @@
else:
group_dict = group_to_api2(group, context)
+ data_dict['id'] = group.id
+
return group_dict
--- a/ckan/logic/action/get.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/logic/action/get.py Wed Jul 27 11:33:48 2011 +0100
@@ -14,20 +14,21 @@
group_dictize)
-def package_list(context):
+def package_list(context, data_dict):
+ '''Lists the package by name'''
model = context["model"]
user = context["user"]
- api = context["api_version"]
+ api = context.get("api_version", '1')
ref_package_by = 'id' if api == '2' else 'name'
query = ckan.authz.Authorizer().authorized_query(user, model.Package)
packages = query.all()
return [getattr(p, ref_package_by) for p in packages]
-def current_package_list_with_resources(context):
+def current_package_list_with_resources(context, data_dict):
model = context["model"]
user = context["user"]
- limit = context.get("limit")
+ limit = data_dict.get("limit")
q = ckan.authz.Authorizer().authorized_query(user, model.PackageRevision)
q = q.filter(model.PackageRevision.state=='active')
@@ -56,15 +57,15 @@
package_list.append(result_dict)
return package_list
-def revision_list(context):
+def revision_list(context, data_dict):
model = context["model"]
revs = model.Session.query(model.Revision).all()
return [rev.id for rev in revs]
-def package_revision_list(context):
+def package_revision_list(context, data_dict):
model = context["model"]
- id = context["id"]
+ id = data_dict["id"]
pkg = model.Package.get(id)
if pkg is None:
raise NotFound
@@ -76,7 +77,7 @@
include_packages=False))
return revision_dicts
-def group_list(context):
+def group_list(context, data_dict):
model = context["model"]
user = context["user"]
api = context.get('api_version') or '1'
@@ -86,7 +87,7 @@
groups = query.all()
return [getattr(p, ref_group_by) for p in groups]
-def group_list_authz(context):
+def group_list_authz(context, data_dict):
model = context['model']
user = context['user']
pkg = context.get('package')
@@ -95,7 +96,7 @@
groups = set(query.all())
return dict((group.id, group.name) for group in groups)
-def group_list_availible(context):
+def group_list_availible(context, data_dict):
model = context['model']
user = context['user']
pkg = context.get('package')
@@ -108,30 +109,30 @@
return [(group.id, group.name) for group in groups]
-def licence_list(context):
+def licence_list(context, data_dict):
model = context["model"]
license_register = model.Package.get_license_register()
licenses = license_register.values()
licences = [l.as_dict() for l in licenses]
return licences
-def tag_list(context):
+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]
return tag_list
-def package_relationships_list(context):
+def package_relationships_list(context, data_dict):
##TODO needs to work with dictization layer
model = context['model']
user = context['user']
- id = context["id"]
- id2 = context.get("id2")
- rel = context.get("rel")
api = context.get('api_version') or '1'
+
+ id = data_dict["id"]
+ id2 = data_dict.get("id2")
+ rel = data_dict.get("rel")
ref_package_by = 'id' if api == '2' else 'name';
-
pkg1 = model.Package.get(id)
pkg2 = None
if not pkg1:
@@ -157,11 +158,11 @@
return relationship_dicts
-def package_show(context):
+def package_show(context, data_dict):
model = context['model']
api = context.get('api_version') or '1'
- id = context['id']
+ id = data_dict['id']
pkg = model.Package.get(id)
@@ -179,10 +180,10 @@
return package_dict
-def revision_show(context):
+def revision_show(context, data_dict):
model = context['model']
api = context.get('api_version') or '1'
- id = context['id']
+ id = data_dict['id']
ref_package_by = 'id' if api == '2' else 'name'
rev = model.Session.query(model.Revision).get(id)
@@ -192,9 +193,9 @@
ref_package_by=ref_package_by)
return rev_dict
-def group_show(context):
+def group_show(context, data_dict):
model = context['model']
- id = context['id']
+ id = data_dict['id']
api = context.get('api_version') or '1'
@@ -213,10 +214,10 @@
return group_dict
-def tag_show(context):
+def tag_show(context, data_dict):
model = context['model']
api = context.get('api_version') or '1'
- id = context['id']
+ id = data_dict['id']
ref_package_by = 'id' if api == '2' else 'name'
obj = model.Tag.get(id) #TODO tags
if obj is None:
@@ -226,12 +227,11 @@
return package_list
-def package_show_rest(context):
+def package_show_rest(context, data_dict):
- package_show(context)
+ package_show(context, data_dict)
api = context.get('api_version') or '1'
-
pkg = context['package']
if api == '1':
@@ -241,9 +241,9 @@
return package_dict
-def group_show_rest(context):
+def group_show_rest(context, data_dict):
- group_show(context)
+ group_show(context, data_dict)
api = context.get('api_version') or '1'
group = context['group']
--- a/ckan/logic/action/update.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/logic/action/update.py Wed Jul 27 11:33:48 2011 +0100
@@ -51,7 +51,7 @@
error_summary[_(prettify(key))] = error[0]
return error_summary
-def check_group_auth(data_dict, context):
+def check_group_auth(context, data_dict):
model = context['model']
pkg = context.get("package")
@@ -105,11 +105,11 @@
context['latest_revision_date'] = latest_rev.revision_timestamp
context['latest_revision'] = latest_rev.revision_id
-def make_latest_pending_package_active(context):
+def make_latest_pending_package_active(context, data_dict):
model = context['model']
session = model.Session
- id = context["id"]
+ id = data_dict["id"]
pkg = model.Package.get(id)
check_access(pkg, model.Action.EDIT, context)
@@ -146,26 +146,28 @@
session.remove()
-def package_update(data_dict, context):
+def package_update(context, data_dict):
model = context['model']
user = context['user']
- id = context["id"]
+
+ 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.'))
- context["id"] = pkg.id
+ data_dict["id"] = pkg.id
check_access(pkg, model.Action.EDIT, context)
data, errors = validate(data_dict, schema, context)
- check_group_auth(data, context)
+ check_group_auth(context, data)
if errors:
model.Session.rollback()
@@ -188,6 +190,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']
@@ -205,13 +232,13 @@
ref_package_by=ref_package_by)
return rel_dict
-def package_relationship_update(data_dict, context):
+def package_relationship_update(context, data_dict):
model = context['model']
user = context['user']
- id = context["id"]
- id2 = context["id2"]
- rel = context["rel"]
+ id = data_dict["id"]
+ id2 = data_dict["id2"]
+ rel = data_dict["rel"]
api = context.get('api_version') or '1'
ref_package_by = 'id' if api == '2' else 'name'
@@ -236,12 +263,12 @@
comment = data_dict.get('comment', u'')
return _update_package_relationship(entity, comment, context)
-def group_update(data_dict, context):
+def group_update(context, data_dict):
model = context['model']
user = context['user']
schema = context.get('schema') or default_update_group_schema()
- id = context['id']
+ id = data_dict['id']
group = model.Group.get(id)
context["group"] = group
@@ -276,16 +303,28 @@
## Modifications for rest api
-def package_update_rest(data_dict, context):
+def package_update_rest(context, data_dict):
model = context['model']
- id = context["id"]
+ id = data_dict.get("id")
+ request_id = context['id']
api = context.get('api_version') or '1'
- pkg = model.Package.get(id)
+ pkg = model.Package.get(request_id)
+
+ if not pkg:
+ raise NotFound
+
+ if id and id != pkg.id:
+ pkg_from_data = model.Package.get(id)
+ if pkg_from_data != pkg:
+ error_dict = {id:('Cannot change value of key from %s to %s. '
+ 'This key is read-only') % (pkg.id, id)}
+ raise ValidationError(error_dict)
+
context["package"] = pkg
context["allow_partial_update"] = True
dictized_package = package_api_to_dict(data_dict, context)
- dictized_after = package_update(dictized_package, context)
+ dictized_after = package_update(context, dictized_package)
pkg = context['package']
@@ -296,16 +335,16 @@
return package_dict
-def group_update_rest(data_dict, context):
+def group_update_rest(context, data_dict):
model = context['model']
- id = context["id"]
+ id = data_dict["id"]
api = context.get('api_version') or '1'
group = model.Group.get(id)
context["group"] = group
context["allow_partial_update"] = True
dictized_package = group_api_to_dict(data_dict, context)
- dictized_after = group_update(dictized_package, context)
+ dictized_after = group_update(context, dictized_package)
group = context['group']
--- a/ckan/model/meta.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/model/meta.py Wed Jul 27 11:33:48 2011 +0100
@@ -1,6 +1,7 @@
import datetime
"""SQLAlchemy Metadata and Session object"""
from sqlalchemy import MetaData, __version__ as sqav
+from sqlalchemy.orm import class_mapper
from sqlalchemy.orm import scoped_session, sessionmaker
import sqlalchemy.orm as orm
from sqlalchemy.orm.session import SessionExtension
@@ -37,42 +38,42 @@
revision = session.revision
except AttributeError:
return
-
new = obj_cache['new']
changed = obj_cache['changed']
deleted = obj_cache['deleted']
-
for obj in new | changed | deleted:
-
if not hasattr(obj, '__revision_class__'):
continue
-
revision_cls = obj.__revision_class__
-
+ revision_table = class_mapper(revision_cls).mapped_table
## when a normal active transaction happens
if 'pending' not in obj.state:
- revision.approved_timestamp = datetime.datetime.now()
- old = session.query(revision_cls).filter_by(
- current='1',
- id = obj.id
- ).first()
- if old:
- old.current = '0'
- session.add(old)
+ ### this is asql statement as we do not want it in object cache
+ session.execute(
+ revision_table.update().where(
+ and_(revision_table.c.id == obj.id,
+ revision_table.c.current == '1')
+ ).values(current='0')
+ )
q = session.query(revision_cls)
q = q.filter_by(expired_timestamp=datetime.datetime(9999, 12, 31), id=obj.id)
results = q.all()
-
for rev_obj in results:
+ values = {}
if rev_obj.revision_id == revision.id:
- rev_obj.revision_timestamp = revision.timestamp
+ values['revision_timestamp'] = revision.timestamp
if 'pending' not in obj.state:
- rev_obj.current = '1'
+ values['current'] = '1'
else:
- rev_obj.expired_id = revision.id
- rev_obj.expired_timestamp = revision.timestamp
- session.add(rev_obj)
+ values['expired_id'] = revision.id
+ values['expired_timestamp'] = revision.timestamp
+ session.execute(
+ revision_table.update().where(
+ and_(revision_table.c.id == rev_obj.id,
+ revision_table.c.revision_id == rev_obj.revision_id)
+ ).values(**values)
+ )
def after_commit(self, session):
if hasattr(session, '_object_cache'):
--- a/ckan/plugins/interfaces.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/plugins/interfaces.py Wed Jul 27 11:33:48 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/tests/functional/api/model/test_package.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/tests/functional/api/model/test_package.py Wed Jul 27 11:33:48 2011 +0100
@@ -48,6 +48,7 @@
# Check the value of the Location header.
location = res.header('Location')
+
assert offset in location
res = self.app.get(location, status=self.STATUS_200_OK)
# Check the database record.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/functional/api/test_action.py Wed Jul 27 11:33:48 2011 +0100
@@ -0,0 +1,72 @@
+from ckan.lib.create_test_data import CreateTestData
+import ckan.model as model
+from ckan.tests import WsgiAppCase
+import json
+from pprint import pprint, pformat
+
+class TestAction(WsgiAppCase):
+
+ @classmethod
+ def setup_class(self):
+ CreateTestData.create()
+
+ @classmethod
+ def teardown_class(self):
+ model.repo.rebuild_db()
+
+ def test_01_package_list(self):
+ postparams = '%s=1' % json.dumps({})
+ res = self.app.post('/api/action/package_list', params=postparams)
+ assert json.loads(res.body) == {"help": "Lists the package by name",
+ "success": True,
+ "result": ["annakarenina", "warandpeace"]}
+
+ def test_02_create_update_package(self):
+
+ package = {
+ 'author': None,
+ 'author_email': None,
+ 'extras': [{'key': u'original media','value': u'"book"'}],
+ 'license_id': u'other-open',
+ 'maintainer': None,
+ 'maintainer_email': None,
+ 'name': u'annakareninanew',
+ 'notes': u'Some test now',
+ 'resources': [{'alt_url': u'alt123',
+ 'description': u'Full text.',
+ 'extras': {u'alt_url': u'alt123', u'size': u'123'},
+ 'format': u'plain text',
+ 'hash': u'abc123',
+ 'position': 0,
+ 'url': u'http://www.annakarenina.com/download/'},
+ {'alt_url': u'alt345',
+ 'description': u'Index of the novel',
+ 'extras': {u'alt_url': u'alt345', u'size': u'345'},
+ 'format': u'json',
+ 'hash': u'def456',
+ 'position': 1,
+ 'url': u'http://www.annakarenina.com/index.json'}],
+ 'tags': [{'name': u'russian'}, {'name': u'tolstoy'}],
+ 'title': u'A Novel By Tolstoy',
+ 'url': u'http://www.annakarenina.com',
+ 'version': u'0.7a'
+ }
+
+ wee = json.dumps(package)
+ postparams = '%s=1' % json.dumps(package)
+ res = self.app.post('/api/action/package_create', params=postparams,
+ extra_environ={'Authorization': 'tester'})
+ package_created = json.loads(res.body)['result']
+ print package_created
+ package_created['name'] = 'moo'
+ postparams = '%s=1' % json.dumps(package_created)
+ res = self.app.post('/api/action/package_update', params=postparams,
+ extra_environ={'Authorization': 'tester'})
+
+ package_updated = json.loads(res.body)['result']
+ package_updated.pop('revision_id')
+ package_updated.pop('revision_timestamp')
+ package_created.pop('revision_id')
+ package_created.pop('revision_timestamp')
+ assert package_updated == package_created#, (pformat(json.loads(res.body)), pformat(package_created['result']))
+
--- a/ckan/tests/lib/test_dictization.py Tue Jul 26 17:55:11 2011 +0100
+++ b/ckan/tests/lib/test_dictization.py Wed Jul 27 11:33:48 2011 +0100
@@ -516,9 +516,8 @@
anna1 = model.Session.query(model.Package).filter_by(name='annakarenina_changed2').one()
context = {"model": model,
"session": model.Session,
- 'user': 'testsysadmin',
- "id": anna1.id}
- make_latest_pending_package_active(context)
+ 'user': 'testsysadmin'}
+ make_latest_pending_package_active(context, {'id': anna1.id})
pkgrevisions = model.Session.query(model.PackageRevision).filter_by(id=anna1.id).all()
sorted_packages = sorted(pkgrevisions, key=lambda x:x.revision_timestamp)[::-1]
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