Merge pull request #43 from DanielGrams/issue/42-settings

Move static legal information to editable settings #42
This commit is contained in:
Daniel Grams 2020-12-12 16:16:45 +01:00 committed by GitHub
commit d437d41e5e
18 changed files with 834 additions and 556 deletions

View File

@ -1,4 +1,4 @@
[ignore: env/**]
[python: app/**.py]
[jinja2: app/templates/**.html]
[python: project/**.py]
[jinja2: project/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@ -42,17 +42,17 @@ flask db upgrade
### Init
```sh
pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel init -i messages.pot -d app/translations -l de
pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel init -i messages.pot -d project/translations -l de
```
### Extract new msgid's and merge into *.po files
```sh
pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel update -i messages.pot -d app/translations
pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel update -i messages.pot -d project/translations
```
#### Compile after translation is done
```sh
pybabel compile -d app/translations
pybabel compile -d project/translations
```

View File

@ -0,0 +1,43 @@
"""empty message
Revision ID: 31b60d93351d
Revises: 3c5b34fd1156
Create Date: 2020-12-12 13:48:34.244288
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
from project import dbtypes
# revision identifiers, used by Alembic.
revision = "31b60d93351d"
down_revision = "3c5b34fd1156"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"settings",
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("tos", sa.UnicodeText(), nullable=True),
sa.Column("legal_notice", sa.UnicodeText(), nullable=True),
sa.Column("contact", sa.UnicodeText(), nullable=True),
sa.Column("privacy", sa.UnicodeText(), nullable=True),
sa.Column("created_by_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["created_by_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
op.drop_table("settings")
# ### end Alembic commands ###

13
project/forms/admin.py Normal file
View File

@ -0,0 +1,13 @@
from flask_wtf import FlaskForm
from flask_babelex import lazy_gettext
from wtforms import TextAreaField, SubmitField
from wtforms.validators import Optional
class AdminSettingsForm(FlaskForm):
tos = TextAreaField(lazy_gettext("Terms of service"), validators=[Optional()])
legal_notice = TextAreaField(lazy_gettext("Legal notice"), validators=[Optional()])
contact = TextAreaField(lazy_gettext("Contact"), validators=[Optional()])
privacy = TextAreaField(lazy_gettext("Privacy"), validators=[Optional()])
submit = SubmitField(lazy_gettext("Save"))

View File

@ -40,6 +40,18 @@ class TrackableMixin(object):
return relationship("User")
# Global
class Settings(db.Model, TrackableMixin):
__tablename__ = "settings"
id = Column(Integer(), primary_key=True)
tos = Column(UnicodeText())
legal_notice = Column(UnicodeText())
contact = Column(UnicodeText())
privacy = Column(UnicodeText())
# Multi purpose

11
project/services/admin.py Normal file
View File

@ -0,0 +1,11 @@
from project import db
from project.models import Settings
def upsert_settings():
result = Settings.query.first()
if result is None:
result = Settings()
db.session.add(result)
return result

View File

@ -11,6 +11,10 @@
</nav>
<div class="list-group">
<a href="{{ url_for('admin_settings') }}" class="list-group-item">
{{ _('Settings') }}
<i class="fa fa-caret-right"></i>
</a>
<a href="{{ url_for('admin_admin_units') }}" class="list-group-item">
{{ _('Admin Units') }}
<i class="fa fa-caret-right"></i>

View File

@ -0,0 +1,21 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field, render_field_with_errors %}
{% block title %}
{{ _('Settings') }}
{% endblock %}
{% block content %}
<h1>{{ _('Settings') }}</h1>
<form action="" method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.tos) }}
{{ render_field_with_errors(form.legal_notice) }}
{{ render_field_with_errors(form.contact) }}
{{ render_field_with_errors(form.privacy) }}
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "layout.html" %}
{% block title %}
Impressum &amp; Kontakt
{% endblock %}
{% block content %}
<div class="p-3">
<p class="h3">Impressum &amp; Kontakt Entwickler</p>
<p>
Daniel Grams<br />
Schreiberstrasse 2<br />
38640 Goslar<br />
<a href="mailto:moin@danielgrams.de">moin@danielgrams.de</a><br />
<a href="https://www.danielgrams.de">https://www.danielgrams.de</a><br />
Link zum Datenschutz: <a href="{{ url_for('datenschutz') }}">Datenschutz</a>
</p>
</div>
{% endblock %}

View File

@ -62,7 +62,7 @@
}
</script>
<title>{% block title %}{{ title|default }}{% endblock title %}</title>
<title>{% block title %}{{ title|default('oveda') }}{% endblock title %}</title>
{% block header %}
{% endblock %}
@ -136,15 +136,19 @@
<div class="col h-100 text-center my-auto">
<ul class="list-inline mb-2">
<li class="list-inline-item">
<a href="{{ url_for('impressum') }}" class="text-muted">Impressum</a>
<a href="{{ url_for('tos') }}" class="text-muted">{{ _('Terms of service') }}</a>
</li>
<li class="list-inline-item">&sdot;</li>
<li class="list-inline-item">
<a href="{{ url_for('impressum') }}" class="text-muted">Kontakt</a>
<a href="{{ url_for('legal_notice') }}" class="text-muted">{{ _('Legal notice') }}</a>
</li>
<li class="list-inline-item">&sdot;</li>
<li class="list-inline-item">
<a href="{{ url_for('datenschutz') }}" class="text-muted">Datenschutz</a>
<a href="{{ url_for('contact') }}" class="text-muted">{{ _('Contact') }}</a>
</li>
<li class="list-inline-item">&sdot;</li>
<li class="list-inline-item">
<a href="{{ url_for('privacy') }}" class="text-muted">{{ _('Privacy') }}</a>
</li>
</ul>
<p class="text-muted small mb-4 mb-lg-0">Mit <i class="fa fa-heart"></i> in Goslar entwickelt.</p>

View File

@ -1,14 +1,7 @@
{% extends "layout.html" %}
{% block title %}
Datenschutz
{{ title }}
{% endblock %}
{% block content %}
<div class="p-3">
<h1>Datenschutzerklärung</h1>
</div>
{{ content }}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,15 @@
from project import app
from project import app, db
from project.models import AdminUnit
from flask import render_template
from flask import render_template, flash, url_for, redirect
from flask_babelex import gettext
from flask_security import roles_required
from project.forms.admin import AdminSettingsForm
from project.services.admin import upsert_settings
from project.views.utils import (
flash_errors,
handleSqlError,
)
from sqlalchemy.exc import SQLAlchemyError
@app.route("/admin")
@ -14,3 +22,25 @@ def admin():
@roles_required("admin")
def admin_admin_units():
return render_template("admin/admin_units.html", admin_units=AdminUnit.query.all())
@app.route("/admin/settings", methods=("GET", "POST"))
@roles_required("admin")
def admin_settings():
settings = upsert_settings()
form = AdminSettingsForm(obj=settings)
if form.validate_on_submit():
form.populate_obj(settings)
try:
db.session.commit()
flash(gettext("Settings successfully updated"), "success")
return redirect(url_for("admin"))
except SQLAlchemyError as e:
db.session.rollback()
flash(handleSqlError(e), "danger")
else:
flash_errors(form)
return render_template("admin/settings.html", form=form)

View File

@ -1,6 +1,9 @@
from project import app
from project.services.admin import upsert_settings
from project.views.utils import track_analytics
from flask import url_for, render_template, request, redirect
from flask_babelex import gettext
from markupsafe import Markup
@app.route("/")
@ -17,14 +20,36 @@ def example():
return render_template("example.html")
@app.route("/impressum")
def impressum():
return render_template("impressum.html")
@app.route("/tos")
def tos():
title = gettext("Terms of service")
settings = upsert_settings()
content = Markup(settings.tos)
return render_template("legal.html", title=title, content=content)
@app.route("/datenschutz")
def datenschutz():
return render_template("datenschutz.html")
@app.route("/legal_notice")
def legal_notice():
title = gettext("Legal notice")
settings = upsert_settings()
content = Markup(settings.legal_notice)
return render_template("legal.html", title=title, content=content)
@app.route("/contact")
def contact():
title = gettext("Contact")
settings = upsert_settings()
content = Markup(settings.contact)
return render_template("legal.html", title=title, content=content)
@app.route("/privacy")
def privacy():
title = gettext("Privacy")
settings = upsert_settings()
content = Markup(settings.privacy)
return render_template("legal.html", title=title, content=content)
@app.route("/developer")

View File

@ -6,10 +6,10 @@ bcrypt==3.2.0
beautifulsoup4==4.9.3
black==20.8b1
blinker==1.4
certifi==2020.11.8
certifi==2020.12.5
cffi==1.14.4
cfgv==3.2.0
chardet==3.0.4
chardet==4.0.0
click==7.1.2
colour==0.1.5
coverage==5.3
@ -38,7 +38,7 @@ GeoAlchemy2==0.8.4
gunicorn==20.0.4
identify==1.5.10
idna==2.10
importlib-metadata==2.1.1
importlib-metadata==3.1.1
iniconfig==1.1.1
itsdangerous==1.1.0
Jinja2==2.11.2
@ -48,14 +48,14 @@ mccabe==0.6.1
mypy-extensions==0.4.3
nodeenv==1.5.0
oauthlib==3.1.0
packaging==20.7
packaging==20.8
passlib==1.7.4
pathspec==0.8.1
Pillow==8.0.1
pluggy==0.13.1
pre-commit==2.9.2
pre-commit==2.9.3
psycopg2-binary==2.8.6
py==1.9.0
py==1.10.0
pycodestyle==2.6.0
pycparser==2.20
pyflakes==2.2.0
@ -74,7 +74,7 @@ requests==2.25.0
requests-oauthlib==1.3.0
rope==0.18.0
six==1.15.0
soupsieve==2.0.1
soupsieve==2.1
speaklater==1.3
SQLAlchemy==1.3.20
SQLAlchemy-Utils==0.36.8
@ -83,7 +83,7 @@ typed-ast==1.4.1
typing-extensions==3.7.4.3
urllib3==1.26.2
URLObject==2.4.3
virtualenv==20.2.1
virtualenv==20.2.2
visitor==0.1.3
Werkzeug==1.0.1
WTForms==2.3.3

View File

@ -1,3 +1,6 @@
import pytest
def test_normal_user(client, seeder, utils):
seeder.create_user()
utils.login()
@ -18,3 +21,40 @@ def test_admin_units(client, seeder, utils, app):
seeder.create_admin_unit(user, "Meine Crew")
response = client.get("/admin/admin_units")
assert b"Meine Crew" in response.data
@pytest.mark.parametrize("db_error", [True, False])
def test_admin_settings(client, seeder, utils, app, mocker, db_error):
user_id, admin_unit_id = seeder.setup_base(True)
url = utils.get_url("admin_settings")
response = utils.get_ok(url)
if db_error:
utils.mock_db_commit(mocker)
response = utils.post_form(
url,
response,
{
"tos": "Meine Nutzungsbedingungen",
"legal_notice": "Mein Impressum",
"contact": "Mein Kontakt",
"privacy": "Mein Datenschutz",
},
)
if db_error:
utils.assert_response_db_error(response)
return
utils.assert_response_redirect(response, "admin")
with app.app_context():
from project.services.admin import upsert_settings
settings = upsert_settings()
assert settings.tos == "Meine Nutzungsbedingungen"
assert settings.legal_notice == "Mein Impressum"
assert settings.contact == "Mein Kontakt"
assert settings.privacy == "Mein Datenschutz"

View File

@ -12,14 +12,56 @@ def test_example(client, seeder, utils):
utils.get_ok(url)
def test_impressum(client, seeder, utils):
url = utils.get_url("impressum")
utils.get_ok(url)
def test_tos(app, db, utils):
with app.app_context():
from project.services.admin import upsert_settings
settings = upsert_settings()
settings.tos = "Meine Nutzungsbedingungen"
db.session.commit()
url = utils.get_url("tos")
response = utils.get_ok(url)
assert b"Meine Nutzungsbedingungen" in response.data
def test_datenschutz(client, seeder, utils):
url = utils.get_url("datenschutz")
utils.get_ok(url)
def test_legal_notice(app, db, utils):
with app.app_context():
from project.services.admin import upsert_settings
settings = upsert_settings()
settings.legal_notice = "Mein Impressum"
db.session.commit()
url = utils.get_url("legal_notice")
response = utils.get_ok(url)
assert b"Mein Impressum" in response.data
def test_contact(app, db, utils):
with app.app_context():
from project.services.admin import upsert_settings
settings = upsert_settings()
settings.contact = "Mein Kontakt"
db.session.commit()
url = utils.get_url("contact")
response = utils.get_ok(url)
assert b"Mein Kontakt" in response.data
def test_privacy(app, db, utils):
with app.app_context():
from project.services.admin import upsert_settings
settings = upsert_settings()
settings.privacy = "Mein Datenschutz"
db.session.commit()
url = utils.get_url("privacy")
response = utils.get_ok(url)
assert b"Mein Datenschutz" in response.data
def test_developer(client, seeder, utils):