Skip to content

Commit

Permalink
Merge pull request harvard-lil#19 from rebeccacremona/snippet
Browse files Browse the repository at this point in the history
Add support for custom post excerpts
  • Loading branch information
bensteinberg authored Dec 7, 2022
2 parents c6810e3 + 23bdd6a commit ddd83f4
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pyc
.env
.flaskenv
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
59 changes: 42 additions & 17 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -32,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 = '<!--more-->'

###
### UTILS ###
###
Expand All @@ -45,22 +48,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):
Expand Down Expand Up @@ -94,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'})


Expand All @@ -108,6 +128,11 @@ def download():
head_matter['author'] = request.form['author']
else:
head_matter['guest-author'] = request.form['author']
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)
Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ gunicorn
requests
pyyaml>=4.2b1
unidecode
python-dotenv
12 changes: 2 additions & 10 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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: "&nbsp;";
}
43 changes: 36 additions & 7 deletions templates/generator/download.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@
<textarea name="content" id="content" class="u-full-width" rows="12"
style="height: auto; background-color: #eee; border: none;" readonly>{{request.form['content']}}</textarea>

<br>
<br>

<fieldset class="use-excerpt">
<legend>Excerpt on blog's index page?</legend>
<label><input type="radio" name="use-excerpt" value="yes" checked="checked">&nbsp;Yes</label>
<label><input type="radio" name="use-excerpt" value="no">&nbsp;No</label>
</fieldset>

<div id="excerpt-fields">
<input type="hidden" name="excerpt-type" value="{{context['excerpt_type']}}">
<label for="excerpt">{{context['excerpt_type']}} Excerpt</label>
<textarea name="excerpt" id="excerpt" class="u-full-width" rows="12" style="height: auto; background-color: #eee; border: none;" readonly>{{context['excerpt']}}</textarea>
</div>

<div style="margin-top: 25px; margin-bottom: 25px; border: 1px solid #eee; padding-top: 25px;">
<ol>
<li>Download your post's markdown file</li>
Expand All @@ -54,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');
Expand All @@ -79,14 +94,28 @@
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;
}
author_select.onchange = function () {
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';
}
}

</script>
{% endblock content %}
10 changes: 10 additions & 0 deletions templates/generator/preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<form method="post">
<input id="title" type="hidden" name="title" value="">
<input id="content" type="hidden" name="content" value="">
<input id="custom-excerpt" type="hidden" name="custom-excerpt" value="">
<button id="lil-export" role="button" class="small-btn">Download</button>
</form>
</div>
Expand All @@ -29,6 +30,15 @@
<div id="lil-instructions">
<p>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.</p>

<h3 id="post-excerpt">Post Excerpt</h3>
You can configure how much of your post will appear on the <a href="https://lil.law.harvard.edu/blog">blog's index page</a>.

The default is to show a "read more" link after the first paragraph.

To move the "read more" button somewhere else, paste <code>&lt;!--more--&gt;</code> wherever you'd like it to appear.

(Note: there's an option to remove the button entirely later on the Download page, as well.)

<h3>Images</h3>
<p>Upload your image using the <a href="https://lil-blog-uploader.herokuapp.com/" target="_blank">LIL blog media upload form</a>, 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:</p>
<code>
Expand Down

0 comments on commit ddd83f4

Please sign in to comment.