From 35f805a57bed8e8e341a898c213f2cb5bad8d2d1 Mon Sep 17 00:00:00 2001
From: Denny Tek
Date: Wed, 16 Mar 2016 16:22:51 -0400
Subject: [PATCH 01/22] Add custom 404 page.
---
app/error_handlers.py | 8 ++++++++
app/static/sass/_error-page.scss | 20 ++++++++++++++++++++
app/static/sass/styles.scss | 2 ++
app/templates/404.html | 15 +++++++++++++++
4 files changed, 45 insertions(+)
create mode 100644 app/error_handlers.py
create mode 100644 app/static/sass/_error-page.scss
create mode 100644 app/templates/404.html
diff --git a/app/error_handlers.py b/app/error_handlers.py
new file mode 100644
index 0000000..65b1515
--- /dev/null
+++ b/app/error_handlers.py
@@ -0,0 +1,8 @@
+import flask
+from flask import Blueprint, render_template
+
+blueprint = flask.Blueprint('error_handlers', __name__)
+
+@blueprint.app_errorhandler(404)
+def page_not_found(e):
+ return render_template('404.html'), 404
diff --git a/app/static/sass/_error-page.scss b/app/static/sass/_error-page.scss
new file mode 100644
index 0000000..3382fcb
--- /dev/null
+++ b/app/static/sass/_error-page.scss
@@ -0,0 +1,20 @@
+.e-error-display {
+ background-color: $blue;
+ text-align: center;
+ text-transform: uppercase;
+ padding-top: 4em;
+ h2,h3,h4 {
+ color: $white-80;;
+ padding: .5em;
+ }
+}
+
+.error-page-icon {
+ color: $white-80;;
+ font-size: 4em;
+ }
+
+.e-error-display a {
+ color: $white-90;
+ font-weight: bold;
+}
diff --git a/app/static/sass/styles.scss b/app/static/sass/styles.scss
index a37f45d..bef6bbb 100644
--- a/app/static/sass/styles.scss
+++ b/app/static/sass/styles.scss
@@ -96,6 +96,8 @@ body {
@import 'desktop-hack';
+@import 'error-page';
+
/* Useful for making blocks of content that we don't have time to style
* not look utterly unstyled and horrible. */
.b-temporary-styling {
diff --git a/app/templates/404.html b/app/templates/404.html
new file mode 100644
index 0000000..79c7720
--- /dev/null
+++ b/app/templates/404.html
@@ -0,0 +1,15 @@
+{% extends '__base_ui__.html' %}
+
+{% block title %}Page Not Found | Network of Innovators{% endblock %}
+{% block body_class %}b-landing-page{% endblock %}
+
+{% block content %}
+
+
+
error_outline
+
Error 404
+
We couldn't find what you were looking for
+
Try using the navigation above or click here to go back to the homepage
+
+
+{% endblock %}
From 6e04e838fc45fe64b35ace0ca09e70135a10a389 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 16 Mar 2016 16:23:10 -0400
Subject: [PATCH 02/22] Register error_handlers blueprint for custom 404 page.
---
app/factory.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/factory.py b/app/factory.py
index 93896ed..98c9321 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -12,7 +12,7 @@
from app import (csrf, cache, mail, bcrypt, s3, assets, security, admin,
babel, alchemydumps, sass, email_errors, csp, oauth,
- linkedin, discourse,
+ linkedin, discourse, error_handlers,
QUESTIONNAIRES, NOI_COLORS, LEVELS, ORG_TYPES,
QUESTIONS_BY_ID, LEVELS_BY_SCORE, QUESTIONNAIRES_BY_ID)
from app.forms import (NOIForgotPasswordForm, NOILoginForm,
@@ -89,6 +89,7 @@ def create_app(config=None): #pylint: disable=too-many-statements
l10n.configure_app(app)
+ app.register_blueprint(error_handlers.blueprint)
app.register_blueprint(views)
if app.config['DEBUG']:
app.register_blueprint(style_guide.views)
From c34f2eb0e7c02cac24a745c28200690dfc33dda7 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Thu, 17 Mar 2016 11:24:55 -0400
Subject: [PATCH 03/22] Add slack integration.
---
app/config/local_config.sample.yml | 10 +++++++
app/factory.py | 4 ++-
app/slack.py | 42 ++++++++++++++++++++++++++++++
manage.py | 3 ++-
4 files changed, 57 insertions(+), 2 deletions(-)
create mode 100644 app/slack.py
diff --git a/app/config/local_config.sample.yml b/app/config/local_config.sample.yml
index 85973b8..5ae1792 100755
--- a/app/config/local_config.sample.yml
+++ b/app/config/local_config.sample.yml
@@ -127,3 +127,13 @@ SECRET_KEY: i_am_insecure
# api_key: PUT_YOUR_DISCOURSE_API_KEY_HERE
# origin: http://discourse-dev.networkofinnovators.org
# sso_secret: PUT_YOUR_SSO_SECRET_HERE
+
+# Slack Integration
+# -----------------
+
+# Events that occur in NoI can automatically be posted to Slack via a
+# Slack Incoming Webhook. For more information, see:
+#
+# https://api.slack.com/incoming-webhooks
+#
+# SLACK_WEBHOOK_URL: https://hooks.slack.com/services/T00000000/B00000000/XXXX
diff --git a/app/factory.py b/app/factory.py
index 93896ed..c073050 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -12,7 +12,7 @@
from app import (csrf, cache, mail, bcrypt, s3, assets, security, admin,
babel, alchemydumps, sass, email_errors, csp, oauth,
- linkedin, discourse,
+ linkedin, discourse, slack,
QUESTIONNAIRES, NOI_COLORS, LEVELS, ORG_TYPES,
QUESTIONS_BY_ID, LEVELS_BY_SCORE, QUESTIONNAIRES_BY_ID)
from app.forms import (NOIForgotPasswordForm, NOILoginForm,
@@ -109,6 +109,8 @@ def create_app(config=None): #pylint: disable=too-many-statements
app.register_blueprint(linkedin.views)
if 'DISCOURSE' in app.config:
discourse.init_app(app)
+ if 'SLACK_WEBHOOK_URL' in app.config:
+ slack.init_app(app)
cache.init_app(app)
csrf.init_app(app)
diff --git a/app/slack.py b/app/slack.py
new file mode 100644
index 0000000..df20bab
--- /dev/null
+++ b/app/slack.py
@@ -0,0 +1,42 @@
+import requests
+from flask import current_app, url_for
+from flask_script import Manager
+from flask_security.signals import user_registered
+
+from signals import user_changed_profile
+
+def init_app(app):
+ assert 'SLACK_WEBHOOK_URL' in app.config
+
+ @user_registered.connect_via(app)
+ def when_user_registered(sender, user, confirm_token, **extra):
+ post_user_message(user, 'just registered.')
+
+ @user_changed_profile.connect_via(app)
+ def when_user_changed_profile(sender, user, avatar_changed=False, **extra):
+ text = 'changed their profile.'
+ if avatar_changed:
+ text = 'changed their avatar.'
+ post_user_message(user, text)
+
+def post_user_message(user, text):
+ url = url_for('views.get_user', userid=user.id, _external=True)
+ post_message(u'<%s|%s> %s' % (url, user.full_name, text))
+
+def post_message(text):
+ res = requests.post(
+ current_app.config['SLACK_WEBHOOK_URL'],
+ json={'text': text}
+ )
+ if res.status_code != 200:
+ return res.raise_for_status()
+
+SlackCommand = manager = Manager(usage='Manage Slack integration.')
+
+@manager.command
+def post(text='Hello! This is just a message to test slack integration.'):
+ '''
+ Post a message with the given optional text.
+ '''
+
+ post_message(text)
diff --git a/manage.py b/manage.py
index 5aca757..1277a2b 100644
--- a/manage.py
+++ b/manage.py
@@ -14,7 +14,7 @@
] + sys.argv)
from app import (mail, models, sass, email_errors, LEVELS, ORG_TYPES, stats,
- questionnaires, blog_posts, linkedin)
+ questionnaires, blog_posts, linkedin, slack)
from app.factory import create_app
from app.models import db, User
from app.utils import csv_reader
@@ -50,6 +50,7 @@
manager.add_command('discourse', DiscourseCommand)
manager.add_command('noi1', Noi1Command)
manager.add_command('db', MigrateCommand)
+manager.add_command('slack', slack.SlackCommand)
#manager.add_command("assets", ManageAssets)
alchemydumps = AlchemyDumps(app, db)
From 0bad7900134f4e8a22ecf417ca29312079933669 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Thu, 17 Mar 2016 11:53:35 -0400
Subject: [PATCH 04/22] Add unit tests for slack integration.
---
app/tests/test_slack.py | 57 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
create mode 100644 app/tests/test_slack.py
diff --git a/app/tests/test_slack.py b/app/tests/test_slack.py
new file mode 100644
index 0000000..944e082
--- /dev/null
+++ b/app/tests/test_slack.py
@@ -0,0 +1,57 @@
+from flask import Flask
+import mock
+import flask_testing
+from flask_security.signals import user_registered
+
+from ..signals import user_changed_profile
+from .test_views import ViewTestCase
+from .. import slack
+
+class SlackTestCase(flask_testing.TestCase):
+ def create_app(self):
+ app = Flask('slack_test')
+ app.config['SLACK_WEBHOOK_URL'] = 'http://slack/123'
+ slack.init_app(app)
+ return app
+
+@mock.patch('app.slack.post_user_message')
+class SignalTests(SlackTestCase):
+ def test_user_changed_avatar(self, post_user_message):
+ user_changed_profile.send(self.app, user='user', avatar_changed=True)
+ post_user_message.assert_called_with('user', 'changed their avatar.')
+
+ def test_user_changed_profile(self, post_user_message):
+ user_changed_profile.send(self.app, user='user', avatar_changed=False)
+ post_user_message.assert_called_with('user', 'changed their profile.')
+
+ def test_user_registered(self, post_user_message):
+ user_registered.send(self.app, user='user', confirm_token='blah')
+ post_user_message.assert_called_with('user', 'just registered.')
+
+@mock.patch('requests.post')
+class PostMessageTests(SlackTestCase):
+ def test_it_works(self, post):
+ post.return_value.status_code = 200
+
+ slack.post_message('hi')
+
+ post.assert_called_once_with('http://slack/123', json={'text': 'hi'})
+ post.return_value.raise_for_status.assert_not_called()
+
+ def test_it_raises_exception_when_not_ok(self, post):
+ post.return_value.status_code = 500
+
+ slack.post_message('hi')
+
+ post.assert_called_once_with('http://slack/123', json={'text': 'hi'})
+ post.return_value.raise_for_status.assert_called_once_with()
+
+@mock.patch('app.slack.post_message')
+class PostUserMessageTests(ViewTestCase):
+ def test_it_works(self, post_message):
+ self.login(first_name='Boop', last_name='Jones')
+ user = self.last_created_user
+ slack.post_user_message(user, 'is cool')
+ post_message.assert_called_once_with(
+ u' is cool' % user.id
+ )
From b25046cc4e40b1694bbc4a923003c54dcbe79207 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 07:02:22 -0400
Subject: [PATCH 05/22] Changed tutorial box text for Discourse. Fixes #296.
---
app/templates/activity.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/templates/activity.html b/app/templates/activity.html
index fcdde1a..b3ee72a 100644
--- a/app/templates/activity.html
+++ b/app/templates/activity.html
@@ -152,7 +152,7 @@
{% if DISCOURSE_ENABLED %}
{% call render_tutorial_step(4, max_step, extra_classes="on-right-side") %}
- {{ gettext("You can now participate in our Discourse forum! Check it out.") }}
+ {{ gettext("Have a question or something to share with the network? Discuss it with like-minded innovators in one of our channels!") }}
{% endcall %}
{% endif %}
From e3a7295e310246e6fe9db254234b73eb9802505d Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 07:24:09 -0400
Subject: [PATCH 06/22] reload dev server when yaml files change.
---
app/factory.py | 18 +++++++++++++++---
manage.py | 9 ++++++---
2 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/app/factory.py b/app/factory.py
index c073050..7c946a6 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -41,6 +41,18 @@
'GA_TRACKING_CODE'
]
+CONFIG_YML_FILENAME = '/noi/app/config/config.yml'
+
+LOCAL_CONFIG_YML_FILENAME = '/noi/app/config/local_config.yml'
+
+DEPLOYMENTS_YML_FILENAME = '/noi/app/data/deployments.yaml'
+
+YML_FILENAMES = [
+ CONFIG_YML_FILENAME,
+ LOCAL_CONFIG_YML_FILENAME,
+ DEPLOYMENTS_YML_FILENAME
+]
+
class DeploySQLAlchemyUserDatastore(SQLAlchemyUserDatastore):
'''
Subclass of SQLAlchemyUserDatastore that overrides `get_user` to take app
@@ -71,12 +83,12 @@ def create_app(config=None): #pylint: disable=too-many-statements
'''
app = Flask(__name__)
- with open('/noi/app/config/config.yml', 'r') as config_file:
+ with open(CONFIG_YML_FILENAME, 'r') as config_file:
app.config.update(yaml.load(config_file))
if config is None:
try:
- with open('/noi/app/config/local_config.yml', 'r') as config_file:
+ with open(LOCAL_CONFIG_YML_FILENAME, 'r') as config_file:
app.config.update(yaml.load(config_file))
except IOError:
app.logger.warn("No local_config.yml file")
@@ -84,7 +96,7 @@ def create_app(config=None): #pylint: disable=too-many-statements
else:
app.config.update(config)
- with open('/noi/app/data/deployments.yaml') as deployments_yaml:
+ with open(DEPLOYMENTS_YML_FILENAME) as deployments_yaml:
deployments = yaml.load(deployments_yaml)
l10n.configure_app(app)
diff --git a/manage.py b/manage.py
index 1277a2b..29d484c 100644
--- a/manage.py
+++ b/manage.py
@@ -15,7 +15,7 @@
from app import (mail, models, sass, email_errors, LEVELS, ORG_TYPES, stats,
questionnaires, blog_posts, linkedin, slack)
-from app.factory import create_app
+from app.factory import create_app, YML_FILENAMES
from app.models import db, User
from app.utils import csv_reader
from app.tests.factories import UserFactory
@@ -28,7 +28,7 @@
from flask_mail import Message
from flask_security.recoverable import send_reset_password_instructions
from flask.ext.script import Command
-from flask.ext.script.commands import InvalidCommand
+from flask.ext.script.commands import InvalidCommand, Server, Shell
from random import choice
from sqlalchemy.exc import IntegrityError
@@ -45,7 +45,10 @@
app = create_app() #pylint: disable=invalid-name
migrate = Migrate(app, db) #pylint: disable=invalid-name
-manager = Manager(app) #pylint: disable=invalid-name
+manager = Manager(app, with_default_commands=False)
+
+manager.add_command('shell', Shell())
+manager.add_command('runserver', Server(extra_files=YML_FILENAMES))
manager.add_command('discourse', DiscourseCommand)
manager.add_command('noi1', Noi1Command)
From f5aa309096fca97041be427b2c9702a5c39e1072 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 08:24:33 -0400
Subject: [PATCH 07/22] link to discourse category in activity feed.
---
app/discourse/models.py | 11 +++++++++++
app/static/sass/_activity.scss | 8 ++++++++
app/templates/_activity_events.html | 2 +-
app/tests/test_discourse.py | 8 ++++++++
migrations/versions/312a48268855_.py | 28 ++++++++++++++++++++++++++++
5 files changed, 56 insertions(+), 1 deletion(-)
create mode 100644 migrations/versions/312a48268855_.py
diff --git a/app/discourse/models.py b/app/discourse/models.py
index c0e5f28..69439d0 100644
--- a/app/discourse/models.py
+++ b/app/discourse/models.py
@@ -30,6 +30,10 @@ class DiscourseTopicEvent(models.UserEvent):
posts_count = Column(types.Integer)
+ category_name = Column(types.Text)
+
+ category_slug = Column(types.Text)
+
__mapper_args__ = {
'polymorphic_identity': 'discourse_topic_event'
}
@@ -38,6 +42,10 @@ class DiscourseTopicEvent(models.UserEvent):
def url(self):
return config.url('/t/%s/%d' % (self.slug, self.discourse_id))
+ @property
+ def category_url(self):
+ return config.url('/c/%s' % self.category_slug)
+
@classmethod
def _get_or_create(cls, discourse_id):
msg = db.session.query(cls).\
@@ -56,6 +64,9 @@ def _update_category(cls, category):
msg.updated_at = parse_iso_datetime(topic['bumped_at'])
msg.slug = topic['slug']
+ msg.category_name = category['name']
+ msg.category_slug = category['slug']
+
user = User.find_by_username(topic['last_poster']['username'])
msg.user = user
diff --git a/app/static/sass/_activity.scss b/app/static/sass/_activity.scss
index 902e89c..606901a 100644
--- a/app/static/sass/_activity.scss
+++ b/app/static/sass/_activity.scss
@@ -142,6 +142,14 @@
}
}
+ .e-discourse-category {
+ font-size: 10px;
+ padding: 2px;
+ color: gray;
+ border: 1px dotted lightgray;
+ white-space: pre;
+ }
+
.e-feed-message {
flex: 1; /* NEW, Spec - Firefox, Chrome, Opera */
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index d8e296a..7597a53 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -33,7 +33,7 @@
{% if event.user %}
{{ gettext('%(user)s posted in Discourse.', user=user_link(event.user)) }}
{% endif %}
- {{ event.title }}
+ {{ event.title }} {{ event.category_name }}
{% if event.excerpt %}
{{ event.excerpt|safe }}
{% endif %}
diff --git a/app/tests/test_discourse.py b/app/tests/test_discourse.py
index 5f2ecc1..03e0989 100644
--- a/app/tests/test_discourse.py
+++ b/app/tests/test_discourse.py
@@ -196,6 +196,10 @@ def test_url_works(self):
evt = DiscourseTopicEvent(discourse_id=5, slug='beep-boop')
self.assertEqual(evt.url, 'http://discourse/t/beep-boop/5')
+ def test_category_url_works(self):
+ evt = DiscourseTopicEvent(category_slug='beep-boop')
+ self.assertEqual(evt.category_url, 'http://discourse/c/beep-boop')
+
@mock.patch('app.discourse.api.get')
def test_update_works(self, get):
fake_topics = [
@@ -225,6 +229,8 @@ def test_update_works(self, get):
},
{
'read_restricted': False,
+ 'name': 'Funky Things',
+ 'slug': 'funky-things',
'topics': fake_topics
}
]
@@ -253,6 +259,8 @@ def test_update_works(self, get):
self.assertIsNone(event.user)
self.assertIsNone(event.excerpt)
self.assertEqual(event.title, 'Hello There')
+ self.assertEqual(event.category_name, 'Funky Things')
+ self.assertEqual(event.category_slug, 'funky-things')
self.assertEqual(event.posts_count, 6)
# Now simulate a new reply.
diff --git a/migrations/versions/312a48268855_.py b/migrations/versions/312a48268855_.py
new file mode 100644
index 0000000..3132ccf
--- /dev/null
+++ b/migrations/versions/312a48268855_.py
@@ -0,0 +1,28 @@
+"""Add category_name, category_slug to discourse_topics
+
+Revision ID: 312a48268855
+Revises: 4309a04aaea9
+Create Date: 2016-03-22 12:03:26.330530
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '312a48268855'
+down_revision = '4309a04aaea9'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('discourse_topics', sa.Column('category_name', sa.Text(), nullable=True))
+ op.add_column('discourse_topics', sa.Column('category_slug', sa.Text(), nullable=True))
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('discourse_topics', 'category_slug')
+ op.drop_column('discourse_topics', 'category_name')
+ ### end Alembic commands ###
From c3eb32a38434b20f705dc4e267d7c313299c0503 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 08:57:45 -0400
Subject: [PATCH 08/22] Only show discourse category if it's non-null.
---
app/templates/_activity_events.html | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index 7597a53..8ae1d39 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -33,7 +33,10 @@
{% if event.user %}
{{ gettext('%(user)s posted in Discourse.', user=user_link(event.user)) }}
{% endif %}
- {{ event.title }} {{ event.category_name }}
+
+ {{ event.title }}
+ {% if event.category_name %}{{ event.category_name }}{% endif %}
+
{% if event.excerpt %}
{{ event.excerpt|safe }}
{% endif %}
From 7564e345c7bf881884420f5ca5cc8349e8a5bda4 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 11:59:19 -0400
Subject: [PATCH 09/22] Open discourse links in same tab. Fixes #298.
---
app/templates/__base_ui__.html | 2 +-
app/templates/_activity_events.html | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/templates/__base_ui__.html b/app/templates/__base_ui__.html
index c815d80..8f6326b 100644
--- a/app/templates/__base_ui__.html
+++ b/app/templates/__base_ui__.html
@@ -36,7 +36,7 @@ {{ gettext('Network of Innovators') }}
{{ gettext('View Network') }}
{% if DISCOURSE_ENABLED %}
-
+
feedback
{{ gettext('Discourse') }}
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index 8ae1d39..4717473 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -35,13 +35,13 @@
{% endif %}
{{ event.title }}
- {% if event.category_name %}{{ event.category_name }}{% endif %}
+ {% if event.category_name %}{{ event.category_name }}{% endif %}
{% if event.excerpt %}
{{ event.excerpt|safe }}
{% endif %}
-
open_in_new
+ open_in_new
{% if event.posts_count > 1 %}
{{ gettext("Read %(posts_count)s replies on Discourse", posts_count=event.posts_count) }}
From 2e66e123135943b49de8a6e08a42beb08ad3523d Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 16:38:50 -0400
Subject: [PATCH 10/22] swap discourse and view network buttons on toolbar
(#299).
---
app/templates/__base_ui__.html | 8 ++++----
app/templates/activity.html | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/templates/__base_ui__.html b/app/templates/__base_ui__.html
index 8f6326b..c6ac83f 100644
--- a/app/templates/__base_ui__.html
+++ b/app/templates/__base_ui__.html
@@ -31,10 +31,6 @@ {{ gettext('Network of Innovators') }}
search
{{ gettext('Find Innovators') }}
-
- language
- {{ gettext('View Network') }}
-
{% if DISCOURSE_ENABLED %}
feedback
@@ -46,6 +42,10 @@ {{ gettext('Network of Innovators') }}
{{ gettext('Share Feedback') }}
{% endif %}
+
+ language
+ {{ gettext('View Network') }}
+
diff --git a/app/templates/activity.html b/app/templates/activity.html
index b3ee72a..12f46f9 100644
--- a/app/templates/activity.html
+++ b/app/templates/activity.html
@@ -151,7 +151,7 @@
{% endcall %}
{% if DISCOURSE_ENABLED %}
-{% call render_tutorial_step(4, max_step, extra_classes="on-right-side") %}
+{% call render_tutorial_step(4, max_step) %}
{{ gettext("Have a question or something to share with the network? Discuss it with like-minded innovators in one of our channels!") }}
{% endcall %}
{% endif %}
From 3dcebb5036a249ad79a8ed2506d0d70ec40db019 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 16:41:34 -0400
Subject: [PATCH 11/22] Refer to 'Discourse' as 'Discuss'. Fixes #299.
---
app/templates/__base_ui__.html | 2 +-
app/templates/_activity_events.html | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/templates/__base_ui__.html b/app/templates/__base_ui__.html
index c6ac83f..5bb925c 100644
--- a/app/templates/__base_ui__.html
+++ b/app/templates/__base_ui__.html
@@ -34,7 +34,7 @@ {{ gettext('Network of Innovators') }}
{% if DISCOURSE_ENABLED %}
feedback
- {{ gettext('Discourse') }}
+ {{ gettext('Discuss') }}
{% else %}
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index 4717473..3be8bb8 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -31,7 +31,7 @@
{% endif %}
From 65d07e6b4084540c3d454db590d40456f117e52e Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 22 Mar 2016 18:40:46 -0400
Subject: [PATCH 12/22] Show every Discourse post w/ excerpt. Fixes #301.
---
app/discourse/models.py | 56 +++++++++++++++++++----------
app/templates/_activity_events.html | 6 +---
app/tests/test_discourse.py | 55 ++++++++++++++++++++--------
3 files changed, 78 insertions(+), 39 deletions(-)
diff --git a/app/discourse/models.py b/app/discourse/models.py
index 69439d0..dd5901d 100644
--- a/app/discourse/models.py
+++ b/app/discourse/models.py
@@ -1,5 +1,5 @@
import datetime
-from sqlalchemy import types, Column, ForeignKey
+from sqlalchemy import types, Column, ForeignKey, UniqueConstraint
from ..models import db, User
from .. import models
@@ -20,7 +20,9 @@ class DiscourseTopicEvent(models.UserEvent):
id = Column(types.Integer, ForeignKey('user_events.id'), primary_key=True)
- discourse_id = Column(types.Integer, unique=True)
+ discourse_id = Column(types.Integer)
+
+ post_number = Column(types.Integer)
slug = Column(types.Text)
@@ -34,52 +36,68 @@ class DiscourseTopicEvent(models.UserEvent):
category_slug = Column(types.Text)
+ __table_args__ = (UniqueConstraint('discourse_id', 'post_number'),)
+
__mapper_args__ = {
'polymorphic_identity': 'discourse_topic_event'
}
@property
def url(self):
- return config.url('/t/%s/%d' % (self.slug, self.discourse_id))
+ return config.url('/t/%s/%d/%d' % (self.slug, self.discourse_id,
+ self.post_number or 0))
@property
def category_url(self):
return config.url('/c/%s' % self.category_slug)
@classmethod
- def _get_or_create(cls, discourse_id):
+ def _get_or_create(cls, discourse_id, post_number=None):
msg = db.session.query(cls).\
- filter_by(discourse_id=discourse_id).first()
+ filter_by(discourse_id=discourse_id,
+ post_number=post_number).first()
if msg is None:
- msg = cls(discourse_id=discourse_id)
+ msg = cls(discourse_id=discourse_id, post_number=post_number)
return msg
@classmethod
- def _update_category(cls, category):
- topics = category.get('topics', [])
- for topic in topics:
- if not topic['visible']: continue
- msg = cls._get_or_create(discourse_id=topic['id'])
- msg.created_at = parse_iso_datetime(topic['created_at'])
- msg.updated_at = parse_iso_datetime(topic['bumped_at'])
+ def _update_topic(cls, category, topic):
+ req = api.get('/t/%d/last.json' % topic['id'])
+
+ if req.status_code != 200:
+ return req.raise_for_status()
+
+ topic_detail = req.json()
+
+ for post in topic_detail['post_stream']['posts']:
+ if post['hidden'] or not post['cooked']: continue
+
+ msg = cls._get_or_create(discourse_id=topic['id'],
+ post_number=post['post_number'])
+ msg.created_at = parse_iso_datetime(post['created_at'])
+ msg.updated_at = parse_iso_datetime(post['updated_at'])
msg.slug = topic['slug']
msg.category_name = category['name']
msg.category_slug = category['slug']
- user = User.find_by_username(topic['last_poster']['username'])
+ user = User.find_by_username(post['username'])
msg.user = user
- # Argh, it looks like only pinned topics have excerpts
- # for now:
- #
- # https://meta.discourse.org/t/get-excerpt-for-regular-topics/33482
- msg.excerpt = topic.get('excerpt')
+ msg.excerpt = post['cooked']
msg.title = topic['title']
msg.posts_count = topic['posts_count']
db.session.add(msg)
+ @classmethod
+ def _update_category(cls, category):
+ topics = category.get('topics', [])
+ for topic in topics:
+ if not topic['visible']: continue
+
+ cls._update_topic(category, topic)
+
@classmethod
def update(cls):
req = api.get('/categories.json')
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index 3be8bb8..609a534 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -43,11 +43,7 @@
diff --git a/app/tests/test_discourse.py b/app/tests/test_discourse.py
index 03e0989..7dafc9f 100644
--- a/app/tests/test_discourse.py
+++ b/app/tests/test_discourse.py
@@ -193,8 +193,13 @@ class DiscourseTopicEventTests(DbTestCase):
BASE_APP_CONFIG.update(DISCOURSE=FAKE_DISCOURSE_CONFIG)
def test_url_works(self):
+ evt = DiscourseTopicEvent(discourse_id=5, post_number=1,
+ slug='beep-boop')
+ self.assertEqual(evt.url, 'http://discourse/t/beep-boop/5/1')
+
+ def test_url_works_with_no_post_number(self):
evt = DiscourseTopicEvent(discourse_id=5, slug='beep-boop')
- self.assertEqual(evt.url, 'http://discourse/t/beep-boop/5')
+ self.assertEqual(evt.url, 'http://discourse/t/beep-boop/5/0')
def test_category_url_works(self):
evt = DiscourseTopicEvent(category_slug='beep-boop')
@@ -210,12 +215,7 @@ def test_update_works(self, get):
{
'visible': True,
'id': 14,
- 'bumped_at': '2016-02-18T14:27:48.103Z',
- 'created_at': '2016-02-15T14:27:48.062Z',
'posts_count': 6,
- 'last_poster': {
- 'username': 'system'
- },
'title': 'Hello There',
'slug': 'hello-there',
}
@@ -236,14 +236,39 @@ def test_update_works(self, get):
]
}
}
+ fake_post = {
+ 'hidden': False,
+ 'cooked': 'Hello
',
+ 'post_number': 1,
+ 'created_at': '2016-02-15T14:27:48.062Z',
+ 'updated_at': '2016-02-18T14:27:48.103Z',
+ 'username': 'system',
+ }
+ fake_topic_detail = {
+ 'post_stream': {
+ 'posts': [fake_post]
+ }
+ }
- get.return_value.status_code = 200
- get.return_value.json.return_value = fake_categories
+ def get_url(url):
+ retval = mock.MagicMock()
+ retval.raise_for_status.side_effect = Exception('kaboom')
+ if url == '/categories.json':
+ retval.status_code = 200
+ retval.json.return_value = fake_categories
+ else:
+ retval.status_code = 200
+ retval.json.return_value = fake_topic_detail
+ return retval
+
+ get.side_effect = get_url
DiscourseTopicEvent.update()
- get.assert_called_once_with('/categories.json')
- get.return_value.raise_for_status.assert_not_called()
+ get.assert_has_calls([
+ mock.call('/categories.json'),
+ mock.call('/t/14/last.json')
+ ])
events = db.session.query(Event).all()
self.assertEqual(len(events), 1)
@@ -257,23 +282,23 @@ def test_update_works(self, get):
datetime.datetime(2016, 2, 18, 14, 27, 48, 103000))
self.assertEqual(event.slug, 'hello-there')
self.assertIsNone(event.user)
- self.assertIsNone(event.excerpt)
+ self.assertEqual(event.excerpt, 'Hello
')
self.assertEqual(event.title, 'Hello There')
self.assertEqual(event.category_name, 'Funky Things')
self.assertEqual(event.category_slug, 'funky-things')
self.assertEqual(event.posts_count, 6)
- # Now simulate a new reply.
+ # Now simulate an edited post.
- fake_topics[1]['bumped_at'] = '2016-02-20T14:27:48.103Z'
- fake_topics[1]['posts_count'] += 1
+ fake_post['updated_at'] = '2016-02-20T14:27:48.103Z'
+ fake_post['cooked'] = 'Blah
'
DiscourseTopicEvent.update()
self.assertEqual(db.session.query(Event).all(), [event])
self.assertEqual(event.updated_at,
datetime.datetime(2016, 2, 20, 14, 27, 48, 103000))
- self.assertEqual(event.posts_count, 7)
+ self.assertEqual(event.excerpt, 'Blah
')
def test_get_or_create_can_get_existing(self):
evt = DiscourseTopicEvent(discourse_id=15)
From 426fc5f68533c216524428f33f05aa6a1824ccda Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 23 Mar 2016 06:56:54 -0400
Subject: [PATCH 13/22] rebase hrefs from discourse snippets.
---
app/discourse/models.py | 15 +++++++++++++++
app/templates/_activity_events.html | 2 +-
app/tests/test_discourse.py | 5 +++++
3 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/app/discourse/models.py b/app/discourse/models.py
index dd5901d..f36192b 100644
--- a/app/discourse/models.py
+++ b/app/discourse/models.py
@@ -15,6 +15,17 @@ def parse_iso_datetime(text):
return datetime.datetime.strptime(text, "%Y-%m-%dT%H:%M:%S.%fZ")
+def rebase_hrefs(html, origin=None):
+ '''
+ >>> rebase_hrefs(u'hi', 'http://bop')
+ u'hi'
+ '''
+
+ if origin is None:
+ origin = config.origin
+
+ return html.replace(u'href="/', u'href="%s/' % unicode(origin))
+
class DiscourseTopicEvent(models.UserEvent):
__tablename__ = 'discourse_topics'
@@ -42,6 +53,10 @@ class DiscourseTopicEvent(models.UserEvent):
'polymorphic_identity': 'discourse_topic_event'
}
+ @property
+ def cleaned_excerpt(self):
+ return rebase_hrefs(self.excerpt or '')
+
@property
def url(self):
return config.url('/t/%s/%d/%d' % (self.slug, self.discourse_id,
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index 609a534..a3f8007 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -38,7 +38,7 @@
{% if event.category_name %}{{ event.category_name }}{% endif %}
{% if event.excerpt %}
- {{ event.excerpt|safe }}
+ {{ event.cleaned_excerpt|safe }}
{% endif %}
open_in_new
diff --git a/app/tests/test_discourse.py b/app/tests/test_discourse.py
index 7dafc9f..d9424cc 100644
--- a/app/tests/test_discourse.py
+++ b/app/tests/test_discourse.py
@@ -205,6 +205,11 @@ def test_category_url_works(self):
evt = DiscourseTopicEvent(category_slug='beep-boop')
self.assertEqual(evt.category_url, 'http://discourse/c/beep-boop')
+ def test_cleaned_excerpt_works(self):
+ evt = DiscourseTopicEvent(excerpt='hi')
+ self.assertEqual(evt.cleaned_excerpt,
+ '
hi')
+
@mock.patch('app.discourse.api.get')
def test_update_works(self, get):
fake_topics = [
From cada612e6b86289cf3621f09d7637ef0c21ed03d Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 23 Mar 2016 07:02:22 -0400
Subject: [PATCH 14/22] Add migrations for discourse_topics changes.
---
migrations/versions/28ff85fcc188_.py | 26 ++++++++++++++++++++++++++
migrations/versions/51bdb7c7928a_.py | 28 ++++++++++++++++++++++++++++
2 files changed, 54 insertions(+)
create mode 100644 migrations/versions/28ff85fcc188_.py
create mode 100644 migrations/versions/51bdb7c7928a_.py
diff --git a/migrations/versions/28ff85fcc188_.py b/migrations/versions/28ff85fcc188_.py
new file mode 100644
index 0000000..4605969
--- /dev/null
+++ b/migrations/versions/28ff85fcc188_.py
@@ -0,0 +1,26 @@
+"""add post_number column to discourse_topics
+
+Revision ID: 28ff85fcc188
+Revises: 312a48268855
+Create Date: 2016-03-22 22:00:07.520355
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '28ff85fcc188'
+down_revision = '312a48268855'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('discourse_topics', sa.Column('post_number', sa.Integer(), nullable=True))
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('discourse_topics', 'post_number')
+ ### end Alembic commands ###
diff --git a/migrations/versions/51bdb7c7928a_.py b/migrations/versions/51bdb7c7928a_.py
new file mode 100644
index 0000000..2c15093
--- /dev/null
+++ b/migrations/versions/51bdb7c7928a_.py
@@ -0,0 +1,28 @@
+"""enforce uniqueness based on discourse_id and post_number
+
+Revision ID: 51bdb7c7928a
+Revises: 28ff85fcc188
+Create Date: 2016-03-22 22:02:14.557660
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '51bdb7c7928a'
+down_revision = '28ff85fcc188'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_constraint(u'discourse_topics_discourse_id_key', 'discourse_topics', type_='unique')
+ op.create_unique_constraint(None, 'discourse_topics', ['discourse_id', 'post_number'])
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_constraint(None, 'discourse_topics', type_='unique')
+ op.create_unique_constraint(u'discourse_topics_discourse_id_key', 'discourse_topics', ['discourse_id'])
+ ### end Alembic commands ###
From 4ffb7839b19ba5593c60d60d1eb19e8ede087437 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 23 Mar 2016 07:59:57 -0400
Subject: [PATCH 15/22] Make AdminTestCase easier to reuse.
---
app/tests/test_admin.py | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/app/tests/test_admin.py b/app/tests/test_admin.py
index ad47232..4ac4993 100644
--- a/app/tests/test_admin.py
+++ b/app/tests/test_admin.py
@@ -11,6 +11,12 @@ class AdminTestCase(ViewTestCase):
BASE_APP_CONFIG['ADMIN_UI_USERS'] = ['admin@example.org']
+ def login_as_normal_user(self):
+ self.login('normal@example.org', 'password')
+
+ def login_as_admin_user(self):
+ self.login('admin@example.org', 'password')
+
def setUp(self):
super(AdminTestCase, self).setUp()
self.admin_user = self.create_user(u'admin@example.org', 'password')
@@ -23,11 +29,11 @@ def test_anonymous_users_are_redirected_to_login(self):
'/login?next=%2Fadmin%2Fuser%2F')
def test_non_admin_users_receive_403(self):
- self.login('normal@example.org', 'password')
+ self.login_as_normal_user()
self.assert403(self.client.get('/admin/user/'))
def test_admin_users_are_not_redirected_to_login(self):
- self.login('admin@example.org', 'password')
+ self.login_as_admin_user()
self.assert200(self.client.get('/admin/user/'))
@@ -37,11 +43,11 @@ def test_anonymous_users_are_redirected_to_login(self):
'/login?next=%2Fadmin%2Fstats%2F')
def test_non_admin_users_receive_403(self):
- self.login('normal@example.org', 'password')
+ self.login_as_normal_user()
self.assert403(self.client.get('/admin/stats/'))
def test_admin_users_are_not_redirected_to_login(self):
- self.login('admin@example.org', 'password')
+ self.login_as_admin_user()
self.assert200(self.client.get('/admin/stats/'))
From 5498bb23f0fba5ca1276dd187e32193eadfb5e59 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 23 Mar 2016 08:01:50 -0400
Subject: [PATCH 16/22] Add discourse admin page.
---
app/admin.py | 8 ++++++++
app/discourse/__init__.py | 6 +++++-
app/discourse/admin.py | 16 ++++++++++++++++
app/factory.py | 3 ++-
app/templates/admin/discourse_index.html | 19 +++++++++++++++++++
app/tests/test_discourse.py | 20 ++++++++++++++++++++
6 files changed, 70 insertions(+), 2 deletions(-)
create mode 100644 app/discourse/admin.py
create mode 100644 app/templates/admin/discourse_index.html
diff --git a/app/admin.py b/app/admin.py
index 28f6315..0597a0f 100644
--- a/app/admin.py
+++ b/app/admin.py
@@ -119,6 +119,14 @@ def scaffold_form(self):
return form_class
+def add_admin_view(app, view):
+ if not (hasattr(app, 'extensions') and
+ len(app.extensions.get('admin', [])) == 1):
+ return
+
+ app.extensions['admin'][0].add_view(view)
+
+
def init_app(app):
admin = Admin(app, name=NAME, template_mode='bootstrap3')
admin.add_view(UserModelView(User, db.session))
diff --git a/app/discourse/__init__.py b/app/discourse/__init__.py
index ef0c512..d8b552b 100644
--- a/app/discourse/__init__.py
+++ b/app/discourse/__init__.py
@@ -1,8 +1,9 @@
from flask_security.signals import user_registered, user_confirmed
from flask.ext.login import user_logged_out
+from ..admin import add_admin_view
from ..signals import user_changed_profile
-from . import views, sso
+from . import views, sso, admin
from .config import DiscourseConfig
def init_app(app):
@@ -12,6 +13,9 @@ def init_app(app):
app.jinja_env.globals['discourse_url'] = config.url
app.register_blueprint(views.views)
+ add_admin_view(app, admin.DiscourseView(name='Discourse',
+ endpoint='discourse_admin'))
+
if app.config['NOI_CAN_UNCONFIRMED_USERS_FULLY_REGISTER']:
raise Exception('Discourse support is incompatible with '
'NOI_CAN_UNCONFIRMED_USERS_FULLY_REGISTER')
diff --git a/app/discourse/admin.py b/app/discourse/admin.py
new file mode 100644
index 0000000..d8a5747
--- /dev/null
+++ b/app/discourse/admin.py
@@ -0,0 +1,16 @@
+from flask import flash, redirect, url_for
+from flask_admin import Admin, BaseView, expose
+
+from ..admin import AdminPermissionRequiredMixin
+from . import models
+
+class DiscourseView(AdminPermissionRequiredMixin, BaseView):
+ @expose('/')
+ def index(self):
+ return self.render('admin/discourse_index.html')
+
+ @expose('/topics/recache', methods=['POST'])
+ def recache_topics(self):
+ models.DiscourseTopicEvent.update()
+ flash('Discourse topics recached.')
+ return redirect(url_for('discourse_admin.index'))
diff --git a/app/factory.py b/app/factory.py
index 7c946a6..3e0d866 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -115,6 +115,8 @@ def create_app(config=None): #pylint: disable=too-many-statements
if not app.config['DEBUG'] and app.config.get('ADMINS'):
email_errors.init_app(app)
+ admin.init_app(app)
+
oauth.init_app(app)
if 'LINKEDIN' in app.config:
app.jinja_env.globals['LINKEDIN_ENABLED'] = True
@@ -160,7 +162,6 @@ def create_app(config=None): #pylint: disable=too-many-statements
app.config['SEARCH_DEPLOYMENTS'].append(noi_deploy)
babel.init_app(app)
l10n.init_app(app)
- admin.init_app(app)
app.config['DOMAINS'] = this_deployment.get('domains',
default_deployment['domains'])
diff --git a/app/templates/admin/discourse_index.html b/app/templates/admin/discourse_index.html
new file mode 100644
index 0000000..e4d495e
--- /dev/null
+++ b/app/templates/admin/discourse_index.html
@@ -0,0 +1,19 @@
+{% extends "admin/master.html" %}
+
+{% block body %}
+
+
+
+
+
+
+
+ This will recache Discourse topics in the activity feed. Use this
+ if the activity feed isn't displaying the latest Discourse topics.
+
+
+
+{% endblock %}
diff --git a/app/tests/test_discourse.py b/app/tests/test_discourse.py
index d9424cc..22d68b6 100644
--- a/app/tests/test_discourse.py
+++ b/app/tests/test_discourse.py
@@ -9,6 +9,7 @@
from .test_views import ViewTestCase
from .test_models import DbTestCase
+from .test_admin import AdminTestCase
from ..models import User, Event, db
from ..signals import user_changed_profile
from ..discourse import sso, api
@@ -339,6 +340,25 @@ def test_parent_event_is_deleted(self):
self.assertEqual(db.session.query(Event).count(), 0)
+
+class AdminViewTests(AdminTestCase):
+ BASE_APP_CONFIG = AdminTestCase.BASE_APP_CONFIG.copy()
+
+ BASE_APP_CONFIG.update(DISCOURSE=FAKE_DISCOURSE_CONFIG)
+
+ def test_index_works(self):
+ self.login_as_admin_user()
+ self.assert200(self.client.get('/admin/discourse_admin/'))
+
+ @mock.patch('app.discourse.models.DiscourseTopicEvent.update')
+ def test_recache_topics_works(self, update):
+ self.login_as_admin_user()
+ self.assertRedirects(
+ self.client.post('/admin/discourse_admin/topics/recache'),
+ '/admin/discourse_admin/'
+ )
+ update.assert_called_once_with()
+
class ViewTests(ViewTestCase):
BASE_APP_CONFIG = ViewTestCase.BASE_APP_CONFIG.copy()
From dfe852c83d314c192f2ac7cc060978149463a454 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 6 Apr 2016 13:12:46 -0400
Subject: [PATCH 17/22] Link discourse topic name, change 'read more' to
'reply'. Fixes #304.
---
app/templates/_activity_events.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/templates/_activity_events.html b/app/templates/_activity_events.html
index a3f8007..2990417 100644
--- a/app/templates/_activity_events.html
+++ b/app/templates/_activity_events.html
@@ -34,7 +34,7 @@
{{ gettext('%(user)s posted in Discuss.', user=user_link(event.user)) }}
{% endif %}
- {{ event.title }}
+ {{ event.title }}
{% if event.category_name %}{{ event.category_name }}{% endif %}
{% if event.excerpt %}
@@ -43,7 +43,7 @@
From fa38182f12e791071dafc75bc730d0ec5fd56ca4 Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Tue, 19 Apr 2016 06:46:51 -0400
Subject: [PATCH 18/22] Do not escape tags in blog posts
---
app/blog_posts.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/blog_posts.py b/app/blog_posts.py
index bb63432..e7d572d 100644
--- a/app/blog_posts.py
+++ b/app/blog_posts.py
@@ -3,6 +3,8 @@
from flask import Markup
import feedparser
+ALLOWED_TAGS = ['p']
+
def truncate(text, max_words):
'''
Truncates the given text with an ellipsis if it's more than the given
@@ -28,7 +30,7 @@ def summarize(feed, max_entries=3, max_words_per_entry=20):
desc = truncate(entry.description, max_words_per_entry)
post = dict(
title=entry.title,
- description=Markup(bleach.clean(desc)),
+ description=Markup(bleach.clean(desc, tags=ALLOWED_TAGS)),
link=entry.link,
domain=urlparse.urlparse(entry.link).netloc
)
From 2d495f0778b40251b0c3883e8e6b7ade190de469 Mon Sep 17 00:00:00 2001
From: Denny Tek
Date: Wed, 16 Mar 2016 16:22:51 -0400
Subject: [PATCH 19/22] Add custom 404 page.
---
app/error_handlers.py | 8 ++++++++
app/static/sass/_error-page.scss | 20 ++++++++++++++++++++
app/static/sass/styles.scss | 2 ++
app/templates/404.html | 15 +++++++++++++++
4 files changed, 45 insertions(+)
create mode 100644 app/error_handlers.py
create mode 100644 app/static/sass/_error-page.scss
create mode 100644 app/templates/404.html
diff --git a/app/error_handlers.py b/app/error_handlers.py
new file mode 100644
index 0000000..65b1515
--- /dev/null
+++ b/app/error_handlers.py
@@ -0,0 +1,8 @@
+import flask
+from flask import Blueprint, render_template
+
+blueprint = flask.Blueprint('error_handlers', __name__)
+
+@blueprint.app_errorhandler(404)
+def page_not_found(e):
+ return render_template('404.html'), 404
diff --git a/app/static/sass/_error-page.scss b/app/static/sass/_error-page.scss
new file mode 100644
index 0000000..3382fcb
--- /dev/null
+++ b/app/static/sass/_error-page.scss
@@ -0,0 +1,20 @@
+.e-error-display {
+ background-color: $blue;
+ text-align: center;
+ text-transform: uppercase;
+ padding-top: 4em;
+ h2,h3,h4 {
+ color: $white-80;;
+ padding: .5em;
+ }
+}
+
+.error-page-icon {
+ color: $white-80;;
+ font-size: 4em;
+ }
+
+.e-error-display a {
+ color: $white-90;
+ font-weight: bold;
+}
diff --git a/app/static/sass/styles.scss b/app/static/sass/styles.scss
index a37f45d..bef6bbb 100644
--- a/app/static/sass/styles.scss
+++ b/app/static/sass/styles.scss
@@ -96,6 +96,8 @@ body {
@import 'desktop-hack';
+@import 'error-page';
+
/* Useful for making blocks of content that we don't have time to style
* not look utterly unstyled and horrible. */
.b-temporary-styling {
diff --git a/app/templates/404.html b/app/templates/404.html
new file mode 100644
index 0000000..79c7720
--- /dev/null
+++ b/app/templates/404.html
@@ -0,0 +1,15 @@
+{% extends '__base_ui__.html' %}
+
+{% block title %}Page Not Found | Network of Innovators{% endblock %}
+{% block body_class %}b-landing-page{% endblock %}
+
+{% block content %}
+
+
+
error_outline
+
Error 404
+
We couldn't find what you were looking for
+
Try using the navigation above or click here to go back to the homepage
+
+
+{% endblock %}
From ec0468b8664993c8240a138273ba40d5436273ce Mon Sep 17 00:00:00 2001
From: Atul Varma
Date: Wed, 16 Mar 2016 16:23:10 -0400
Subject: [PATCH 20/22] Register error_handlers blueprint for custom 404 page.
---
app/factory.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/factory.py b/app/factory.py
index 3e0d866..fe0b635 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -101,6 +101,7 @@ def create_app(config=None): #pylint: disable=too-many-statements
l10n.configure_app(app)
+ app.register_blueprint(error_handlers.blueprint)
app.register_blueprint(views)
if app.config['DEBUG']:
app.register_blueprint(style_guide.views)
From a8457a31e083bb8ed0a294adc9a2e143eb2e6af8 Mon Sep 17 00:00:00 2001
From: Denny Tek
Date: Wed, 27 Apr 2016 09:36:23 -0400
Subject: [PATCH 21/22] Add error_handlers
---
app/factory.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/factory.py b/app/factory.py
index fe0b635..610e930 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -12,7 +12,7 @@
from app import (csrf, cache, mail, bcrypt, s3, assets, security, admin,
babel, alchemydumps, sass, email_errors, csp, oauth,
- linkedin, discourse, slack,
+ linkedin, discourse, slack, error_handlers,
QUESTIONNAIRES, NOI_COLORS, LEVELS, ORG_TYPES,
QUESTIONS_BY_ID, LEVELS_BY_SCORE, QUESTIONNAIRES_BY_ID)
from app.forms import (NOIForgotPasswordForm, NOILoginForm,
From 0b8d943f7d250de81c8cbb0dbd66ac897cc2148b Mon Sep 17 00:00:00 2001
From: Denny Tek
Date: Wed, 27 Apr 2016 09:37:59 -0400
Subject: [PATCH 22/22] Edit copy
---
app/templates/404.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/templates/404.html b/app/templates/404.html
index 79c7720..e83fc63 100644
--- a/app/templates/404.html
+++ b/app/templates/404.html
@@ -9,7 +9,7 @@
error_outline
Error 404
We couldn't find what you were looking for
- Try using the navigation above or click here to go back to the homepage
+
{% endblock %}