This commit is contained in:
Jérémie 2018-10-04 18:18:06 +02:00
commit 7958b92141
13 changed files with 250 additions and 171 deletions

View File

@ -4,6 +4,7 @@ python:
- 3.6
install:
- virtualenv -p python .env
- make install
script:

View File

@ -2,11 +2,12 @@ VENV = $(PWD)/.env
PIP = $(VENV)/bin/pip
PYTHON = $(VENV)/bin/python
FLASK = $(VENV)/bin/flask
AFPY_SERVER = afpy_web
all: install serve
install:
test -d $(VENV) || virtualenv -p python $(VENV)
test -d $(VENV) || python3 -m venv $(VENV)
$(PIP) install --upgrade --no-cache pip setuptools -e .[test]
clean:
@ -22,3 +23,7 @@ test:
serve:
env FLASK_APP=afpy.py FLASK_ENV=development $(FLASK) run
afpy:
ssh -t $(AFPY_SERVER) 'cd site && git pull'
ssh -t $(AFPY_SERVER) 'killall uwsgi-3.6 && /usr/local/etc/rc.d/uwsgi restart'

View File

@ -13,3 +13,9 @@ d'abord.
Si vous avez votre propre venv, un `FLASK_APP=afpy.py
FLASK_ENV=development flask run` vous suffira.
## Déployer
Pour publier lancez `make afpy`.
Votre clé ssh publique doit être installée sur le serveur pour pouvoir déployer, et configurer la connexion ssh avec les informations fournies par les admins.

50
afpy.py
View File

@ -58,12 +58,11 @@ def index():
for post in data.get_posts(data.POST_ACTUALITIES, end=4):
timestamp = post[data.TIMESTAMP]
posts[timestamp] = post
if (post[data.DIR] / 'post.jpg').is_file():
posts[timestamp]['image'] = '/'.join(
(data.POST_ACTUALITIES, data.STATE_PUBLISHED, timestamp)
)
return render_template(
'index.html', body_id='index', name=data.POST_ACTUALITIES, posts=posts
'index.html',
body_id='index',
name=data.POST_ACTUALITIES,
posts=posts
)
@ -89,7 +88,10 @@ def rest(name):
except FileNotFoundError:
abort(404)
return render_template(
'rst.html', body_id=name, html=parts['body'], title=parts['title']
'rst.html',
body_id=name,
html=parts['body'],
title=parts['title']
)
@ -127,7 +129,11 @@ def edit_post_admin(name, timestamp):
if not post:
abort(404)
return render_template(
'edit_post.html', body_id='edit-post', post=post, name=name, admin=True
'edit_post.html',
body_id='edit-post',
post=post,
name=name,
admin=True
)
@ -152,7 +158,10 @@ def save_post(name, token=None):
edit_post_url = url_for(
'edit_post', name=name, token=signer.dumps(post['_timestamp'])
)
return render_template('confirmation.html', edit_post_url=edit_post_url)
return render_template(
'confirmation.html',
edit_post_url=edit_post_url
)
@app.route('/admin/post/edit/<name>/<timestamp>', methods=['post'])
@ -184,10 +193,6 @@ def posts(name, page=1):
):
timestamp = post[data.TIMESTAMP]
posts[timestamp] = post
if (timestamp / 'post.jpg').is_file():
posts[timestamp]['image'] = '/'.join(
(name, data.STATE_PUBLISHED, timestamp)
)
return render_template(
'posts.html',
body_id=name,
@ -225,21 +230,24 @@ def post(name, timestamp):
post = data.get_post(name, timestamp, data.STATE_PUBLISHED)
if not post:
abort(404)
if (post[data.DIR] / 'post.jpg').is_file():
post['image'] = '/'.join((name, data.STATE_PUBLISHED, timestamp))
return render_template('post.html', body_id='post', post=post, name=name)
return render_template(
'post.html',
body_id='post',
post=post,
name=name
)
@app.route('/post_image/<path:path>/post.jpg')
@app.route('/post_image/<path:path>')
def post_image(path):
if path.count('/') != 2:
if path.count('/') != 3:
abort(404)
name, status, timestamp = path.split('/')
if name not in data.POSTS:
category, state, timestamp, name = path.split('/')
if category not in data.POSTS:
abort(404)
if status not in data.STATES:
if state not in data.STATES:
abort(404)
return send_from_directory(data.root / path, 'post.jpg')
return send_from_directory(data.root, path)
@app.route('/feed/<name>/rss.xml')

View File

@ -10,7 +10,8 @@ POSTS = {POST_ACTUALITIES: "Actualités", POST_JOBS: "Offres demploi"}
STATE_WAITING = 'waiting'
STATE_PUBLISHED = 'published'
STATE_TRASHED = 'trash'
STATE_TRASHED = 'trashed'
STATES = {
STATE_WAITING: "En attente",
STATE_PUBLISHED: "Publié",
@ -28,6 +29,7 @@ ACTIONS = {
ACTION_TRASH: "Supprimer",
}
IMAGE = '_image'
TIMESTAMP = '_timestamp'
STATE = '_state'
PATH = '_path'
@ -35,6 +37,7 @@ DIR = '_dir'
BASE_DIR = 'posts'
BASE_FILE = 'post.xml'
BASE_IMAGE = 'post.jpg'
class DataException(Exception):
@ -87,7 +90,12 @@ def get_post(category, timestamp, states=None):
return None
tree = ElementTree.parse(path)
post = {item.tag: (item.text or '').strip() for item in tree.iter()}
post[TIMESTAMP] = int(timestamp)
# Calculated fields
image = post.get('image') or BASE_IMAGE
if (dir / image).is_file():
post[IMAGE] = '/'.join((category, state, timestamp, image))
post[TIMESTAMP] = timestamp
post[STATE] = state
post[DIR] = dir
post[PATH] = path
@ -140,6 +148,5 @@ def save_post(category, timestamp, admin, form):
(root / category / STATE_TRASHED / timestamp).rename(
root / category / STATE_PUBLISHED / timestamp
)
return get_post(category, timestamp)

View File

@ -1,11 +0,0 @@
Flask<1.0.1
Flask-Cache
libsass
docutils
feedparser
python-dateutil
itsdangerous
pytest
pytest-isort
pytest-flake8
pytest-coverage

View File

@ -7,10 +7,8 @@ $text: #eaeaea
a
color: $action-secondary
font-size: .8em
font-weight: 700
text-decoration: none
text-transform: uppercase
transition: color 250ms
&:hover
@ -82,13 +80,6 @@ iframe
height: 55em
width: 100%
img
display: block
float: left
margin: 0.5em 1em 1em 0
max-height: 10em
max-width: 80%
body
background: $bkg
color: $text
@ -107,38 +98,91 @@ header
order: 1
padding: 0 1em
nav
align-items: center
display: flex
justify-content: center
min-height: 70px
order: 0
.wrapper
width: 100%
max-width: 1200px
margin: 0 auto
box-sizing: border-box
ul
.menu
display: flex
flex-direction: row
justify-content: flex-start
align-items: center
padding: 0 1em
min-height: 70px
// Footer navigation
&--footer
background: $header
box-sizing: border-box
margin-top: 2em
order: 4
&__toggle
order: 2
text-align: right
align-self: flex-start
padding-top: 1em
@media screen and (min-width: 840px)
display: none
&__checkbox
display: none
&:checked + .menu__list
max-height: 84px
overflow: hidden
// ul element
&__list
flex: 1 1 100%
display: flex
flex-wrap: wrap
flex-direction: column
list-style: none
margin: 0
padding: 0 calc(4em + 50px)
padding: 0
max-height: 1000px
transition: max-height .3s
li
&:first-child
a::before
content: url(../images/logo.svg)
left: 3em
position: absolute
top: 10px
@media screen and (min-width: 840px)
flex-direction: row
justify-content: center
align-items: center
a
color: $text
display: block
font-weight: 600
padding: 1em
text-decoration: none
.active a
color: $action-secondary
&__item
padding: 1em 0
font-size: .8em
text-transform: uppercase
white-space: nowrap
@media screen and (min-width: 840px)
padding: 1em
&--brand
flex: 1 1 100%
padding-left: 0
.brand
flex: 1 1 100%
display: flex
flex-direction: row
align-items: center
font-size: 1.5em
img
padding-right: 1em
a
font-weight: normal
text-transform: none
padding: 0
main
box-sizing: border-box
flex-grow: 1
@ -155,23 +199,12 @@ aside
width: 80%
footer
background: $header
box-sizing: border-box
margin-top: 2em
order: 4
padding: 0 1em
ul
display: flex
justify-content: center
list-style: none
padding: 0
a
color: inherit
text-decoration: inherit
padding: 1em
h1
color: $action-secondary
font-weight: 300
@ -212,6 +245,10 @@ time
box-sizing: border-box
flex: 1 50%
padding: 2em
word-wrap: break-word
img
max-width: 100%
a
color: $action-secondary
@ -242,3 +279,4 @@ time
#actualites main, #emplois main, #index-news
article
flex: 1 100%
width: 100%

View File

@ -2,10 +2,8 @@
@import url("https://fonts.googleapis.com/css?family=Hind:300,600,700");
a {
color: #ffcd05;
font-size: .8em;
font-weight: 700;
text-decoration: none;
text-transform: uppercase;
transition: color 250ms; }
a:hover {
color: #ffd738; }
@ -69,13 +67,6 @@ iframe {
height: 55em;
width: 100%; }
img {
display: block;
float: left;
margin: 0.5em 1em 1em 0;
max-height: 10em;
max-width: 80%; }
body {
background: #25252d;
color: #eaeaea;
@ -94,31 +85,81 @@ header {
order: 1;
padding: 0 1em; }
nav {
align-items: center;
.wrapper {
width: 100%;
max-width: 1200px;
margin: 0 auto;
box-sizing: border-box; }
.menu {
display: flex;
justify-content: center;
min-height: 70px;
order: 0; }
nav ul {
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0 1em;
min-height: 70px; }
.menu--footer {
background: #1d1e23;
box-sizing: border-box;
margin-top: 2em;
order: 4; }
.menu__toggle {
order: 2;
text-align: right;
align-self: flex-start;
padding-top: 1em; }
@media screen and (min-width: 840px) {
.menu__toggle {
display: none; } }
.menu__checkbox {
display: none; }
.menu__checkbox:checked + .menu__list {
max-height: 84px;
overflow: hidden; }
.menu__list {
flex: 1 1 100%;
display: flex;
flex-wrap: wrap;
flex-direction: column;
list-style: none;
margin: 0;
padding: 0 calc(4em + 50px); }
nav ul li:first-child a::before {
content: url(../images/logo.svg);
left: 3em;
position: absolute;
top: 10px; }
nav ul a {
padding: 0;
max-height: 1000px;
transition: max-height .3s; }
@media screen and (min-width: 840px) {
.menu__list {
flex-direction: row;
justify-content: center;
align-items: center; } }
.menu__list a {
color: #eaeaea;
display: block;
font-weight: 600;
padding: 1em;
text-decoration: none; }
nav ul .active a {
.menu__list .active a {
color: #ffcd05; }
.menu__item {
padding: 1em 0;
font-size: .8em;
text-transform: uppercase;
white-space: nowrap; }
@media screen and (min-width: 840px) {
.menu__item {
padding: 1em; } }
.menu__item--brand {
flex: 1 1 100%;
padding-left: 0; }
.menu__item .brand {
flex: 1 1 100%;
display: flex;
flex-direction: row;
align-items: center;
font-size: 1.5em; }
.menu__item .brand img {
padding-right: 1em; }
.menu__item .brand a {
font-weight: normal;
text-transform: none;
padding: 0; }
main {
box-sizing: border-box;
@ -135,21 +176,11 @@ aside {
padding: 1em 2em;
width: 80%; }
footer {
background: #1d1e23;
box-sizing: border-box;
margin-top: 2em;
order: 4;
padding: 0 1em; }
footer ul {
display: flex;
justify-content: center;
list-style: none;
padding: 0; }
footer ul a {
color: inherit;
text-decoration: inherit;
padding: 1em; }
footer ul {
display: flex;
justify-content: center;
list-style: none;
padding: 0; }
h1 {
color: #ffcd05;
@ -186,7 +217,10 @@ time {
border: 1px solid #25252d;
box-sizing: border-box;
flex: 1 50%;
padding: 2em; }
padding: 2em;
word-wrap: break-word; }
#actualites main article img, #emplois main article img, #index-news article img {
max-width: 100%; }
#actualites main article a, #emplois main article a, #index-news article a {
color: #ffcd05;
font-size: .8em;
@ -210,6 +244,7 @@ time {
@media screen and (max-width: 640px) {
#actualites main article, #emplois main article, #index-news article {
flex: 1 100%; } }
flex: 1 100%;
width: 100%; } }
/*# sourceMappingURL=../static/css/style.sass.css.map */

View File

@ -5,63 +5,57 @@
<link rel="icon" href="{{ url_for('static', filename='images/favicon.ico') }}" />
<title>AFPY - Le site web de l'Association Francophone de Python</title>
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/style.sass.css') }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
{% block script %}{% endblock script %}
</head>
<body id="{{ body_id }}">
<header>
{% block header %}{% endblock header %}
<div class="wrapper">
{% block header %}{% endblock header %}
</div>
</header>
<nav>
<ul>
<li{% if body_id == 'index' %} class="active"{% endif %}>
<a href="{{ url_for('index') }}">Accueil</a>
</li>
<li{% if body_id == 'a-propos' %} class="active"{% endif %}>
<a href="{{ url_for('rest', name='a-propos') }}">Qui sommes-nous ?</a>
</li>
<li{% if body_id == 'actualites' %} class="active"{% endif %}>
<a href="{{ url_for('posts', name='actualites') }}">Actualités</a>
</li>
<li{% if body_id == 'emplois' %} class="active"{% endif %}>
<a href="{{ url_for('posts', name='emplois') }}">Offres d'emplois</a>
</li>
<li{% if body_id == 'communaute' %} class="active"{% endif %}>
<a href="{{ url_for('pages', name='communaute') }}">Communauté</a>
</li>
<li{% if body_id == 'discussion' %} class="active"{% endif %}>
<a href="{{ url_for('pages', name='discussion') }}">Discussion</a>
</li>
<li{% if body_id == 'adhesions' %} class="active"{% endif %}>
<a href="{{ url_for('pages', name='adhesions') }}">Adhésions</a>
<nav class="wrapper menu menu--main">
{% set navigation_bar = [
(url_for('index'), 'index', 'Accueil'),
(url_for('rest', name='a-propos'), 'a-propos', 'Qui sommes-nous ?'),
(url_for('posts', name='actualites'), 'actualites', 'Actualités'),
(url_for('posts', name='emplois'), 'emplois', 'Offres d\'emplois'),
(url_for('pages', name='communaute'), 'communaute', 'Communauté'),
(url_for('pages', name='discussion'), 'discussion', 'Discussion'),
(url_for('pages', name='adhesions'), 'adhesions', 'Adhésions')
] -%}
<label for="toggle" class="menu__toggle">Menu</label>
<input class="menu__checkbox" type="checkbox" name="toggle" id="toggle" checked>
<ul class="menu__list">
<li class="menu__item menu__item--brand">
<a class="brand" href="{{ url_for('index') }}">
<img src="{{ url_for('static', filename='images/logo.svg') }}" title="Logo afpy" />
<span>AFPy</span>
</a>
</li>
{% for href, id, caption in navigation_bar %}
<li class="menu__item{% if id == body_id %} active{% endif
%}"><a href="{{ href|e }}">{{ caption|e }}</a></li>
{% endfor %}
</ul>
</nav>
<main>
{% block main %}{% endblock main %}
</main>
<footer>
<ul>
<li>
<a href="{{ url_for('rest', name='contact') }}">Contact</a>
</li>
<li>
<a href="{{ url_for('rest', name='charte') }}">Charte</a>
</li>
<li>
<a href="{{ url_for('rest', name='legal') }}">Mentions légales</a>
</li>
<li>
<a href="{{ url_for('rest', name='rss') }}">Flux RSS</a>
</li>
<li>
<a href="https://twitter.com/asso_python_fr">Twitter</a>
</li>
<li>
<a href="{{ url_for('admin', name='actualites') }}">Admin actualités</a>
</li>
<li>
<a href="{{ url_for('admin', name='emplois') }}">Admin offres d'emploi</a>
</li>
<footer class="wrapper menu menu--footer">
{% set navigation_bar = [
(url_for('rest', name='contact'), 'Contact'),
(url_for('rest', name='charte'), 'Charte'),
(url_for('rest', name='legal'), 'Mentions légales'),
(url_for('rest', name='rss'), 'Flux RSS'),
('https://twitter.com/asso_python_fr', 'Twitter'),
(url_for('admin', name='actualites'), 'Admin actualités'),
(url_for('admin', name='emplois'), 'Admin offres d\'emploi')
] -%}
<ul class="menu__list">
{% for href, caption in navigation_bar %}
<li class="menu__item"><a href="{{ href|e }}">{{ caption|e }}</a></li>
{% endfor %}
</ul>
</footer>
</body>

View File

@ -52,7 +52,6 @@
</label>
<input type="submit" value="Enregistrer" />
{% if admin %}
{% if post._state == 'waiting' %}
<input type="submit" name="publish" value="Publier" />
{% elif post._state == 'published' %}
@ -60,13 +59,10 @@
{% else %}
<input type="submit" name="republish" value="Republier" />
{% endif %}
{% endif %}
{% if post._state == 'published' %}
<input type="submit" name="trashed" value="Supprimer" />
{% endif %}
</form>
{% if name == 'actualites' %}
<p>

View File

@ -27,8 +27,8 @@
<time pubdate datetime="{{ post.published }}">
{{ post.published | parse_iso_datetime('%x') }}
</time>
{% if post.image %}
<img src="{{ url_for('post_image', path=post.image) }}" alt="{{ post.title }}" />
{% if post._image %}
<img src="{{ url_for('post_image', path=post._image) }}" alt="{{ post.title }}" />
{% endif %}
{{ post.summary | safe }}
<p><a href="{{ url_for('post', name=name, timestamp=timestamp) }}">Lire la suite…</a></p>

View File

@ -14,8 +14,8 @@
{{ post.summary | safe if post.summary }}
</em>
</p>
{% if post.image %}
<img src="{{ url_for('post_image', path=post.image) }}" alt="{{ post.title }}" />
{% if post._image %}
<img src="{{ url_for('post_image', path=post._image) }}" alt="{{ post.title }}" />
{% endif %}
{{ post.content | safe }}
</article>

View File

@ -15,8 +15,8 @@
<time pubdate datetime="{{ post.published }}">
{{ post.published | parse_iso_datetime('%x') }}
</time>
{% if post.image %}
<img src="{{ url_for('post_image', path=post.image) }}" alt="{{ post.title }}" />
{% if post._image %}
<img src="{{ url_for('post_image', path=post._image) }}" alt="{{ post.title }}" />
{% endif %}
{{ post.summary | safe if post.summary }}
<p><a href="{{ url_for('post', name=name, timestamp=timestamp) }}">Lire la suite…</a></p>