From 2e92037461b6ea4639886f1395aedceb2569d783 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Thu, 16 Jan 2014 12:24:24 -0500 Subject: [PATCH 1/7] First stab at comma-delimited detail_value. --- fmn/lib/models.py | 2 +- fmn/lib/tests/test_recipients.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fmn/lib/models.py b/fmn/lib/models.py index 157f534..e045dc7 100644 --- a/fmn/lib/models.py +++ b/fmn/lib/models.py @@ -148,7 +148,7 @@ def _recipients(self, session, config, valid_paths, message): if filter: yield { 'user': user.openid, - pref.context.detail_name: pref.detail_value, + pref.context.detail_name: pref.detail_value.split(','), 'filter': filter.name, } diff --git a/fmn/lib/tests/test_recipients.py b/fmn/lib/tests/test_recipients.py index f650980..960bc20 100644 --- a/fmn/lib/tests/test_recipients.py +++ b/fmn/lib/tests/test_recipients.py @@ -32,7 +32,7 @@ def create_preference_data_empty(self): self.sess, user=user, context=context, - detail_value="threebean", + detail_value="threebean,threebean2", ) def create_preference_data_basic(self, code_path): @@ -79,10 +79,10 @@ def test_basic_recipients_list(self): recipients = fmn.lib.recipients_for_context( self.sess, self.config, self.valid_paths, 'irc', msg) eq_(list(recipients), [{ - 'irc nick': 'threebean', + 'irc nick': ['threebean', 'threebean2'], 'user': 'ralph.id.fedoraproject.org', 'filter': 'test filter', - }]) + }]) def test_miss_recipients_list(self): self.create_user_and_context_data() @@ -139,7 +139,7 @@ def test_multiple_identical_filters_hit(self): recipients = fmn.lib.recipients_for_context( self.sess, self.config, self.valid_paths, 'irc', msg) eq_(list(recipients), [{ - 'irc nick': 'threebean', + 'irc nick': ['threebean', 'threebean2'], 'user': 'ralph.id.fedoraproject.org', 'filter': 'test filter', }]) @@ -164,7 +164,7 @@ def test_multiple_different_filters_hit(self): recipients = fmn.lib.recipients_for_context( self.sess, self.config, self.valid_paths, 'irc', msg) eq_(list(recipients), [{ - 'irc nick': 'threebean', + 'irc nick': ['threebean', 'threebean2'], 'user': 'ralph.id.fedoraproject.org', 'filter': 'test filter', }]) From 183def98e84d9d8152c48328d693a55ef382e9d4 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Thu, 16 Jan 2014 13:07:15 -0500 Subject: [PATCH 2/7] Start of some tests for confirmations. --- fmn/lib/__init__.py | 2 +- fmn/lib/models.py | 9 ++++- fmn/lib/tests/test_confirmations.py | 59 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 fmn/lib/tests/test_confirmations.py diff --git a/fmn/lib/__init__.py b/fmn/lib/__init__.py index 6134972..27195b0 100644 --- a/fmn/lib/__init__.py +++ b/fmn/lib/__init__.py @@ -58,7 +58,7 @@ def load_rules(root='fmn.rules'): obj = getattr(module, name) if not callable(obj): continue - log.info("Found rule %r %r" % (name, obj)) + log.debug("Found rule %r %r" % (name, obj)) doc = inspect.getdoc(obj) diff --git a/fmn/lib/models.py b/fmn/lib/models.py index e045dc7..418a21f 100644 --- a/fmn/lib/models.py +++ b/fmn/lib/models.py @@ -465,7 +465,12 @@ def load(cls, session, user, context): .first() def update_details(self, session, value): - self.detail_value = value + if self.detail_value: + tokens = self.detail_value.split(',') + if value not in tokens: + self.detail_value = ','.join(tokens + [value]) + else: + self.detail_value = value session.flush() session.commit() @@ -632,7 +637,7 @@ def set_status(self, session, status): # Propagate back to the Preference if everything is good. if self.status == 'accepted': pref = Preference.load(session, self.openid, self.context_name) - pref.detail_value = self.detail_value + pref.update_details(session, self.detail_value) session.flush() session.commit() diff --git a/fmn/lib/tests/test_confirmations.py b/fmn/lib/tests/test_confirmations.py new file mode 100644 index 0000000..51b993f --- /dev/null +++ b/fmn/lib/tests/test_confirmations.py @@ -0,0 +1,59 @@ +from nose.tools import eq_, assert_not_equals + +import os +import fmn.lib.models +import fmn.lib.tests + + +class TestConfirmations(fmn.lib.tests.Base): + def create_user_and_context_data(self): + user1 = fmn.lib.models.User.get_or_create( + self.sess, openid="ralph.id.fedoraproject.org", + openid_url="http://ralph.id.fedoraproject.org/", + ) + context1 = fmn.lib.models.Context.create( + self.sess, name="irc", description="Internet Relay Chat", + detail_name="irc nick", icon="user", + ) + + def create_preference_data_empty(self): + user = fmn.lib.models.User.get( + self.sess, openid="ralph.id.fedoraproject.org") + context = fmn.lib.models.Context.get(self.sess, name="irc") + preference = fmn.lib.models.Preference.create( + self.sess, + user=user, + context=context, + detail_value=None, + ) + + def test_updating_details(self): + self.create_user_and_context_data() + self.create_preference_data_empty() + user = fmn.lib.models.User.get( + self.sess, openid="ralph.id.fedoraproject.org") + context = fmn.lib.models.Context.get(self.sess, name="irc") + preference = fmn.lib.models.Preference.load(self.sess, user, context) + preference.update_details(self.sess, 'wat') + eq_(preference.detail_value, 'wat') + preference.update_details(self.sess, 'wat2') + eq_(preference.detail_value, 'wat,wat2') + preference.update_details(self.sess, 'wat') + eq_(preference.detail_value, 'wat,wat2') + + def test_confirmation(self): + self.create_user_and_context_data() + self.create_preference_data_empty() + user = fmn.lib.models.User.get( + self.sess, openid="ralph.id.fedoraproject.org") + context = fmn.lib.models.Context.get(self.sess, name="irc") + preference = fmn.lib.models.Preference.load(self.sess, user, context) + confirmation = fmn.lib.models.Confirmation.create( + self.sess, + user, + context, + detail_value='awesome', + ) + eq_(preference.detail_value, None) + confirmation.set_status(self.sess, 'accepted') + eq_(preference.detail_value, 'awesome') From 9af3ddf24562751967235d073497ffc75a148857 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 17 Jan 2014 14:44:15 -0500 Subject: [PATCH 3/7] Validation facilities for detail_values. --- fmn/lib/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/fmn/lib/__init__.py b/fmn/lib/__init__.py index 27195b0..d319bd3 100644 --- a/fmn/lib/__init__.py +++ b/fmn/lib/__init__.py @@ -4,6 +4,7 @@ import inspect import logging +import re import bs4 import docutils.examples @@ -16,6 +17,10 @@ log = logging.getLogger(__name__) +irc_regex = r'^[\w-]+$' +email_regex = r'^([a-zA-Z0-9_\-\.]+)@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$' +gcm_regex = r'^[\w-]+$' + def recipients(session, config, valid_paths, message): """ The main API function. @@ -102,3 +107,19 @@ def strip_anchor_tags(soup): yield tag.string else: yield tag + + +def validate_detail_value(ctx, value): + if ctx.name == 'irc': + if re.match(irc_regex, value) is None: + raise ValueError("irc nick must be alphanumeric") + elif ctx.name == 'email': + if re.match(email_regex, value) is None: + raise ValueError("value must be an email address") + elif ctx.name == 'gcm': + if re.match(gcm_regex, value) is None: + raise ValueError("not a valid android registration id") + else: + raise NotImplementedError("No validation scheme for %r" % ctx.name) + # Happy. + return From 7ff335e671e02ef8f40cebaf90dc3a549e69614a Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 17 Jan 2014 21:39:35 -0500 Subject: [PATCH 4/7] Added a comment. --- fmn/lib/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fmn/lib/__init__.py b/fmn/lib/__init__.py index d319bd3..e20ebe3 100644 --- a/fmn/lib/__init__.py +++ b/fmn/lib/__init__.py @@ -72,6 +72,10 @@ def load_rules(root='fmn.rules'): doc = doc.decode('utf-8') if doc: + # If we have a docstring, then mark it up beautifully for display + # in the web app. + # FWIW, this should probably be moved into fmn.web since nowhere + # else are we going to want HTML... we'll still want raw .rst. title, doc_as_rst = doc.split('\n', 1) doc = docutils.examples.html_parts(doc_as_rst)['body'] soup = bs4.BeautifulSoup(doc) From 8bb445a1b112c50252fe3619e87dc9ed20e4eb73 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Mon, 20 Jan 2014 09:53:11 -0500 Subject: [PATCH 5/7] Update irc nick validation regex. --- fmn/lib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fmn/lib/__init__.py b/fmn/lib/__init__.py index e20ebe3..cff4377 100644 --- a/fmn/lib/__init__.py +++ b/fmn/lib/__init__.py @@ -17,7 +17,7 @@ log = logging.getLogger(__name__) -irc_regex = r'^[\w-]+$' +irc_regex = r'[a-zA-Z_\-\[\]\\^{}|`][a-zA-Z0-9_\-\[\]\\^{}|`]*' email_regex = r'^([a-zA-Z0-9_\-\.]+)@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$' gcm_regex = r'^[\w-]+$' @@ -116,7 +116,7 @@ def strip_anchor_tags(soup): def validate_detail_value(ctx, value): if ctx.name == 'irc': if re.match(irc_regex, value) is None: - raise ValueError("irc nick must be alphanumeric") + raise ValueError("value must be a valid irc nick") elif ctx.name == 'email': if re.match(email_regex, value) is None: raise ValueError("value must be an email address") From 64c757bc6e604bcb4e97fbc5109f6bda6141a9d5 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Mon, 20 Jan 2014 09:55:34 -0500 Subject: [PATCH 6/7] .strip() value before adding to the detail_value list. --- fmn/lib/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fmn/lib/models.py b/fmn/lib/models.py index 418a21f..e9c5a6d 100644 --- a/fmn/lib/models.py +++ b/fmn/lib/models.py @@ -465,6 +465,7 @@ def load(cls, session, user, context): .first() def update_details(self, session, value): + value = value.strip() if self.detail_value: tokens = self.detail_value.split(',') if value not in tokens: From 940a098c5ea8ecf0ae33ffc773ceb0918c32e71d Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Mon, 20 Jan 2014 11:48:20 -0500 Subject: [PATCH 7/7] Protect against null detail_value. --- fmn/lib/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fmn/lib/models.py b/fmn/lib/models.py index e9c5a6d..5ff79cf 100644 --- a/fmn/lib/models.py +++ b/fmn/lib/models.py @@ -143,7 +143,7 @@ def _recipients(self, session, config, valid_paths, message): """ Returns the list of recipients for a message. """ for user in User.all(session): pref = Preference.load(session, user, self) - if pref: + if pref and pref.detail_value: filter = pref.prefers(session, config, valid_paths, message) if filter: yield {