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

+

Try using the navigation above or go back to the homepage

{% endblock %}