Merge pull request #8 from technikamateur/beta

Beta
This commit is contained in:
Daniel 2019-11-27 16:07:05 +01:00 committed by GitHub
commit e5589b8494
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 429 additions and 87 deletions

View file

@ -5,7 +5,7 @@ from logging.config import dictConfig
from apscheduler.jobstores.base import ConflictingIdError from apscheduler.jobstores.base import ConflictingIdError
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
VERSION = "1.0.1" VERSION = "1.0.2"
baseDir = os.path.abspath(os.path.dirname(__file__)) baseDir = os.path.abspath(os.path.dirname(__file__))
staticDir = os.path.join(baseDir, 'static') staticDir = os.path.join(baseDir, 'static')

View file

@ -95,7 +95,7 @@ def pool_gen():
raid = "unbekannt" raid = "unbekannt"
drives = "" drives = ""
for part in value: for part in value:
drives = drives + str(Drive.query.get(part.drive_id)) + "," drives = drives + str(Drive.query.get(part.drive_id).name) + ","
drives = drives[:-1] drives = drives[:-1]
value = value[0] value = value[0]
missing = absent_drive(drives) missing = absent_drive(drives)

View file

@ -1,32 +1,160 @@
from dirkules.models import Pool
from dirkules.config import staticDir from dirkules.config import staticDir
from dirkules import db, app_version from dirkules import db, app_version, app
from dirkules.samba.models import SambaGlobal from dirkules.samba.models import SambaGlobal, SambaShare, SambaOption
def get_pools():
"""
This function returns suitable pools for a share.
:return: List of tuples (id, label) of pools
"""
pools = Pool.query.all()
choices = [(str(pool.id), pool.label + ": " + pool.drives + " mounted on " + pool.mountpoint) for pool in pools]
return choices
def create_share(name, path, user, dir_mask, create_mask, writeable, btrfs, recycling):
"""
Creates a samba share in db with all needed options for proper generation.
:param name: Share name
:type name: String
:param path: Path to share. might change in later versions.
:type path: String
:param user: User which is allowed to access
:type user: String
:param dir_mask: The UNIX octal mask for directories in share
:type dir_mask: Integer
:param create_mask: The UNIX octal mask for files in share
:type create_mask: Integer
:param writeable: Defines whether share is writeable or not
:type writeable: Boolean
:param btrfs: Defines the btrfs vfs_object
:type btrfs: Boolean
:param recycling: Defines the recycle vfs_object
:type recycling: Boolean
:return: Nothing
:rtype: None
"""
path = Pool.query.get(int(path)).mountpoint + "/{}".format(name)
share = SambaShare(name, path, btrfs=btrfs, recycle=recycling)
user = SambaOption("valid users", user)
if dir_mask is None:
dir_mask = "0700"
if create_mask is None:
create_mask = "0600"
dir_mask = SambaOption("directory mask", dir_mask)
create_mask = SambaOption("create mask", create_mask)
if writeable:
writeable = SambaOption("writeable", "yes")
else:
writeable = SambaOption("writeable", "no")
share.options.extend([user, dir_mask, create_mask, writeable])
db.session.add(share)
db.session.commit()
def set_samba_global(workgroup, name): def set_samba_global(workgroup, name):
"""
Sets the variables for the [global] part in smb.conf
:param workgroup: Workgroup name
:type workgroup: string
:param name: Smbd server name
:type name: string
:return: nothing
:rtype: None
"""
SambaGlobal.query.delete() SambaGlobal.query.delete()
workgroup = SambaGlobal("workgroup", workgroup) workgroup = SambaGlobal("workgroup", workgroup)
name = SambaGlobal("server string", "%h {}".format(name)) name = SambaGlobal("server string", "{}".format(name))
db.session.add(workgroup) db.session.add(workgroup)
db.session.add(name) db.session.add(name)
db.session.commit() db.session.commit()
def generate_smb(): def generate_smb():
"""
Function is used to generate a new smb.conf file
:return: nothing
:rtype: None
"""
if SambaGlobal.query.first() is None: if SambaGlobal.query.first() is None:
app.logger.warning("Samba not configured. Using default fallback.")
workgroup = 'WORKGROUP' workgroup = 'WORKGROUP'
server_string = '%h dirkules' server_string = '%h dirkules'
else: else:
workgroup = SambaGlobal.query.get(1) workgroup = SambaGlobal.query.get(1).value
server_string = SambaGlobal.query.get(2) server_string = "%h " + str(SambaGlobal.query.get(2).value)
f = open("smb.conf.txt", "w") f = open("smb.conf.txt", "w")
samba_global = open(staticDir + "/conf/samba_global.conf", "r") samba_global = open(staticDir + "/conf/samba_global.conf", "r")
f.write("# This file was generated by dirkules v{}".format(app_version)) samba_share = open(staticDir + "/conf/samba_share.conf", "r")
f.write() samba_recycle = open(staticDir + "/conf/samba_recycle.conf", "r")
f.write("# Global Config") f.write("# This file was generated by dirkules v{}\n\n".format(app_version))
f.write("server string = {}".format(server_string)) f.write("server string = {}\n".format(server_string))
f.write("workgroup = {}".format(workgroup)) f.write("workgroup = {}\n".format(workgroup))
f.write(samba_global.read()) f.write(samba_global.read())
f.write("\n\n")
for share in SambaShare.query.all():
if share.enabled:
f.write("[{}]\n".format(share.name))
f.write("path = {}\n".format(share.path))
if share.recycle or share.btrfs:
vfs_obj = ""
if share.recycle:
vfs_obj = vfs_obj + "recycle "
if share.btrfs:
vfs_obj = vfs_obj + "btrfs"
f.write("vfs objects = {}\n".format(vfs_obj))
f.write(samba_share.read())
# reset file pointer
samba_share.seek(0)
f.write("\n")
for entry in share.options:
f.write("{} = {}\n".format(entry.option, entry.value))
if share.recycle:
f.write(samba_recycle.read())
samba_recycle.seek(0)
f.write("\n\n")
f.close() f.close()
samba_global.close()
samba_share.close()
samba_recycle.close()
app.logger.info("Generated new samba config.")
def disable_share(share):
"""
Disables a given share object.
:param share: share object
:return: nothing
"""
try:
share.enabled = False
db.session.commit()
except:
db.session.rollback()
def enable_share(share):
"""
Enables a given share object.
:param share: share object
:return: nothing
"""
try:
share.enabled = True
db.session.commit()
except:
db.session.rollback()
def remove_share(share, remove_data=False):
# TODO: Handling of remove_data. Not implemented because removing of share folder is a future thing.
try:
db.session.delete(share)
db.session.commit()
except:
db.session.rollback()

View file

@ -14,21 +14,29 @@ class SambaGlobal(db.Model):
class SambaShare(db.Model): class SambaShare(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False) name = db.Column(db.String, nullable=False)
writeable = db.Column(db.Boolean) path = db.Column(db.String, nullable=False)
recycling = db.Column(db.Boolean) recycle = db.Column(db.Boolean)
btrfs = db.Column(db.Boolean) btrfs = db.Column(db.Boolean)
options = db.relationship('SambaOptions', order_by="SambaOptions.id", backref="samba_share", lazy="select", enabled = db.Column(db.Boolean)
options = db.relationship('SambaOption', order_by="SambaOption.id", backref="samba_share", lazy="select",
cascade="all, delete-orphan") cascade="all, delete-orphan")
def __init__(self, name, writeable=False, recycling=False, btrfs=False): def __init__(self, name, path, recycle=False, btrfs=False, enabled=True):
self.name = name self.name = name
self.writeable = writeable self.path = path
self.recycling = recycling self.recycle = recycle
self.btrfs = btrfs self.btrfs = btrfs
self.enabled = enabled
class SambaOptions(db.Model): class SambaOption(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
sambashare_id = db.Column(db.Integer, db.ForeignKey('samba_share.id'))
option = db.Column(db.String, nullable=False) option = db.Column(db.String, nullable=False)
value = db.Column(db.String, nullable=False) value = db.Column(db.String, nullable=False)
sambashare_id = db.Column(db.Integer, db.ForeignKey('samba_share.id'), nullable=False)
def __init__(self, option, value):
self.option = option
self.value = value

View file

@ -1,13 +1,17 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Dashboard{% endblock %} {% block title %}Freigabe hinzufügen{% endblock %}
{% block head %} {% block head %}
{{ super() }} {{ super() }}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="ui container"> <div class="ui container">
<t2>Neue Samba Share einrichten</t2>
{% from "_formhelpers.html" import render_field %} {% from "_formhelpers.html" import render_field %}
<form method=post class="ui form error"> {% if form.errors %}
<form method=post class="ui form error">
{% else %}
<form method=post class="ui form">
{% endif %}
{{ form.hidden_tag() }}
<h4 class="ui dividing header">Allgemeine Informationen</h4> <h4 class="ui dividing header">Allgemeine Informationen</h4>
{{ render_field(form.name) }} {{ render_field(form.name) }}
<div class="fields"> <div class="fields">
@ -17,12 +21,12 @@
</div> </div>
<h4 class="ui dividing header">Erweiterte Informationen</h4> <h4 class="ui dividing header">Erweiterte Informationen</h4>
{{ render_field(form.path) }} {{ render_field(form.path) }}
{{ render_field(form.user) }}
<div class="fields"> <div class="fields">
{{ render_field(form.user) }}
{{ render_field(form.create_mask) }} {{ render_field(form.create_mask) }}
{{ render_field(form.dir_mask) }} {{ render_field(form.dir_mask) }}
</div> </div>
<button class="ui button" type="submit"><i class="plus icon"></i> Freigabe hinzufügen</button> {{ render_field(form.submit) }}
</form> </form>
</div> </div>
<script> <script>

View file

@ -16,18 +16,19 @@
<i class="plus icon"></i> Freigabe hinzufügen <i class="plus icon"></i> Freigabe hinzufügen
</div> </div>
</a> </a>
<a href="{{ url_for('.generate') }}"> <a href="{{ url_for('.generate') }}">
<div class="ui primary labeled icon button"> <div class="ui primary labeled icon button">
<i class="magic icon"></i> Generieren <i class="magic icon"></i> Generieren
</div> </div>
</a> </a>
<table class="ui celled fixed table"> <table class="ui celled padded table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Schreibzugriff</th> <th>Pfad</th>
<th>Papierkorb</th> <th class="collapsing">Papierkorb</th>
<th>BtrFS Optimierungen</th> <th class="collapsing">BtrFS Modul</th>
<th>Aktionen</th>
</tr> </tr>
</thead> </thead>
@ -35,9 +36,36 @@
{% for share in shares %} {% for share in shares %}
<tr> <tr>
<td>{{ share.name }}</td> <td>{{ share.name }}</td>
<td>{{ share.writeable }}</td> <td>{{ share.path }}</td>
<td>{{ share.recycling }}</td> {% if share.recycle %}
<td>{{ share.btrfs }}</td> <td class="collapsing"><i class="check icon"></i> Aktiviert</td>
{% else %}
<td class="collapsing"><i class="close icon"></i> Deaktiviert</td>
{% endif %}
{% if share.btrfs %}
<td class="collapsing"><i class="check icon"></i> Aktiviert</td>
{% else %}
<td class="collapsing"><i class="close icon"></i> Deaktiviert</td>
{% endif %}
<td>{% if share.enabled %}
<a href="{{ url_for('.index', disable=share.id) }}">
<div class="ui orange icon basic button">
<i class="unlink icon"></i>
</div>
</a>
{% else %}
<a href="{{ url_for('.index', enable=share.id) }}">
<div class="ui green icon basic button">
<i class="linkify icon"></i>
</div>
</a>
{% endif %}
<a href="{{ url_for('.remove', share=share.id) }}">
<div class="ui red icon basic button">
<i class="trash icon"></i>
</div>
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}Freigabe entfernen{% endblock %}
{% block body %}
<div class="topspacer"></div>
<div class="ui container">
<h2 class="ui center aligned icon header">
<i class="circular trash icon"></i>
<div class="content">
Freigabe entfernen
<div class="sub header">Sie möchten die Samba Freigabe mit dem Namen "{{ name }}" entfernen. Bitte
konkretisieren und bestätigen.
</div>
</div>
</h2>
{% from "_formhelpers.html" import render_field %}
{% if form.errors %}
<form method=post class="ui form error">
{% else %}
<form method=post class="ui form">
{% endif %}
{{ form.hidden_tag() }}
{{ render_field(form.remove_data) }}
{% if show_modal %}
<div class="ui segment">
{{ render_field(form.okay) }}
</div>
<div class="ui error message">
<i class="close icon"></i>
<div class="header">
Verletzung der Sicherheitsprotokolle
</div>
Die Sicherheitsprotokolle verhindern die automatische Ausführung.<br>
Für das Ausführen der gewünschten Aktion, ist eine Überbrückung der Sicherheitsprotokolle nötig.
Bitte bestätigen.
</div>
{% endif %}
{{ render_field(form.submit) }}
</form>
</div>
{% endblock %}

View file

@ -1,38 +1,44 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SelectField, IntegerField, RadioField, validators, SubmitField from wtforms import StringField, BooleanField, SelectField, validators, SubmitField
from dirkules.wtforms_extension import ToggleBooleanField, RequiredIf
class SambaConfigForm(FlaskForm): class SambaConfigForm(FlaskForm):
workgroup = StringField("workgroup", [validators.required(message="Bitte Feld ausfüllen!"), workgroup = StringField("workgroup", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Regexp('^[a-z]+$', message="Bitte nur Kleinbuchstaben eingeben."), validators.Regexp('^[a-z]+$', message="Bitte nur Kleinbuchstaben eingeben."),
validators.Length(max=255, message="Eingabe zu lang")], validators.Length(max=11, message="Maximal 11 Zeichen.")],
render_kw={"placeholder": "Nichts..."}) render_kw={"placeholder": "Nichts..."})
server_string = StringField("server string", [validators.required(message="Bitte Feld ausfüllen!"), server_string = StringField("server string", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Regexp('^[a-z]+$', validators.Regexp('^[a-z]+$',
message="Bitte nur Kleinbuchstaben eingeben."), message="Bitte nur Kleinbuchstaben eingeben."),
validators.Length(max=255, message="Eingabe zu lang")], validators.Length(max=15, message="Maximal 15 Zeichen.")],
render_kw={"placeholder": "Nichts..."}) render_kw={"placeholder": "Nichts..."})
submit = SubmitField("Speichern") submit = SubmitField("Speichern")
class SambaAddForm(FlaskForm): class SambaAddForm(FlaskForm):
name = StringField("Name der Freigabe", [validators.required(message="Bitte Feld ausfüllen!"), name = StringField("Name der Freigabe", [validators.InputRequired(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")], validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "Bilder"}) render_kw={"placeholder": "Bilder"})
writeable = BooleanField("Schreibzugriff") writeable = BooleanField("Schreibzugriff")
recycling = BooleanField("Papierkorb") recycling = BooleanField("Papierkorb")
btrfs = BooleanField("BtrFS Optimierungen (Vorsicht!)") btrfs = BooleanField("BtrFS Optimierungen (Vorsicht!)")
# additional path = SelectField("Pfad", validators=[validators.InputRequired(message="Bitte eine Auswahl treffen!")])
path = SelectField("Pfad", choices=[("Value1", "Label1"), ("Value2", "Label2")]) user = StringField("Berechtigte Nutzer", [validators.InputRequired(message="Bitte Feld ausfüllen!"),
user = StringField("Berechtigte Nutzer", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")], validators.Length(max=255, message="Eingabe zu lang")],
render_kw={"placeholder": "sambadaniel"}) render_kw={"placeholder": "sambadaniel"})
create_mask = IntegerField("Dateimaske", [validators.Optional(), create_mask = StringField("Dateimaske", [validators.Optional(),
validators.NumberRange(min=4, max=4, validators.Regexp('^[0-7]{4}$', message="Dies ist kein gültiger Wert!")],
message="Bitte 4 Zahlen eingeben!")], render_kw={"placeholder": "0600"})
render_kw={"placeholder": "0600"}) dir_mask = StringField("Ordnermaske", [validators.Optional(),
dir_mask = IntegerField("Ordnermaske", [validators.Optional(), validators.Regexp('^[0-7]{4}$', message="Dies ist kein gültiger Wert!")],
validators.NumberRange(min=4, max=4, render_kw={"placeholder": "0700"})
message="Bitte 4 Zahlen eingeben!")], submit = SubmitField("Freigabe hinzufügen")
render_kw={"placeholder": "0700"})
submit = SubmitField("Speichern")
class SambaRemovalForm(FlaskForm):
remove_data = BooleanField("Alle Daten entfernen")
okay = ToggleBooleanField("Sicherheitsprotokolle überbrücken.",
validators=[RequiredIf("remove_data",)])
submit = SubmitField("Freigabe löschen")

View file

@ -1,24 +1,52 @@
from dirkules import db from dirkules import db
from dirkules.config import staticDir from dirkules.config import staticDir
from flask import render_template, url_for, request, redirect from flask import render_template, url_for, request, redirect, flash
from dirkules.samba import bp_samba from dirkules.samba import bp_samba
from dirkules.samba.manager import set_samba_global from dirkules.samba import manager as smb_man
from dirkules.samba.models import SambaShare from dirkules.samba.models import SambaShare, SambaGlobal
from dirkules.samba.validation import SambaConfigForm, SambaAddForm from dirkules.samba.validation import SambaConfigForm, SambaAddForm, SambaRemovalForm
@bp_samba.route('/', methods=['GET']) @bp_samba.route('/', methods=['GET'])
def index(): def index():
e_share = request.args.get('enable')
d_share = request.args.get('disable')
shares = SambaShare.query.order_by(db.asc(db.collate(SambaShare.name, 'NOCASE'))).all() shares = SambaShare.query.order_by(db.asc(db.collate(SambaShare.name, 'NOCASE'))).all()
if not (e_share is not None and d_share is not None):
if e_share is not None:
try:
e_share = int(e_share)
share = SambaShare.query.get_or_404(e_share)
smb_man.enable_share(share)
except ValueError:
flash("ValueError: enable", category="error")
except LookupError:
flash("LookupError: id not valid", category="error")
return redirect(url_for('.index'))
elif d_share is not None:
try:
d_share = int(d_share)
share = SambaShare.query.get_or_404(d_share)
smb_man.disable_share(share)
except ValueError:
flash("ValueError: disable", category="error")
except LookupError:
flash("LookupError: id not valid", category="error")
return redirect(url_for('.index'))
else:
flash("Value Error: enable and disable set", category="error")
return render_template('samba/index.html', shares=shares) return render_template('samba/index.html', shares=shares)
@bp_samba.route('/config', methods=['GET', 'POST']) @bp_samba.route('/config', methods=['GET', 'POST'])
def config(): def config():
form = SambaConfigForm(request.form) form = SambaConfigForm(request.form)
if SambaGlobal.query.first() is not None:
form.workgroup.data = SambaGlobal.query.get(1).value
form.server_string.data = SambaGlobal.query.get(2).value
if request.method == 'POST' and form.validate(): if request.method == 'POST' and form.validate():
set_samba_global(form.workgroup.data, form.server_string.data) smb_man.set_samba_global(form.workgroup.data, form.server_string.data)
return redirect(url_for('.index')) return redirect(url_for('.index'))
file = open(staticDir + "/conf/samba_global.conf", "r") file = open(staticDir + "/conf/samba_global.conf", "r")
conf = list() conf = list()
@ -34,11 +62,42 @@ def config():
@bp_samba.route('/add', methods=['GET', 'POST']) @bp_samba.route('/add', methods=['GET', 'POST'])
def add(): def add():
form = SambaAddForm(request.form) form = SambaAddForm(request.form)
form.path.choices = smb_man.get_pools()
if request.method == 'POST' and form.validate(): if request.method == 'POST' and form.validate():
smb_man.create_share(form.name.data, form.path.data, form.user.data, form.dir_mask.data, form.create_mask.data,
form.writeable.data, form.btrfs.data, form.recycling.data)
return redirect(url_for('.index')) return redirect(url_for('.index'))
return render_template('samba/add.html', form=form) return render_template('samba/add.html', form=form)
@bp_samba.route('/generate') @bp_samba.route('/generate')
def generate(): def generate():
smb_man.generate_smb()
if SambaGlobal.query.first() is None:
flash("Samba wurde nicht konfiguriert. Es wird der default fallback verwendet", category="warn")
flash("Konfiguration erfolgreich generiert", category="positive")
return redirect(url_for('.index')) return redirect(url_for('.index'))
@bp_samba.route('/remove', methods=['GET', 'POST'])
def remove():
share_id = request.args.get('share')
show_modal = False
if share_id is None:
flash("Keine id angegeben. Möglicherweise ist der Verweis veraltet", category="error")
return redirect(url_for('.index'))
else:
try:
form = SambaRemovalForm(request.form)
share_id = int(share_id)
share = SambaShare.query.get_or_404(share_id)
if request.method == 'POST':
if form.validate():
smb_man.remove_share(share, remove_data=bool(form.remove_data.data))
return redirect(url_for('.index'))
else:
show_modal = True
return render_template('samba/remove.html', name=share.name, form=form, show_modal=show_modal)
except ValueError:
flash("ValueError: id is not an int", category="error")
return redirect(url_for('.index'))

View file

@ -1,5 +1,4 @@
socket options = TCP_NODELAY socket options = TCP_NODELAY
security = user
encrypt passwords = yes encrypt passwords = yes
invalid users = root invalid users = root
usershare allow guests = no usershare allow guests = no

View file

@ -0,0 +1,6 @@
recycle:repository = .recycle
recycle:touch_mtime = Yes
recycle:keeptree = Yes
recycle:versions = Yes
recycle:directory_mode = 0700
recycle:exclude = *.TMP | *.tmp | ~$*.doc | ~$*.docx | ~$*.xls | ~$*.xlsx | ~$*.xlsm | ~$*.pptx | ~$*.xlam | .~lock.*.odt# | .~lock.*.odp# | .~lock.*.ods#

View file

@ -0,0 +1,2 @@
browseable = no
guest ok = no

View file

@ -15,7 +15,7 @@
{% endif %} {% endif %}
{{ field(**kwargs)|safe }} {{ field(**kwargs)|safe }}
</div> </div>
{% elif field.type == 'SelectField' or field.type =='QuerySelectField' %} {% elif field.type == 'SelectField' %}
<div class="field"> <div class="field">
{% if field.errors %} {% if field.errors %}
<div class="ui mymindrop selection dropdown error"> <div class="ui mymindrop selection dropdown error">
@ -26,17 +26,9 @@
<i class="dropdown icon"></i> <i class="dropdown icon"></i>
<div class="default text">{{ field.label.text }}</div> <div class="default text">{{ field.label.text }}</div>
<div class="menu"> <div class="menu">
{% if field.type == 'SelectField' %} {% for value, label in field.choices %}
{% for value, label in field.choices %} <div class="item" data-value="{{ value }}">{{ label }}</div>
<div class="item" data-value="{{ value }}">{{ label }}</div> {% endfor %}
{% endfor %}
{% else %}
{% for drive in query %}
<div class="item" data-value="{{ drive.name }}">{{ drive.name }}: {{ drive.model }}
({{ drive.size|filesizeformat(true) }})
</div>
{% endfor %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -46,13 +38,7 @@
{% else %} {% else %}
<div class="field"> <div class="field">
{% endif %} {% endif %}
{% if field.errors %}
{% for error in field.errors %}
<div class="ui pointing below red basic label">
{{ error }}
</div>
{% endfor %}
{% endif %}
{% if field.type == 'BooleanField' %} {% if field.type == 'BooleanField' %}
<div class="ui checkbox"> <div class="ui checkbox">
{% else %} {% else %}
@ -110,6 +96,6 @@
</div> </div>
</div> </div>
{% else %} {% else %}
ERROR This field is not defined in formhelpers.
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

View file

@ -28,8 +28,9 @@
<a class="ui item" href="{{ url_for('about') }}">über Dirkules</a> <a class="ui item" href="{{ url_for('about') }}">über Dirkules</a>
</div> </div>
</nav> </nav>
{% with messages = get_flashed_messages() %}
{% if messages %} {% with errors = get_flashed_messages(category_filter=["error"]) %}
{% if errors %}
<div class="tablebox"> <div class="tablebox">
<div class="ui negative message"> <div class="ui negative message">
<i class="close icon"></i> <i class="close icon"></i>
@ -37,16 +38,72 @@
Ein oder Mehrere Fehler sind aufgetreten Ein oder Mehrere Fehler sind aufgetreten
</div> </div>
<ul class="list"> <ul class="list">
{% for message in messages %} {% for msg in errors %}
<li>{{ message }}</li> <li>{{ msg }}</li>
{% endfor %} {% endfor -%}
</ul> </ul>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with warns = get_flashed_messages(category_filter=["warn"]) %}
{% if warns %}
<div class="tablebox">
<div class="ui warning message">
<i class="close icon"></i>
<div class="header">
Ein oder Mehrere Warnungen sind aufgetreten
</div>
<ul class="list">
{% for msg in warns %}
<li>{{ msg }}</li>
{% endfor -%}
</ul>
</div>
</div>
{% endif %}
{% endwith %}
{% with info = get_flashed_messages(category_filter=["info"]) %}
{% if info %}
<div class="tablebox">
<div class="ui message">
<i class="close icon"></i>
<div class="header">
Folgende Informationen sind vorhanden
</div>
<ul class="list">
{% for msg in info %}
<li>{{ msg }}</li>
{% endfor -%}
</ul>
</div>
</div>
{% endif %}
{% endwith %}
{% with pos = get_flashed_messages(category_filter=["positive"]) %}
{% if pos %}
<div class="tablebox">
<div class="ui positive message">
<i class="close icon"></i>
<div class="header">
Folgendes wurde erfolgreich abgeschlossen
</div>
<ul class="list">
{% for msg in pos %}
<li>{{ msg }}</li>
{% endfor -%}
</ul>
</div>
</div>
{% endif %}
{% endwith %}
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</body>
<script> <script>
$('.message .close') $('.message .close')
.on('click', function () { .on('click', function () {
@ -57,6 +114,4 @@
}) })
; ;
</script> </script>
</body>
</html> </html>

View file

@ -1,6 +1,7 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SelectField, IntegerField, RadioField, validators, SubmitField from wtforms import StringField, BooleanField, SelectField, IntegerField, RadioField, validators, SubmitField
from dirkules.models import Drive from dirkules.models import Drive
from dirkules.wtforms_extension import ToggleBooleanField
class CleaningForm(FlaskForm): class CleaningForm(FlaskForm):
@ -27,10 +28,6 @@ class SemanticMultiSelectField(SelectField):
raise ValueError(self.gettext('There are no elements available but this field is required.')) raise ValueError(self.gettext('There are no elements available but this field is required.'))
class ToggleBooleanField(BooleanField):
pass
class PoolAddForm(FlaskForm): class PoolAddForm(FlaskForm):
name = StringField("Name", [validators.required(message="Bitte Feld ausfüllen!"), name = StringField("Name", [validators.required(message="Bitte Feld ausfüllen!"),
validators.Length(max=255, message="Eingabe zu lang")], validators.Length(max=255, message="Eingabe zu lang")],

View file

@ -37,7 +37,7 @@ def drives():
abort(500, description="Expected int, but got {}.".format(delete)) abort(500, description="Expected int, but got {}.".format(delete))
except LookupError: except LookupError:
abort(500, description="Invalid drive id {}".format(delete)) abort(500, description="Invalid drive id {}".format(delete))
return redirect(url_for('drives'))
return render_template('drives.html', drives=Drive.query.all()) return render_template('drives.html', drives=Drive.query.all())

View file

@ -0,0 +1,23 @@
from wtforms import BooleanField
from wtforms.validators import InputRequired, Optional
class ToggleBooleanField(BooleanField):
pass
class RequiredIf(InputRequired):
field_flags = ('requiredif',)
def __init__(self, other_field_name, message=None, *args, **kwargs):
self.other_field_name = other_field_name
self.message = message
def __call__(self, form, field):
other_field = form[self.other_field_name]
if other_field is None:
raise Exception('no field named "%s" in form' % self.other_field_name)
if bool(other_field.data):
super(RequiredIf, self).__call__(form, field)
else:
Optional().__call__(form, field)