From b6c44de561d226f857c6e3ea70d371564e43161e Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 6 Dec 2022 11:04:49 -0500 Subject: [PATCH 1/4] Make it easier to diable login locally. --- .gitignore | 1 + README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ app.py | 36 ++++++++++++++++++++---------------- requirements.in | 1 + requirements.txt | 12 ++---------- 5 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 29f322e..ecffb86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc .env +.flaskenv diff --git a/README.md b/README.md new file mode 100644 index 0000000..08caad9 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ + +# lil-blog-generator + +This app makes it easier to write [blog posts for the LIL website](https://github.com/harvard-lil/website-static#writing-blog-posts-docker-not-required). + +Write your post using the WYSIWYG editor, then click the download button. You'll get a markdown file that you can upload straight to Github, following the on-screen instructions. + + +## Running locally + +1. Make a virtual environment and install requirements: + +``` +pyenv virtualenv blog-generator +pyenv activate blog-generator +pip install -r requirements.txt +``` + +2. Configure local settings: + +``` +echo "FLASK_SECRET_KEY=adjkahflashfjdlsahfjahlsdfa" >> .flaskenv +echo "FLASK_ENV=development" >> .flaskenv +``` + +### (Recommended) + +To bypass login locally: +``` +echo "BYPASS_LOGIN=True" >> .flaskenv +``` + +3. Run the Flask development server + +``` +flask run +``` + + +## Authentication via Github + +In production, this app authenticates via Github. Configuration can be found in the "Developer Settings > OAuth Apps" section of the LIL Github organization; the application looks for corresponding [`GITHUB_*` env vars](https://github.com/harvard-lil/lil-blog-generator/blob/develop/app.py#L19-L21). + +You can partially test the integration locally: does the redirect occur? Does the auth flow work as expected on the Github side? + +But, after successfully authenticating, Github cannot, of course, hand things back to localhost. + +So unless specifically working on authentication, you will likely want to [disable login locally](#recommended). diff --git a/app.py b/app.py index e858d02..8d6fc42 100644 --- a/app.py +++ b/app.py @@ -23,6 +23,7 @@ app.config['SESSION_COOKIE_SECURE'] = literal_eval(environ.get('SESSION_COOKIE_SECURE', 'True')) app.config['LOGIN_EXPIRY_MINUTES'] = environ.get('LOGIN_EXPIRY', 30) app.config['LOG_LEVEL'] = environ.get('LOG_LEVEL', 'WARNING') +app.config['BYPASS_LOGIN'] = environ.get('BYPASS_LOGIN', False) # register error handlers error_handling.init_app(app) @@ -45,22 +46,25 @@ def setup_logging(): def login_required(func): - @wraps(func) - def handle_login(*args, **kwargs): - logged_in = session.get('logged_in') - valid_until = session.get('valid_until') - if valid_until: - valid = datetime.strptime(valid_until, '%Y-%m-%d %H:%M:%S') > datetime.utcnow() - else: - valid = False - if logged_in and logged_in == "yes" and valid: - app.logger.debug("User session valid") - return func(*args, **kwargs) - else: - app.logger.debug("Redirecting to GitHub") - session['next'] = request.url - return redirect('{}?scope=read:org&client_id={}'.format(AUTHORIZE_URL, app.config['GITHUB_CLIENT_ID'])) - return handle_login + if app.debug and not app.config['BYPASS_LOGIN']: + @wraps(func) + def handle_login(*args, **kwargs): + logged_in = session.get('logged_in') + valid_until = session.get('valid_until') + if valid_until: + valid = datetime.strptime(valid_until, '%Y-%m-%d %H:%M:%S') > datetime.utcnow() + else: + valid = False + if logged_in and logged_in == "yes" and valid: + app.logger.debug("User session valid") + return func(*args, **kwargs) + else: + app.logger.debug("Redirecting to GitHub") + session['next'] = request.url + return redirect('{}?scope=read:org&client_id={}'.format(AUTHORIZE_URL, app.config['GITHUB_CLIENT_ID'])) + return handle_login + app.logger.warning('Login disabled!') + return func def is_safe_url(target): diff --git a/requirements.in b/requirements.in index fa3dfe0..6b0d7ce 100644 --- a/requirements.in +++ b/requirements.in @@ -4,3 +4,4 @@ gunicorn requests pyyaml>=4.2b1 unidecode +python-dotenv diff --git a/requirements.txt b/requirements.txt index 82db7cb..bb78ea3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,10 +18,6 @@ gunicorn==20.1.0 # via -r requirements.in idna==2.10 # via requests -importlib-metadata==4.5.0 - # via - # click - # pep517 itsdangerous==2.0.1 # via flask jinja2==3.0.1 @@ -32,24 +28,20 @@ pep517==0.10.0 # via pip-tools pip-tools==6.1.0 # via -r requirements.in +python-dotenv==0.21.0 + # via -r requirements.in pyyaml==5.4.1 # via -r requirements.in requests==2.25.1 # via -r requirements.in toml==0.10.2 # via pep517 -typing-extensions==3.10.0.0 - # via importlib-metadata unidecode==1.2.0 # via -r requirements.in urllib3==1.26.5 # via requests werkzeug==2.0.1 # via flask -zipp==3.4.1 - # via - # importlib-metadata - # pep517 # The following packages are considered to be unsafe in a requirements file: # pip From 7579e8953244ee475ba737b3c91cc5fd12ba0957 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 6 Dec 2022 12:24:19 -0500 Subject: [PATCH 2/4] Add excerpt separator. --- app.py | 20 +++++++++++++++++++- templates/generator/download.html | 4 ++++ templates/generator/preview.html | 8 ++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 8d6fc42..6728940 100644 --- a/app.py +++ b/app.py @@ -33,6 +33,8 @@ ORGS_URL = 'https://api.github.com/user/orgs' REVOKE_TOKEN_URL = 'https://api.github.com/applications/{}/token'.format(app.config['GITHUB_CLIENT_ID']) +EXCERPT_SEPARATOR = '' + ### ### UTILS ### ### @@ -98,7 +100,21 @@ def landing(): if request.method == 'POST': r = requests.get('https://raw.githubusercontent.com/harvard-lil/website-static/develop/app/_data/people.yaml') authors = sorted(safe_load(r.text).keys()) - return render_template('generator/download.html', context={'heading': 'Download Post', 'authors': authors}) + if EXCERPT_SEPARATOR in request.form['content']: + excerpt_type = 'Custom' + excerpt = request.form['content'].split(EXCERPT_SEPARATOR)[0] + else: + excerpt_type = 'Default' + excerpt = request.form['content'].split("\r\n")[0] + return render_template( + 'generator/download.html', + context={ + 'heading': 'Download Post', + 'authors': authors, + 'excerpt_type': excerpt_type, + 'excerpt': excerpt + } + ) return render_template('generator/preview.html', context={'heading': 'Preview Blog Post'}) @@ -112,6 +128,8 @@ def download(): head_matter['author'] = request.form['author'] else: head_matter['guest-author'] = request.form['author'] + if request.form['excerpt-type'] == 'Custom': + head_matter['excerpt_separator'] = EXCERPT_SEPARATOR if request.form['tags']: head_matter['tags'] = request.form['tags'].split(' ') head_matter = dump(head_matter, sort_keys=False) diff --git a/templates/generator/download.html b/templates/generator/download.html index c8b069c..e5cdefe 100644 --- a/templates/generator/download.html +++ b/templates/generator/download.html @@ -35,6 +35,10 @@ + + + +
  1. Download your post's markdown file
  2. diff --git a/templates/generator/preview.html b/templates/generator/preview.html index 5299c7a..fdc5932 100644 --- a/templates/generator/preview.html +++ b/templates/generator/preview.html @@ -21,6 +21,7 @@
    +
@@ -29,6 +30,13 @@

You can enter any text you want into the editor, including HTML (audio/video embed tags...even script tags), and it will be rendered during preview as-is. So if you are tempted to paste in random javascript you find on the web, think twice.

+

Post Excerpt

+ You can configure how much of your post will appear on the [blog's index page](https://lil.law.harvard.edu/blog). + + The default is to show a "read more" link after the first paragraph. + + To move the "read more" button somewhere else, paste <!--more--> wherever you'd like it to appear. +

Images

Upload your image using the LIL blog media upload form, and note the URL it assigns to your image. Use the editor's image button, swapping in the image URL for the placeholder. It should look like this, when you are done:

From a46c018c6d05e0ab59bf1afb433fff992771bf70 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 6 Dec 2022 14:42:23 -0500 Subject: [PATCH 3/4] Opt out of excerpts. --- app.py | 7 +++-- static/css/custom.css | 13 +++++++++ templates/generator/download.html | 45 ++++++++++++++++++++++++------- templates/generator/preview.html | 2 ++ 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/app.py b/app.py index 6728940..0e498fd 100644 --- a/app.py +++ b/app.py @@ -128,8 +128,11 @@ def download(): head_matter['author'] = request.form['author'] else: head_matter['guest-author'] = request.form['author'] - if request.form['excerpt-type'] == 'Custom': - head_matter['excerpt_separator'] = EXCERPT_SEPARATOR + if request.form['use-excerpt'] == 'yes': + if request.form['excerpt-type'] == 'Custom': + head_matter['excerpt_separator'] = EXCERPT_SEPARATOR + else: + head_matter['no-excerpt'] = True if request.form['tags']: head_matter['tags'] = request.form['tags'].split(' ') head_matter = dump(head_matter, sort_keys=False) diff --git a/static/css/custom.css b/static/css/custom.css index 696fb8f..1a3f376 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -21,3 +21,16 @@ label.required:after { content:"*"; color:red; } + +form { + text-align: left; +} + +.use-excerpt label { + display: inline-block; + margin-right: 4px; +} + +.use-excerpt input:after { + content: " "; +} diff --git a/templates/generator/download.html b/templates/generator/download.html index e5cdefe..d82986d 100644 --- a/templates/generator/download.html +++ b/templates/generator/download.html @@ -35,9 +35,20 @@ - - - +
+
+ +
+ Excerpt on blog's index page? + + +
+ +
+ + + +
    @@ -58,11 +69,11 @@ document.getElementById('date').valueAsDate = new Date(); // Toggle between author and guest author - var author_column = document.getElementById('author-column'); - var author_select = document.getElementById('author-selector'); - var guest_author = document.getElementById('guest-author'); - var author = document.getElementById('author'); - var guest_author_input = document.createElement('input'); + const author_column = document.getElementById('author-column'); + const author_select = document.getElementById('author-selector'); + const guest_author = document.getElementById('guest-author'); + const author = document.getElementById('author'); + const guest_author_input = document.createElement('input'); guest_author_input.type = 'text'; guest_author_input.name = 'author'; guest_author_input.setAttribute('aria-label' , 'guest author name'); @@ -83,8 +94,8 @@ author.click(); // Author stored in local storage for better UX - var selectOption = author_select.options[author_select.selectedIndex]; - var lastSelected = localStorage.getItem('default-author'); + const selectOption = author_select.options[author_select.selectedIndex]; + const lastSelected = localStorage.getItem('default-author'); if(lastSelected) { author_select.value = lastSelected; } @@ -92,5 +103,19 @@ lastSelected = author_select.options[author_select.selectedIndex].value; localStorage.setItem('default-author', lastSelected); } + + // If you opt not to use the excerpt, hide the other excerpt-related fields + const excerptFields = document.getElementById('excerpt-fields'); + if (document.querySelector('input[name="use-excerpt"]:checked').value == "no"){ + excerptFields.style.display = 'none'; + } + document.querySelector('.use-excerpt').onchange = function(e) { + if (e.target.value == "no") { + excerptFields.style.display = 'none'; + } else { + excerptFields.style.display = 'block'; + } + } + {% endblock content %} diff --git a/templates/generator/preview.html b/templates/generator/preview.html index fdc5932..dd35a9e 100644 --- a/templates/generator/preview.html +++ b/templates/generator/preview.html @@ -37,6 +37,8 @@

    Post Excerpt

    To move the "read more" button somewhere else, paste <!--more--> wherever you'd like it to appear. + (Note: there's an option to remove the button entirely later on the Download page, as well.) +

    Images

    Upload your image using the LIL blog media upload form, and note the URL it assigns to your image. Use the editor's image button, swapping in the image URL for the placeholder. It should look like this, when you are done:

    From 23bdd6a7491540af55a911e3a82a8363ef68f910 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 6 Dec 2022 14:58:23 -0500 Subject: [PATCH 4/4] This is not markdown. --- templates/generator/preview.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/generator/preview.html b/templates/generator/preview.html index dd35a9e..ccf44fa 100644 --- a/templates/generator/preview.html +++ b/templates/generator/preview.html @@ -31,7 +31,7 @@

    You can enter any text you want into the editor, including HTML (audio/video embed tags...even script tags), and it will be rendered during preview as-is. So if you are tempted to paste in random javascript you find on the web, think twice.

    Post Excerpt

    - You can configure how much of your post will appear on the [blog's index page](https://lil.law.harvard.edu/blog). + You can configure how much of your post will appear on the blog's index page. The default is to show a "read more" link after the first paragraph.