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

Bitbucket commits-noreply at bitbucket.org
Thu Jul 21 13:26:22 UTC 2011


2 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/fc8a649e2673/
changeset:   fc8a649e2673
branch:      feature-1141-moderated-edits-ajax
user:        John Glover
date:        2011-07-21 11:02:43
summary:     [merge] default
affected #:  26 files (16.8 KB)

--- a/CHANGELOG.txt	Mon Jul 18 15:44:46 2011 +0100
+++ b/CHANGELOG.txt	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/README.txt	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/ckan_nose_plugin.py	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/controllers/home.py	Thu Jul 21 10:02:43 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
@@ -73,7 +74,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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/controllers/package.py	Thu Jul 21 10:02:43 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,
@@ -730,7 +726,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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/controllers/user.py	Thu Jul 21 10:02:43 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/mailer.py	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/lib/mailer.py	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/migration/versions/039_add_expired_id_and_dates.py	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/public/css/ckan.css	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/templates/_util.html	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/templates/package/layout.html	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/templates/package/search.html	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/templates/user/request_reset.html	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/tests/functional/test_home.py	Thu Jul 21 10:02:43 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_user.py	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/tests/functional/test_user.py	Thu Jul 21 10:02:43 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	Thu Jul 21 10:02:43 2011 +0100
@@ -0,0 +1,112 @@
+import time
+from nose.tools import assert_equal, assert_raises
+from pylons import config
+from email.mime.text import MIMEText
+
+from ckan import model
+from ckan.tests.pylons_controller import PylonsTestCase
+from ckan.tests.mock_mail_server import SmtpServerHarness
+from ckan.lib.mailer import mail_recipient, mail_user, send_reset_link, add_msg_niceties, MailerException, get_reset_link_body, get_reset_link
+from ckan.lib.create_test_data import CreateTestData
+from ckan.lib.base import g
+
+class TestMailer(SmtpServerHarness, PylonsTestCase):
+    @classmethod
+    def setup_class(cls):
+        CreateTestData.create_user(name='bob', email='bob at bob.net')
+        CreateTestData.create_user(name='mary') #NB No email addr provided
+        SmtpServerHarness.setup_class()
+        PylonsTestCase.setup_class()
+
+    @classmethod
+    def teardown_class(cls):
+        SmtpServerHarness.teardown_class()
+        model.repo.rebuild_db()
+
+    def setup(self):
+        self.clear_smtp_messages()
+
+    def mime_encode(self, msg, recipient_name):
+        sender_name = g.site_title
+        sender_url = g.site_url
+        body = add_msg_niceties(recipient_name, msg, sender_name, sender_url)
+        encoded_body = MIMEText(body.encode('utf-8'), 'plain', 'utf-8').get_payload().strip()
+        return encoded_body
+
+    def test_mail_recipient(self):
+        msgs = self.get_smtp_messages()
+        assert_equal(msgs, [])
+
+        # send email
+        test_email = {'recipient_name': 'Bob',
+                      'recipient_email':'bob at bob.net',
+                      'subject': 'Meeting', 
+                      'body': 'The meeting is cancelled.',
+                      'headers': {'header1': 'value1'}}
+        mail_recipient(**test_email)
+        time.sleep(0.1)
+
+        # check it went to the mock smtp server
+        msgs = self.get_smtp_messages()
+        assert_equal(len(msgs), 1)
+        msg = msgs[0]
+        assert_equal(msg[1], config['ckan.mail_from'])
+        assert_equal(msg[2], [test_email['recipient_email']])
+        assert test_email['headers'].keys()[0] in msg[3], msg[3]
+        assert test_email['headers'].values()[0] in msg[3], msg[3]
+        assert test_email['subject'] in msg[3], msg[3]
+        expected_body = self.mime_encode(test_email['body'],
+                                         test_email['recipient_name'])
+        assert expected_body in msg[3], '%r not in %r' % (expected_body, msg[3])
+
+    def test_mail_user(self):
+        msgs = self.get_smtp_messages()
+        assert_equal(msgs, [])
+
+        # send email
+        test_email = {'recipient': model.User.by_name(u'bob'),
+                      'subject': 'Meeting', 
+                      'body': 'The meeting is cancelled.',
+                      'headers': {'header1': 'value1'}}
+        mail_user(**test_email)
+        time.sleep(0.1)
+
+        # check it went to the mock smtp server
+        msgs = self.get_smtp_messages()
+        assert_equal(len(msgs), 1)
+        msg = msgs[0]
+        assert_equal(msg[1], config['ckan.mail_from'])
+        assert_equal(msg[2], [model.User.by_name(u'bob').email])
+        assert test_email['headers'].keys()[0] in msg[3], msg[3]
+        assert test_email['headers'].values()[0] in msg[3], msg[3]
+        assert test_email['subject'] in msg[3], msg[3]
+        expected_body = self.mime_encode(test_email['body'],
+                                         'bob')
+        assert expected_body in msg[3], '%r not in %r' % (expected_body, msg[3])
+
+    def test_mail_user_without_email(self):
+        # send email
+        test_email = {'recipient': model.User.by_name(u'mary'),
+                      'subject': 'Meeting', 
+                      'body': 'The meeting is cancelled.',
+                      'headers': {'header1': 'value1'}}
+        assert_raises(MailerException, mail_user, **test_email)
+
+    def test_send_reset_email(self):
+        # send email
+        send_reset_link(model.User.by_name(u'bob'))
+        time.sleep(0.1)
+
+        # check it went to the mock smtp server
+        msgs = self.get_smtp_messages()
+        assert_equal(len(msgs), 1)
+        msg = msgs[0]
+        assert_equal(msg[1], config['ckan.mail_from'])
+        assert_equal(msg[2], [model.User.by_name(u'bob').email])
+        assert 'Reset' in msg[3], msg[3]
+        test_msg = get_reset_link_body(model.User.by_name(u'bob'))
+        expected_body = self.mime_encode(test_msg,
+                                         u'bob') 
+        assert expected_body in msg[3], '%r not in %r' % (expected_body, msg[3])
+        
+        # reset link tested in user functional test


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/misc/test_mock_mail_server.py	Thu Jul 21 10:02:43 2011 +0100
@@ -0,0 +1,33 @@
+import time
+from nose.tools import assert_equal
+from pylons import config
+from email.mime.text import MIMEText
+
+from ckan.tests.pylons_controller import PylonsTestCase
+from ckan.tests.mock_mail_server import SmtpServerHarness
+from ckan.lib.mailer import mail_recipient
+
+class TestMockMailServer(SmtpServerHarness, PylonsTestCase):
+    @classmethod
+    def setup_class(cls):
+        SmtpServerHarness.setup_class()
+        PylonsTestCase.setup_class()
+
+    @classmethod
+    def teardown_class(cls):
+        SmtpServerHarness.teardown_class()
+
+    def test_basic(self):
+        msgs = self.get_smtp_messages()
+        assert_equal(msgs, [])
+
+        test_email = {'recipient_name': 'Bob',
+                      'recipient_email':'bob at bob.net',
+                      'subject': 'Meeting', 
+                      'body': 'The meeting is cancelled.',
+                      'headers': {'header1': 'value1'}}
+        mail_recipient(**test_email)
+        time.sleep(0.1)
+
+        msgs = self.get_smtp_messages()
+        assert_equal(len(msgs), 1)


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/mock_mail_server.py	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/ckan/tests/wsgi_ckanclient.py	Thu Jul 21 10:02:43 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	Mon Jul 18 15:44:46 2011 +0100
+++ b/fabfile.py	Thu Jul 21 10:02:43 2011 +0100
@@ -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	Mon Jul 18 15:44:46 2011 +0100
+++ b/requires/lucid_present.txt	Thu Jul 21 10:02:43 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/test-core.ini	Mon Jul 18 15:44:46 2011 +0100
+++ b/test-core.ini	Thu Jul 21 10:02:43 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/fb04156bb32a/
changeset:   fb04156bb32a
branch:      feature-1141-moderated-edits-ajax
user:        John Glover
date:        2011-07-21 15:22:36
summary:     [moderatededits] Bug fix: when listing tags make sure to select the current revision
affected #:  1 file (156 bytes)

--- a/ckan/model/tag.py	Thu Jul 21 10:02:43 2011 +0100
+++ b/ckan/model/tag.py	Thu Jul 21 14:22:36 2011 +0100
@@ -1,4 +1,5 @@
 from sqlalchemy.orm import eagerload_all
+from sqlalchemy import and_
 import vdm.sqlalchemy
 
 from types import make_uuid
@@ -61,7 +62,9 @@
     def all(cls):
         q = Session.query(cls)
         q = q.distinct().join(PackageTagRevision)
-        q = q.filter(PackageTagRevision.state == 'active')
+        q = q.filter(and_(
+            PackageTagRevision.state == 'active', PackageTagRevision.current == True
+        ))
         return q
 
     @property
@@ -69,7 +72,9 @@
         q = Session.query(Package)
         q = q.join(PackageTagRevision)
         q = q.filter(PackageTagRevision.tag_id == self.id)
-        q = q.filter(PackageTagRevision.state == 'active')
+        q = q.filter(and_(
+            PackageTagRevision.state == 'active', PackageTagRevision.current == True
+        ))
         packages = [p for p in q]
         ourcmp = lambda pkg1, pkg2: cmp(pkg1.name, pkg2.name)
         return sorted(packages, cmp=ourcmp)

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