Browse Source

v1

master
Astequ 1 year ago
parent
commit
7d26c1bb1e
24 changed files with 708 additions and 0 deletions
  1. +1
    -0
      .flaskenv
  2. +110
    -0
      app/__init__.py
  3. +9
    -0
      app/config.py
  4. +0
    -0
      app/db_actions.py
  5. +15
    -0
      app/locale.py
  6. +11
    -0
      app/models.py
  7. +32
    -0
      app/utils.py
  8. +0
    -0
      clivage.py
  9. +1
    -0
      migrations/README
  10. +45
    -0
      migrations/alembic.ini
  11. +96
    -0
      migrations/env.py
  12. +24
    -0
      migrations/script.py.mako
  13. +34
    -0
      migrations/versions/cff6e9c9dc04_.py
  14. +17
    -0
      requirements.txt
  15. +47
    -0
      static/global.css
  16. +0
    -0
      static/home.css
  17. +61
    -0
      static/results.css
  18. +65
    -0
      static/vote.css
  19. +26
    -0
      templates/common/base.html
  20. +2
    -0
      templates/common/footer.html
  21. +1
    -0
      templates/common/header.html
  22. +24
    -0
      templates/pages/home.html
  23. +49
    -0
      templates/pages/results.html
  24. +38
    -0
      templates/pages/vote.html

+ 1
- 0
.flaskenv View File

@@ -0,0 +1 @@
FLASK_APP=clivage.py

+ 110
- 0
app/__init__.py View File

@@ -0,0 +1,110 @@
from flask import Flask, abort, redirect, url_for, render_template
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func, text

from app.config import Config
from app.locale import Locale
from app.utils import WordResult, UIColor

app = Flask(__name__, template_folder='../templates', static_folder='../static')
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

from app import models
from app.models import Word


@app.route('/')
def home():
return render_template("pages/home.html", color=UIColor.color_random())


@app.route('/vote')
@app.route('/vote/')
def vote_index():
word = db.session.query(Word).order_by(func.random()).first()
return redirect(url_for('vote_from_all', id=word.id, word=word.text))


@app.route('/vote/<string:word>')
@app.route('/vote/<string:word>/')
def vote_from_word(word: str):
db_word = db.session.query(Word).filter_by(text=word).scalar()
if db_word is not None:
return redirect(url_for('vote_from_all', id=db_word.id, word=db_word.text))
else:
abort(404)


@app.route('/vote/<int:id>')
@app.route('/vote/<int:id>/')
def vote_from_id(id: int):
db_word = db.session.query(Word).filter_by(id=id).scalar()
if db_word is not None:
return redirect(url_for('vote_from_all', id=db_word.id, word=db_word.text))
else:
abort(404)


@app.route('/vote/<int:id>/<string:word>')
@app.route('/vote/<int:id>/<string:word>/')
def vote_from_all(id: int, word: str):
db_word = db.session.query(Word).filter_by(id=id, text=word).scalar()
if db_word is not None:
return render_template("pages/vote.html", color=UIColor.color_from(db_word.text), title=db_word.text,
word=db_word,
url_left=url_for('vote_for', id=id, word=word, vote="left"),
url_right=url_for('vote_for', id=id, word=word, vote="right"),
url_index=url_for('vote_index'))
else:
abort(404)


@app.route('/vote/<int:id>/<string:word>/<string:vote>')
@app.route('/vote/<int:id>/<string:word>/<string:vote>/')
def vote_for(vote: str, id: int, word: str):
print(word)
db_word = db.session.query(Word).filter_by(id=id, text=word).scalar()

if db_word is None:
abort(404)

if Locale.is_left(vote):
print("left")
db_word.left = Word.left + 1
elif Locale.is_right(vote):
print("right")
db_word.right = Word.right + 1
else:
abort(400)

db.session.commit()
return redirect(url_for('vote_index'))


@app.route('/results')
@app.route('/results/')
@app.route('/results/<string:sort>')
@app.route('/results/<string:sort>/')
def results_index(sort: str = "id"):
if sort == "left":
def sort_key(x):
return x.left_percentage
elif sort == "right":
def sort_key(x):
return x.right_percentage
else:
def sort_key(x):
return x.word
results = [
WordResult(result.text, result.left, result.right, url_for('vote_from_all', id=result.id, word=result.text)) for
result in
db.session.query(Word).from_statement(text("SELECT * FROM word WHERE word.right <> 0 OR word.left <> 0"))]
results.sort(key=sort_key, reverse=True)
return render_template("pages/results.html", color=UIColor.color_random(), results=results)


if __name__ == '__main__':
app.run()

+ 9
- 0
app/config.py View File

@@ -0,0 +1,9 @@
import os

basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))


class Config(object):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + basedir + '/app.sqlite'
SQLALCHEMY_TRACK_MODIFICATIONS = False

+ 0
- 0
app/db_actions.py View File


+ 15
- 0
app/locale.py View File

@@ -0,0 +1,15 @@
class Locale:
left = ['left', 'gauche', 'goche']
right = ['right', 'droite', 'drouate']

@classmethod
def check(cls, dic: list, word: str):
return any(word in string for string in dic)

@classmethod
def is_left(cls, word: str):
return cls.check(cls.left, word)

@classmethod
def is_right(cls, word: str):
return cls.check(cls.right, word)

+ 11
- 0
app/models.py View File

@@ -0,0 +1,11 @@
from app import db


class Word(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.String(255), unique=True)
left = db.Column(db.Integer, default=0)
right = db.Column(db.Integer, default=0)

def __repr__(self):
return '<Word {} ({}-{})>'.format(self.text, self.right, self.left)

+ 32
- 0
app/utils.py View File

@@ -0,0 +1,32 @@
from random import randint


class WordResult:

def __init__(self, word, left, right, url):
self.word = word
self.left = left
self.right = right
self.left_percentage = round(left / (left + right) * 100, 1)
self.right_percentage = 100 - self.left_percentage
if left > right:
self.major = -1
elif right > left:
self.major = 1
else:
self.major = 0
self.url = url


class UIColor:
colorlist = ("F44336", "E91E63", "9C27B0", "673AB7", "3F51B5", "2196F3", "03A9F4", "00BCD4", "009688", "4CAF50",
"8BC34A", "CDDC39", "FFEB3B", "FFC107", "FF9800", "FF5722", "795548", "607D8B")

@classmethod
def color_from(cls, input_string: str):
return "#" + cls.colorlist[
sum(input_string.encode('utf-8')) % len(cls.colorlist)]

@classmethod
def color_random(cls):
return "#" + cls.colorlist[randint(0, len(cls.colorlist) - 1)]

+ 0
- 0
clivage.py View File


+ 1
- 0
migrations/README View File

@@ -0,0 +1 @@
Generic single-database configuration.

+ 45
- 0
migrations/alembic.ini View File

@@ -0,0 +1,45 @@
# A generic, single database configuration.

[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

+ 96
- 0
migrations/env.py View File

@@ -0,0 +1,96 @@
from __future__ import with_statement

import logging
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config
from sqlalchemy import pool

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app

config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata


# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""

# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')

connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

+ 24
- 0
migrations/script.py.mako View File

@@ -0,0 +1,24 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade():
${upgrades if upgrades else "pass"}


def downgrade():
${downgrades if downgrades else "pass"}

+ 34
- 0
migrations/versions/cff6e9c9dc04_.py View File

@@ -0,0 +1,34 @@
"""empty message

Revision ID: cff6e9c9dc04
Revises:
Create Date: 2019-05-05 20:30:16.807935

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = 'cff6e9c9dc04'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('word',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('text', sa.String(length=255), nullable=True),
sa.Column('left', sa.Integer(), nullable=True),
sa.Column('right', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('text')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('word')
# ### end Alembic commands ###

+ 17
- 0
requirements.txt View File

@@ -0,0 +1,17 @@
alembic==1.0.10
Click==7.0
Flask==1.0.2
Flask-Migrate==2.4.0
Flask-SQLAlchemy==2.4.0
Flask-WTF==0.14.2
itsdangerous==1.1.0
Jinja2==2.10.1
Mako==1.0.9
MarkupSafe==1.1.1
python-dateutil==2.8.0
python-dotenv==0.10.1
python-editor==1.0.4
six==1.12.0
SQLAlchemy==1.3.3
Werkzeug==0.15.2
WTForms==2.2.1

+ 47
- 0
static/global.css View File

@@ -0,0 +1,47 @@
@import url('https://fonts.googleapis.com/css?family=Karla');

html {
font-size: 100%;
}

body {
margin: 0;
font-family: Karla, sans-serif;

}

header.global, footer.global {
height: 3rem;
line-height: 3rem;
background-color: black;
color: #fff;
padding-left: 5%;
padding-right: 5%;
}

header.global a {
text-decoration: none;
color: #fff;
}

h1 {
margin: 0;
}

main {
min-height: calc(100vh - 6rem);
height: 100%;
display: block;
}

footer.global {
width: 90%;
display: inline-flex;
justify-content: space-between;
}

footer.global a {
text-decoration: none;
color: #aaaaaa;
font-weight: bold;
}

+ 0
- 0
static/home.css View File


+ 61
- 0
static/results.css View File

@@ -0,0 +1,61 @@
main {
color: rgba(0, 0, 0, 0.7);
margin: auto;
width: 100%;
max-width: 1200px;
}

table a {
color: #fff;
text-decoration: none;
}

table {
width: 100%;
color: white;
}

td:first-child {
text-transform: capitalize;
}

td > a {
display: block;
width: 100%;
height: 100%;
transition: color 80ms;
}

td > a:hover {
color: #aaaaaa;
}

td, th {
border-style: solid;
border-width: 2px;
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.7);
border-collapse: collapse;
padding: 0.3rem 0.3rem 0.3rem 1rem;
}

td {
transition: background-color 160ms;
}

.center {
text-align: center;
}

.right {
text-align: right;
padding-right: 1rem;
}

.left:hover {
background-color: #79211b;
}

.right:hover {
background-color: #104a79;
}

+ 65
- 0
static/vote.css View File

@@ -0,0 +1,65 @@
section {
color: #fff;
text-align: center;
position: relative;
top: calc(50vh - 3rem);
right: 0;
left: 0;
transform: translateY(-50%);
box-shadow: 10px 10px 20px 0px rgba(0, 0, 0, 0.5);
background-color: rgba(0, 0, 0, 0.7);
border-bottom: 1px solid rgba(0, 0, 0, 0);
padding-top: 20px;
max-height: calc(100vh - 6rem);
}

section.content > h1, section.content > h2 {
margin-left: 20px;
margin-right: 20px;
}

section.content > h1 {
font-size: 2rem;
text-transform: capitalize;
}

section.content > h2 {
font-weight: lighter;
font-style: italic;
}

div.controls {
display: inline-flex;
justify-content: space-evenly;
width: 100%;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}

#skip {
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-left: 1px solid rgba(255, 255, 255, 0.2);
}

div.controls > a {
display: block;
flex-grow: 1;
padding: 5px;
text-decoration: none;
color: white;
font-size: 1.5rem;
font-weight: bold;
transition: flex-grow 80ms, background-color 80ms;
}

div.controls > a:hover {
flex-grow: 1.5;
background-color: rgba(255, 255, 255, 0.2);
}

#left:hover {
background-color: #79211b;
}

#right:hover {
background-color: #104a79;
}

+ 26
- 0
templates/common/base.html View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% if title %}
<title>{{ title }} | Clivage</title>
{% else %}
<title>Clivage</title>
{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='global.css') }}">
{% block link %}{% endblock %}

{% block style %}{% endblock %}
</head>
<body>
<header class="global">
{% include 'common/header.html' %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer class="global">
{% include 'common/footer.html' %}
</footer>
</body>
</html>

+ 2
- 0
templates/common/footer.html View File

@@ -0,0 +1,2 @@
<span id="credits">Réalisé par Astequ</span>
<span id="repo">Sources disponibles sur <a href="https://git.foxgl.ovh/Astequ/clivage">Gitrap</a></span>

+ 1
- 0
templates/common/header.html View File

@@ -0,0 +1 @@
<a href="{{ url_for("home") }}"><h1>Clivage</h1></a>

+ 24
- 0
templates/pages/home.html View File

@@ -0,0 +1,24 @@
{% extends "common/base.html" %}

{% block link %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vote.css') }}">
{% endblock %}

{% block style %}
<style>
body {
background-color: {{ color }};
}
</style>
{% endblock %}

{% block content %}
<section class="content">
<h1>Le fantastique jeu du clivage</h1>
<h2>Déterminer le bord politique de mots ou expressions</h2>
<div class="controls">
<a href="{{ url_for('vote_index') }}">Catégoriser des mots</a>
<a href="{{ url_for('results_index') }}">Consulter le dictionnaire</a>
</div>
</section>
{% endblock %}

+ 49
- 0
templates/pages/results.html View File

@@ -0,0 +1,49 @@
{% extends "common/base.html" %}

{% block link %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='results.css') }}">
{% endblock %}

{% block style %}
<style>
body {
background-color: {{ color }};
}

td, th, table {
border-color: {{ color }};
}
</style>
{% endblock %}

{% block content %}
<h1> Mots ayant reçu des votes </h1>
{% if results %}
<table>
{% if results | length > 0 %}
<tr>
<th>Mot ou expression</th>
<th>Votes "gauche"</th>
<th>Votes "droite"</th>
<th>Bord</th>
</tr>
{% endif %}
{% for result in results %}
<tr>
<td><a href="{{ result.url }}">{{ result.word }}</a></td>
<td>{{ result.left }} <span class="percentage">({{ result.left_percentage }}%)</span></td>
<td>{{ result.right }} <span class="percentage">({{ result.right_percentage }}%)</span></td>
<td class="{% if result.major == -1 %}left{% elif result.major == 1 %}right{% else %}center{% endif %}">
{% if result.major == -1 %}
gauche
{% elif result.major == 1 %}
droite
{% else %}
centre
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

+ 38
- 0
templates/pages/vote.html View File

@@ -0,0 +1,38 @@
{% extends "common/base.html" %}

{% block link %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vote.css') }}">
{% endblock %}

{% block style %}
<style>
body {
background-color: {{ color }};
}
</style>
{% endblock %}

{% block content %}
<section class="content">
<h1>{{ word.text }}</h1>
<h2>
{% if word.text | wordcount == 1 %}
Mot
{% else %}
Expression
{% endif %}
{% if word.left > word.right %}
de gauche à {{ (word.left / (word.left + word.right) * 100) | round(1) }}%
{% elif word.left < word.right %}
de droite à {{ (word.right / (word.left + word.right) * 100) | round(1) }}%
{% else %}
ni de droite ni de gauche. Askip.
{% endif %}
</h2>
<div class="controls">
<a href="{{ url_left }}" id="left">Gauche</a>
<a href="{{ url_index }}" id="skip">Sauter</a>
<a href="{{ url_right }}" id="right">Droite</a>
</div>
</section>
{% endblock %}

Loading…
Cancel
Save