Commit b14ce108 authored by buttle's avatar buttle

Added flask blueprints.

Split views and templates into folders
Created utils module
Refactored lots of code.
No database changes
parent 4f1950d4
## v.42 2020/03/23
* Added flask blueprints.
* Split views and templates into folders
* Created utils module
* Refactored lots of code.
* No database changes
......@@ -32,7 +32,7 @@ babel = Babel(app)
csrf = CSRFProtect()
csrf.init_app(app)
app.config['APP_VERSION'] = 41
app.config['APP_VERSION'] = 42
app.config['SCHEMA_VERSION'] = 13
app.config['RESERVED_SLUGS'] = ['login', 'static', 'admin', 'admins', 'user', 'users',
......@@ -51,7 +51,21 @@ app.config['FAVICON_FOLDER'] = "%s/static/images/favicon/" % os.path.dirname(os.
sys.path.append(os.path.dirname(os.path.realpath(__file__)) + "/form_templates")
from GNGforms import views
from GNGforms.views.main import main_bp
from GNGforms.views.user import user_bp
from GNGforms.views.form import form_bp
from GNGforms.views.site import site_bp
from GNGforms.views.admin import admin_bp
from GNGforms.views.entries import entries_bp
app.register_blueprint(main_bp)
app.register_blueprint(user_bp)
app.register_blueprint(form_bp)
app.register_blueprint(site_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(entries_bp)
if __name__ == '__main__':
app.run()
"""
“Copyright 2019 La Coordinadora d’Entitats per la Lleialtat Santsenca”
This file is part of GNGforms.
GNGforms is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from flask import g, flash, request
from flask_babel import gettext
from GNGforms import app
from GNGforms.persistence import Site
import smtplib, ssl, socket
from threading import Thread
def createSmtpObj():
config=g.site.smtpConfig
try:
if config["encryption"] == "SSL":
server = smtplib.SMTP_SSL(config["host"], port=config["port"], timeout=2)
server.login(config["user"], config["password"])
elif config["encryption"] == "STARTTLS":
server = smtplib.SMTP_SSL(config["host"], port=config["port"], timeout=2)
context = ssl.create_default_context()
server.starttls(context=context)
server.login(config["user"], config["password"])
else:
server = smtplib.SMTP(config["host"], port=config["port"])
if config["user"] and config["password"]:
server.login(config["user"], config["password"])
return server
except socket.error as e:
if g.isAdmin:
flash(str(e), 'error')
return False
def sendMail(email, message):
server = createSmtpObj()
if server:
try:
header='To: ' + email + '\n' + 'From: ' + g.site.smtpConfig["noreplyAddress"] + '\n'
message=header + message
server.sendmail(g.site.smtpConfig["noreplyAddress"], email, message.encode('utf-8'))
return True
except Exception as e:
if g.isAdmin:
flash(str(e) , 'error')
return False
def sendConfirmEmail(user, newEmail=None):
link="%suser/validate-email/%s" % (g.site.host_url, user.token['token'])
message=gettext("Hello %s\n\nPlease confirm your email\n\n%s") % (user.username, link)
message = 'Subject: {}\n\n{}'.format(gettext("GNGforms. Confirm email"), message)
if newEmail:
return sendMail(newEmail, message)
else:
return sendMail(user.email, message)
def sendInvite(invite):
site=Site.find(hostname=invite.hostname)
link="%suser/new/%s" % (site.host_url, invite.token['token'])
message="%s\n\n%s" % (invite.message, link)
message='Subject: {}\n\n{}'.format(gettext("GNGforms. Invitation to %s" % site.hostname), message)
return sendMail(invite.email, message)
def sendRecoverPassword(user):
link="%ssite/recover-password/%s" % (g.site.host_url, user.token['token'])
message=gettext("Please use this link to recover your password")
message="%s\n\n%s" % (message, link)
message='Subject: {}\n\n{}'.format(gettext("GNGforms. Recover password"), message)
return sendMail(user.email, message)
def sendNewFormEntryNotification(emails, entry, slug):
message=gettext("New form entry in %s at %s\n" % (slug, g.site.hostname))
for data in entry:
message="%s\n%s: %s" % (message, data[0], data[1])
message="%s\n" % message
message='Subject: {}\n\n{}'.format(gettext("GNGforms. New form entry"), message)
for email in emails:
sendMail(email, message)
def sendExpiredFormNotification(editorEmails, form):
message=gettext("The form '%s' has expired at %s" % (form.slug, g.site.hostname))
message='Subject: {}\n\n{}'.format(gettext("GNGforms. A form has expired"), message)
for email in editorEmails:
sendMail(email, message)
def sendNewFormNotification(adminEmails, form):
message=gettext("New form '%s' created at %s" % (form.slug, g.site.hostname))
message='Subject: {}\n\n{}'.format(gettext("GNGforms. New form notification"), message)
for email in adminEmails:
sendMail(email, message)
def sendNewUserNotification(adminEmails, username):
message=gettext("New user '%s' created at %s" % (username, g.site.hostname))
message='Subject: {}\n\n{}'.format(gettext("GNGforms. New user notification"), message)
for email in adminEmails:
sendMail(email, message)
def sendTestEmail(email):
message=gettext("Congratulations!")
message='Subject: {}\n\n{}'.format(gettext("SMTP test"), message)
return sendMail(email, message)
"""
“Copyright 2019 La Coordinadora d’Entitats per la Lleialtat Santsenca”
This file is part of GNGforms.
GNGforms is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from GNGforms.persistence import *
#from pprint import pprint as pp
"""
Mirgations previous to schemaVersion 13 used the flask_pymongo library.
schemaVersion >= 13 uses flask_mongoengine.
"""
def migrateMongoSchema(schemaVersion):
#if schemaVersion == 13:
# ....
# schemaVersion = 14
return schemaVersion
......@@ -18,25 +18,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from GNGforms import app, db
from GNGforms.utils import *
from GNGforms.migrate import migrateMongoSchema
from GNGforms.utils.utils import *
from GNGforms.utils.migrate import migrateMongoSchema
from flask import flash, request, g
from flask_babel import gettext
from urllib.parse import urlparse
import os, string, random, datetime, json, markdown
from mongoengine import QuerySet
from pprint import pformat
#from pprint import pprint as pp
#from pprint import pprint as pp
def get_obj_values_as_dict(obj):
values = {}
fields = type(obj).__dict__['_fields']
for key, _ in fields.items():
value = getattr(obj, key, None)
values[key] = value
return values
class HostnameQuerySet(QuerySet):
def ensure_hostname(self, **kwargs):
......@@ -125,10 +117,10 @@ class User(db.Document):
if self.blocked:
return False
return True
@property
def forms(self):
return Form.findAll(editor=str(self.id))
return Form.findAll(editor_id=str(self.id))
@property
def authored_forms(self):
......@@ -147,7 +139,7 @@ class User(db.Document):
forms = Form.findAll(author_id=str(self.id))
for form in forms:
form.delete()
forms = Form.findAll(editor=str(self.id))
forms = Form.findAll(editor_id=str(self.id))
for form in forms:
del form.editors[str(self.id)]
form.save()
......@@ -242,9 +234,9 @@ class Form(db.Document):
@classmethod
def findAll(cls, **kwargs):
if 'editor' in kwargs:
kwargs={"__raw__": {'editors.%s' % kwargs["editor"]: {'$exists': True}}, **kwargs}
kwargs.pop('editor')
if 'editor_id' in kwargs:
kwargs={"__raw__": {'editors.%s' % kwargs["editor_id"]: {'$exists': True}}, **kwargs}
kwargs.pop('editor_id')
if 'key' in kwargs:
kwargs={"sharedEntries__key": kwargs['key'], **kwargs}
kwargs.pop('key')
......
"""
“Copyright 2019 La Coordinadora d’Entitats per la Lleialtat Santsenca”
This file is part of GNGforms.
GNGforms is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from flask import session
import json
def ensureSessionFormKeys():
if not 'slug' in session:
session['slug'] = ""
if not 'formFieldIndex' in session:
session['formFieldIndex'] = []
if not 'formStructure' in session:
session['formStructure'] = json.dumps([])
if not 'afterSubmitTextMD' in session:
session['afterSubmitTextMD'] = ''
def populateSessionFormData(form):
#session['form_id'] = str(form._id)
session['slug'] = form.slug
session['formFieldIndex'] = form.fieldIndex
session['formStructure'] = form.structure
session['afterSubmitTextMD'] = form.afterSubmitText['markdown']
def clearSessionFormData():
session['slug'] = ""
session['form_id']=None
session['formFieldIndex'] = []
session['formStructure'] = json.dumps([])
session['afterSubmitTextMD'] = ''
......@@ -29,7 +29,7 @@
</thead>
{% for form in user.authored_forms %}
<tr>
<td>{{form.slug}}</td>
<td><a href="/forms/view/{{form.id}}">{{form.slug}}</a></td>
<td>{{form.editors|length}}</td>
<td>{{form.entries|length}}</td>
</tr>
......@@ -51,7 +51,7 @@
{% else %}
<input class="btn-danger btn" type="submit" value="{%trans%}Delete user{%endtrans%}">
{% endif %}
<input class="btn-primary btn" type="button" value="{%trans%}Cancel{%endtrans%}" onClick="location.href='/admin/users/id/{{user.id}}'">
<input class="btn-primary btn" type="button" value="{%trans%}Cancel{%endtrans%}" onClick="location.href='/admin/users/{{user.id}}'">
</form>
</div>
......
......@@ -29,7 +29,7 @@
<tr>
<td><a href="/forms/view/{{form.id}}">{{form.slug}}</a></td>
<td>{{form.created}}</td>
<td><a href="/admin/users/id/{{form.user.id}}">{{form.user.username}}</a></td>
<td><a href="/admin/users/{{form.user.id}}">{{form.user.username}}</a></td>
<td>{{form.isPublic()}}</td>
<td>{{form.entries|length}}</td>
{% if g.isRootUser %}
......
......@@ -36,7 +36,7 @@
<a href="/forms">{%trans%}My forms{%endtrans%}</a> |
{% endif %}
<a href="/user/{{ g.current_user.username }}">{%trans%}Settings{%endtrans%}</a> |
<a href="/site/logout">{%trans%}Logout{%endtrans%}</a>
<a href="/user/logout">{%trans%}Logout{%endtrans%}</a>
</div>
</div>
{% endif %}
......
......@@ -17,7 +17,7 @@
</div>
{%trans%}You are going to delete this form and{%endtrans%}
<span class="highlightedText">{{form.totalEntries}} {%trans%}entries{%endtrans%}</span>.
<span class="highlightedText">{{form.totalEntries}} {%trans%}entries{%endtrans%}</span>
<br />
{%trans%}Write the name of the form below{%endtrans%}.
<p></p>
......
......@@ -13,27 +13,9 @@
</div>
<div class="col-md-2"></div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-9">
<div class="title_1">{%trans%}Share results{%endtrans%}</div>
<p>{%trans%}Share a readonly link with interested parties{%endtrans%}</p>
<div>
<span id="toggle_sharedEntries" class="btn-group btn-toggle">
<button id="sharedEntries_true" class="btn btn-xs btn-default {% if form.areEntriesShared() %}btn-success{% endif %}">{%trans%}Enabled{%endtrans%}</button>
<button id="sharedEntries_false" class="btn btn-xs btn-default {% if not form.areEntriesShared() %}btn-primary{% endif %}">{%trans%}Disabled{%endtrans%}</button>
</span>
<a id="enabled_link" href="{{ form.getSharedEntriesURL() }}" {%if not form.areEntriesShared()%}style="display:None"{%endif%}>{{ form.getSharedEntriesURL() }}</a>
<span id="disabled_link" style="text-decoration:line-through; {%if form.areEntriesShared()%}display:None{%endif%}" >{{ form.getSharedEntriesURL() }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-5">
<p>&nbsp;</p>
<div class="title_1">{%trans%}Editors{%endtrans%}</div>
<p>{%trans%}Users listed here have the <b>same permissions</b> as you{%endtrans%}</p>
......@@ -71,6 +53,24 @@
</div>
<div class="col-md-4"></div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-9">
<div class="title_1">{%trans%}Share results{%endtrans%}</div>
<p>{%trans%}Share a readonly link with interested parties{%endtrans%}</p>
<div>
<span id="toggle_sharedEntries" class="btn-group btn-toggle">
<button id="sharedEntries_true" class="btn btn-xs btn-default {% if form.areEntriesShared() %}btn-success{% endif %}">{%trans%}Enabled{%endtrans%}</button>
<button id="sharedEntries_false" class="btn btn-xs btn-default {% if not form.areEntriesShared() %}btn-primary{% endif %}">{%trans%}Disabled{%endtrans%}</button>
</span>
<a id="enabled_link" href="{{ form.getSharedEntriesURL() }}" {%if not form.areEntriesShared()%}style="display:None"{%endif%}>{{ form.getSharedEntriesURL() }}</a>
<span id="disabled_link" style="text-decoration:line-through; {%if form.areEntriesShared()%}display:None{%endif%}" >{{ form.getSharedEntriesURL() }}</span>
</div>
</div>
</div>
<p>&nbsp;</p>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-9">
......
......@@ -38,7 +38,7 @@
<div class="row col-md-1"></div>
<div class="row col-md-3">
<form action="/site/login" method="POST" style="margin-top:1.5em">
<form action="/user/login" method="POST" style="margin-top:1.5em">
{{ wtform.csrf_token }}
<p>
{{ wtform.username.label }}<br />
......@@ -53,7 +53,7 @@
<br />
{% if g.current_user %}
<a href="/site/reset-password">{%trans%}Forgot your password?{%endtrans%}</a>
<a href="/user/reset-password">{%trans%}Forgot your password?{%endtrans%}</a>
{% else %}
<a href="/site/recover-password">{%trans%}Forgot your password?{%endtrans%}</a>
{% endif %}
......
<h1>OK</h1>
{% for site in sites %}
{{ site.hostname }}
{% endfor %}
"""
“Copyright 2019 La Coordinadora d’Entitats per la Lleialtat Santsenca”
This file is part of GNGforms.
GNGforms is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from GNGforms import app, db
from flask import g, flash, redirect, render_template, url_for
from flask_babel import gettext
from unidecode import unidecode
import time, re, string, random, datetime, csv
from passlib.hash import pbkdf2_sha256
from password_strength import PasswordPolicy
import markdown, html.parser
from functools import wraps
""" ######## View wrappers ######## """
def login_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if g.current_user:
return f(*args, **kwargs)
else:
return redirect(url_for('index'))
return wrap
def enabled_user_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if g.current_user and g.current_user.enabled:
return f(*args, **kwargs)
else:
return redirect(url_for('index'))
return wrap
def admin_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if g.isAdmin:
return f(*args, **kwargs)
else:
return redirect(url_for('index'))
return wrap
def rootuser_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if g.isRootUser:
return f(*args, **kwargs)
else:
return redirect(url_for('index'))
return wrap
def anon_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if g.current_user:
return redirect(url_for('index'))
else:
return f(*args, **kwargs)
return wrap
def sanitized_slug_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if not 'slug' in kwargs:
if g.current_user:
flash("No slug found!", 'error')
return render_template('page-not-found.html'), 404
if kwargs['slug'] in app.config['RESERVED_SLUGS']:
if g.current_user:
flash("Reserved slug!", 'warning')
return render_template('page-not-found.html'), 404
if kwargs['slug'] != sanitizeSlug(kwargs['slug']):
if g.current_user:
flash("That's a nasty slug!", 'warning')
return render_template('page-not-found.html'), 404
return f(*args, **kwargs)
return wrap
def sanitized_key_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if not ('key' in kwargs and kwargs['key'] == sanitizeString(kwargs['key'])):
if g.current_user:
flash(gettext("That's a nasty key!"), 'warning')
return render_template('page-not-found.html'), 404
else:
return f(*args, **kwargs)
return wrap
def sanitized_token(f):
@wraps(f)
def wrap(*args, **kwargs):
if 'token' in kwargs and kwargs['token'] != sanitizeTokenString(kwargs['token']):
if g.current_user:
flash(gettext("That's a nasty token!"), 'warning')
return render_template('page_not_found.html'), 404
else:
return f(*args, **kwargs)
return wrap
""" ######## Sanitizers ######## """
def sanitizeString(string):
string = unidecode(string)
string = string.replace(" ", "")
return re.sub('[^A-Za-z0-9\-]', '', string)
def sanitizeSlug(slug):
slug = slug.lower()
slug = slug.replace(" ", "-")
return sanitizeString(slug)
def sanitizeHexidecimal(string):
return re.sub('[^A-Fa-f0-9]', '', string)
def isSaneSlug(slug):
if slug and slug == sanitizeSlug(slug):
return True
return False
def sanitizeUsername(username):
return sanitizeString(username)
def isSaneUsername(username):
if username and username == sanitizeUsername(username):
return True
return False
def sanitizeTokenString(string):
return re.sub('[^a-z0-9]', '', string)
def stripHTMLTags(text):
h = html.parser.HTMLParser()
text=h.unescape(text)
return re.sub('<[^<]+?>', '', text)
# remember to remove this from the code because now tags are stripped from Labels at view/preview
def stripHTMLTagsForLabel(text):
h = html.parser.HTMLParser()
text=h.unescape(text)
text = text.replace("<br>","-") # formbuilder generates "<br>"s
return re.sub('<[^<]+?>', '', text)
def escapeMarkdown(MDtext):
return re.sub(r'<[^>]*?>', '', MDtext)
def markdown2HTML(MDtext):
MDtext=escapeMarkdown(MDtext)
return markdown.markdown(MDtext, extensions=['nl2br'])
""" ######## Password ######## """
pwd_policy = PasswordPolicy.from_names(
length=8, # min length: 8
uppercase=0, # need min. 2 uppercase letters
numbers=0, # need min. 2 digits
special=0, # need min. 2 special characters
nonletters=1, # need min. 2 non-letter characters (digits, specials, anything)
)
def hashPassword(password):
return pbkdf2_sha256.hash(password, rounds=200000, salt_size=16)
def verifyPassword(password, hash):
return pbkdf2_sha256.verify(password, hash)
""" ######## fieldIndex helpers ######## """
def getFieldByNameInIndex(index, name):
for field in index:
if 'name' in field and field['name'] == name:
return field
return None
""" ######## Tokens ######## """
def getRandomString(length=32):
return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length))
"""
Create a unique token.
persistentClass may be a User class, or an Invite class, ..
"""
def createToken(persistentClass, **kwargs):
tokenString = getRandomString(length=48)
while persistentClass.find(token=tokenString):
tokenString = getRandomString(length=48)
result={'token': tokenString, 'created': datetime.datetime.now()}
return {**result, **kwargs}
def isValidToken(tokenData):
token_age = datetime.datetime.now() - tokenData['created']
if token_age.total_seconds() > app.config['TOKEN_EXPIRATION']:
return False
return True
""" ######## Dates ######## """