[ckan-changes] commit/ckan: 6 new changesets
Bitbucket
commits-noreply at bitbucket.org
Wed Jul 20 17:29:57 UTC 2011
6 new changesets in ckan:
http://bitbucket.org/okfn/ckan/changeset/89a15163579e/
changeset: 89a15163579e
user: dread
date: 2011-07-20 19:12:17
summary: [controllers][xs]: Removed redundant cache clearer. Ratings mothballed anyway.
affected #: 1 file (165 bytes)
--- a/ckan/controllers/package.py Wed Jul 20 15:30:42 2011 +0100
+++ b/ckan/controllers/package.py Wed Jul 20 18:12:17 2011 +0100
@@ -170,10 +170,6 @@
# 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,
@@ -729,7 +725,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:
http://bitbucket.org/okfn/ckan/changeset/744210672b17/
changeset: 744210672b17
user: dread
date: 2011-07-20 19:14:50
summary: [README][xs]: Added note on common test failure reason.
affected #: 1 file (421 bytes)
--- a/README.txt Wed Jul 20 18:12:17 2011 +0100
+++ b/README.txt Wed Jul 20 18:14:50 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
------------------
http://bitbucket.org/okfn/ckan/changeset/c1e1748a83a1/
changeset: c1e1748a83a1
branch: release-v1.4.2
user: dread
date: 2011-07-20 19:19:04
summary: [migrations][s]: Fix problem with migration and timezone. Caused test to fail for postgres tests on machines with a locale/timezone setup: ckan.tests.functional.test_home.TestHomeController.test_calculate_etag_hash.
affected #: 1 file (3 bytes)
--- a/ckan/migration/versions/039_add_expired_id_and_dates.py Wed Jul 20 14:47:51 2011 +0100
+++ b/ckan/migration/versions/039_add_expired_id_and_dates.py Wed Jul 20 18:19:04 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 = '''
http://bitbucket.org/okfn/ckan/changeset/9dd7f90de589/
changeset: 9dd7f90de589
branch: release-v1.4.2
user: kindly
date: 2011-07-19 20:45:57
summary: [tests] ticket 1230 make sure configure is run before all tests
affected #: 1 file (447 bytes)
--- a/ckan/ckan_nose_plugin.py Wed Jul 20 18:19:04 2011 +0100
+++ b/ckan/ckan_nose_plugin.py Tue Jul 19 19:45:57 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',
http://bitbucket.org/okfn/ckan/changeset/1ed3fde56a61/
changeset: 1ed3fde56a61
user: dread
date: 2011-07-20 19:14:56
summary: [controller]: #1234 locale tests and fix error conditions.
affected #: 2 files (624 bytes)
--- a/ckan/controllers/home.py Wed Jul 20 18:14:50 2011 +0100
+++ b/ckan/controllers/home.py Wed Jul 20 18:14:56 2011 +0100
@@ -73,7 +73,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/tests/functional/test_home.py Wed Jul 20 18:14:50 2011 +0100
+++ b/ckan/tests/functional/test_home.py Wed Jul 20 18:14:56 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()
@@ -106,6 +107,20 @@
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
+
+ @search_related
+ 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
+
+ @search_related
def test_locale_change_with_false_hash(self):
offset = url_for('home')
res = self.app.get(offset)
http://bitbucket.org/okfn/ckan/changeset/0789e037fda6/
changeset: 0789e037fda6
user: dread
date: 2011-07-20 19:26:36
summary: [merge] from release-v1.4.2.
affected #: 20 files (14.9 KB)
--- a/CHANGELOG.txt Wed Jul 20 18:14:56 2011 +0100
+++ b/CHANGELOG.txt Wed Jul 20 18:26:36 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/ckan/controllers/home.py Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/controllers/home.py Wed Jul 20 18:26:36 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
--- a/ckan/controllers/user.py Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/controllers/user.py Wed Jul 20 18:26:36 2011 +0100
@@ -189,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.'))
@@ -205,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/mailer.py Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/lib/mailer.py Wed Jul 20 18:26:36 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/migration/versions/039_add_expired_id_and_dates.py Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/migration/versions/039_add_expired_id_and_dates.py Wed Jul 20 18:26:36 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/public/css/ckan.css Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/public/css/ckan.css Wed Jul 20 18:26:36 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 {
Binary file ckan/public/images/icons/page_stack.png has changed
--- a/ckan/templates/_util.html Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/templates/_util.html Wed Jul 20 18:26:36 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/layout.html Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/templates/package/layout.html Wed Jul 20 18:26:36 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/search.html Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/templates/package/search.html Wed Jul 20 18:26:36 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 Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/templates/user/request_reset.html Wed Jul 20 18:26:36 2011 +0100
@@ -14,7 +14,7 @@
Request a password reset
</h2>
- <form id="user-edit" action="" method="post" class="simple-form"
+ <form id="user-password-reset" action="" method="post" class="simple-form"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
>
@@ -25,7 +25,7 @@
</fieldset><div>
- ${h.submit('save', _('Reset password'))}
+ ${h.submit('reset', _('Reset password'))}
</div></form></div>
--- a/ckan/tests/functional/test_home.py Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/tests/functional/test_home.py Wed Jul 20 18:26:36 2011 +0100
@@ -20,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)
@@ -49,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)
@@ -78,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)
@@ -95,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)
@@ -106,21 +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
- @search_related
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
- @search_related
def test_locale_change_with_false_hash(self):
offset = url_for('home')
res = self.app.get(offset)
--- a/ckan/tests/functional/test_user.py Wed Jul 20 18:14:56 2011 +0100
+++ b/ckan/tests/functional/test_user.py Wed Jul 20 18:26:36 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):
@@ -450,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 20 18:26:36 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 20 18:26:36 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 20 18:26:36 2011 +0100
@@ -0,0 +1,82 @@
+import threading
+import asyncore
+import socket
+from smtpd import SMTPServer
+
+from pylons import config
+
+from ckan.lib.mailer import _mail_recipient
+
+class MockSmtpServer(SMTPServer):
+ '''A mock SMTP server that operates in an asyncore loop'''
+ def __init__(self, host, port):
+ self.msgs = []
+ SMTPServer.__init__(self, (host, port), None)
+
+ def process_message(self, peer, mailfrom, rcpttos, data):
+ self.msgs.append((peer, mailfrom, rcpttos, data))
+
+ def get_smtp_messages(self):
+ return self.msgs
+
+ def clear_smtp_messages(self):
+ self.msgs = []
+
+class MockSmtpServerThread(threading.Thread):
+ '''Runs the mock SMTP server in a thread'''
+ def __init__(self, host, port):
+ self.assert_port_free(host, port)
+ # init thread
+ self._stop_event = threading.Event()
+ self.thread_name = self.__class__
+ threading.Thread.__init__(self, name=self.thread_name)
+ # init smtp server
+ self.server = MockSmtpServer(host, port)
+
+ def assert_port_free(self, host, port):
+ test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
+ test_socket.getsockopt(socket.SOL_SOCKET,
+ socket.SO_REUSEADDR) | 1 )
+ test_socket.bind((host, port))
+ test_socket.close()
+
+ def run(self):
+ while not self._stop_event.isSet():
+ asyncore.loop(timeout=0.01, count=1)
+
+ def stop(self, timeout=None):
+ self._stop_event.set()
+ threading.Thread.join(self, timeout)
+ self.server.close()
+
+ def get_smtp_messages(self):
+ return self.server.get_smtp_messages()
+
+ def clear_smtp_messages(self):
+ return self.server.clear_smtp_messages()
+
+class SmtpServerHarness(object):
+ '''Derive from this class to run MockSMTP - a test harness that
+ records what email messages are requested to be sent by it.'''
+
+ @classmethod
+ def setup_class(cls):
+ smtp_server = config.get('test_smtp_server') or config['smtp_server']
+ if ':' in smtp_server:
+ host, port = smtp_server.split(':')
+ else:
+ host, port = smtp_server, 25
+ cls.smtp_thread = MockSmtpServerThread(host, int(port))
+ cls.smtp_thread.start()
+
+ @classmethod
+ def teardown_class(cls):
+ cls.smtp_thread.stop()
+
+ def get_smtp_messages(self):
+ return self.smtp_thread.get_smtp_messages()
+
+ def clear_smtp_messages(self):
+ return self.smtp_thread.clear_smtp_messages()
+
--- a/ckan/tests/test_mailer.py Wed Jul 20 18:14:56 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/test-core.ini Wed Jul 20 18:14:56 2011 +0100
+++ b/test-core.ini Wed Jul 20 18:26:36 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
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