~evilham/gngforms

b2542d82cd34bcdba2f18d07e06ba51362db33ac — Evilham 2 years ago a9f3448
[format] Run black on the project.

https://github.com/psf/black

This ensures that there is no friction when collaborating because of different
formatting styles.
M gngforms/__init__.py => gngforms/__init__.py +36 -23
@@ 26,38 26,51 @@ import sys, os


app = Flask(__name__)
app.config.from_pyfile('../config.cfg')
app.config.from_pyfile("../config.cfg")
db = MongoEngine(app)
babel = Babel(app)

app.secret_key = app.config['SECRET_KEY']
app.config['SESSION_TYPE'] = "filesystem"
app.secret_key = app.config["SECRET_KEY"]
app.config["SESSION_TYPE"] = "filesystem"
Session(app)

app.config['WTF_CSRF_TIME_LIMIT']=5400  # 1.5 hours. Time to fill out a form.
app.config["WTF_CSRF_TIME_LIMIT"] = 5400  # 1.5 hours. Time to fill out a form.
csrf = CSRFProtect()
csrf.init_app(app)

app.config['APP_VERSION'] = "1.4.0"
app.config['SCHEMA_VERSION'] = 15

app.config['RESERVED_SLUGS'] = ['login', 'static', 'admin', 'admins', 'user', 'users',
                                'form', 'forms', 'site', 'sites', 'update']
app.config["APP_VERSION"] = "1.4.0"
app.config["SCHEMA_VERSION"] = 15

app.config["RESERVED_SLUGS"] = [
    "login",
    "static",
    "admin",
    "admins",
    "user",
    "users",
    "form",
    "forms",
    "site",
    "sites",
    "update",
]
# DPL = Data Protection Law
app.config['RESERVED_FORM_ELEMENT_NAMES'] = ['created', 'csrf_token', 'DPL']
app.config['RESERVED_USERNAMES'] = ['system', 'admin']

app.config['FORMBUILDER_DISABLED_ATTRS']=['className','toggle','access']
app.config['FORMBUILDER_DISABLE_FIELDS']=['autocomplete','hidden', 'button', 'file']

app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations;form_templates/translations'
#http://www.lingoes.net/en/translator/langcode.htm
app.config['LANGUAGES'] = {
    'en': ('English', 'en-US'),
    'ca': ('Català', 'ca-ES'),
    'es': ('Castellano', 'es-ES')
app.config["RESERVED_FORM_ELEMENT_NAMES"] = ["created", "csrf_token", "DPL"]
app.config["RESERVED_USERNAMES"] = ["system", "admin"]

app.config["FORMBUILDER_DISABLED_ATTRS"] = ["className", "toggle", "access"]
app.config["FORMBUILDER_DISABLE_FIELDS"] = ["autocomplete", "hidden", "button", "file"]

app.config["BABEL_TRANSLATION_DIRECTORIES"] = "translations;form_templates/translations"
# http://www.lingoes.net/en/translator/langcode.htm
app.config["LANGUAGES"] = {
    "en": ("English", "en-US"),
    "ca": ("Català", "ca-ES"),
    "es": ("Castellano", "es-ES"),
}
app.config['FAVICON_FOLDER'] = "%s/static/images/favicon/" % os.path.dirname(os.path.abspath(__file__))
app.config["FAVICON_FOLDER"] = "%s/static/images/favicon/" % os.path.dirname(
    os.path.abspath(__file__)
)

sys.path.append(os.path.dirname(os.path.realpath(__file__)) + "/form_templates")



@@ 77,5 90,5 @@ app.register_blueprint(admin_bp)
app.register_blueprint(entries_bp)


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

M gngforms/form_templates/form_templates.py => gngforms/form_templates/form_templates.py +145 -52
@@ 18,80 18,173 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

from flask_babel import lazy_gettext as _
 


formTemplates = [
    {
        'id': "tpl-1",
        'name': _('One day congress'),
        'description': "Let attendees choose one of two talks running in parallel. They will also select their lunch menu.",
        'structure': [
            {"subtype": "h1", "label": _('One day congress'), "type": "header"},
            {"subtype": "p", "label": "<div>12th July, all day event<br></div><div>Our meeting point.</div><div>46, Big Street.</div><div>Post code.</div>", "type": "paragraph"},
            {"subtype": "p", "label": "<div>We are holding talks all day on the 12th. We have two conference rooms, and are running the talks in parallel.</div><div><br></div><div>&lt;b&gt;09h - 10h Room 1. Presentation.&lt;/b&gt;<br></div>", "type": "paragraph"},
            {"name": "radio-group-1563733517177",
        "id": "tpl-1",
        "name": _("One day congress"),
        "description": "Let attendees choose one of two talks running in parallel. They will also select their lunch menu.",
        "structure": [
            {"subtype": "h1", "label": _("One day congress"), "type": "header"},
            {
                "subtype": "p",
                "label": "<div>12th July, all day event<br></div><div>Our meeting point.</div><div>46, Big Street.</div><div>Post code.</div>",
                "type": "paragraph",
            },
            {
                "subtype": "p",
                "label": "<div>We are holding talks all day on the 12th. We have two conference rooms, and are running the talks in parallel.</div><div><br></div><div>&lt;b&gt;09h - 10h Room 1. Presentation.&lt;/b&gt;<br></div>",
                "type": "paragraph",
            },
            {
                "name": "radio-group-1563733517177",
                "values": [
                    {"label": _("Room 1. Municipal Strategies"), "value": "strategies"},
                    {"label": _("Room 2. Decentralized tech. Freedom of speech"), "value": "feedom-speech"}
                    {
                        "label": _("Room 2. Decentralized tech. Freedom of speech"),
                        "value": "feedom-speech",
                    },
                ],
                "label": "10h - 12h", "type": "radio-group"},
            {"name": "radio-group-1563733735531",
                "label": "10h - 12h",
                "type": "radio-group",
            },
            {
                "name": "radio-group-1563733735531",
                "values": [
                    {"label": "Room 1. AI descrimination", "value": "ai-descrimination"},
                    {"label": "Room 2. Build an anonymous web", "value": "anonymous-web"}
                    {
                        "label": "Room 1. AI descrimination",
                        "value": "ai-descrimination",
                    },
                    {
                        "label": "Room 2. Build an anonymous web",
                        "value": "anonymous-web",
                    },
                ],
                "label": "12h - 14h", "type": "radio-group"},
            {"name": "radio-group-1563733855827",
                "label": "12h - 14h",
                "type": "radio-group",
            },
            {
                "name": "radio-group-1563733855827",
                "values": [
                    {"label": _("Vegan"), "value": "vegan"},
                    {"label": _("Vegaterian"), "value": "vegaterian"},
                    {"label": _("I'm eating else where"), "value": "not-eating"}
                    {"label": _("I'm eating else where"), "value": "not-eating"},
                ],
                "label": "14h - 15h Lunch", "type": "radio-group"}
        ]
                "label": "14h - 15h Lunch",
                "type": "radio-group",
            },
        ],
    },
    {
        'id': "tpl-2",
        'name': _("Summer courses"),
        'description': _("Students can enroll in a variety of activities spread out across three days."),
        'structure': [
        "id": "tpl-2",
        "name": _("Summer courses"),
        "description": _(
            "Students can enroll in a variety of activities spread out across three days."
        ),
        "structure": [
            {"label": _("Summer courses"), "subtype": "h1", "type": "header"},
            {"label": "<div><br></div><div>Please enroll here for this year's Summer courses</div><div><br></div>", "subtype": "p", "type": "paragraph"},
            {"values":
                [
                    {"label": "10h - 13h. Neutral networks. A practical presentation of our WIFI installation", "value": "eXO-guifi-net"},
                    {"label": "18h - 20h. GIT for beginners Session 1", "value": "git-session-1"}
            {
                "label": "<div><br></div><div>Please enroll here for this year's Summer courses</div><div><br></div>",
                "subtype": "p",
                "type": "paragraph",
            },
            {
                "values": [
                    {
                        "label": "10h - 13h. Neutral networks. A practical presentation of our WIFI installation",
                        "value": "eXO-guifi-net",
                    },
                    {
                        "label": "18h - 20h. GIT for beginners Session 1",
                        "value": "git-session-1",
                    },
                ],
                "label": _("Tuesday 25th"), "type": "checkbox-group", "name": "checkbox-group-1563572627073"
                "label": _("Tuesday 25th"),
                "type": "checkbox-group",
                "name": "checkbox-group-1563572627073",
            },
            {"values":
                [
                    {"label": "10h - 13h. Presentation/demo. TPV to manage the cafe", "value": "tpv-cafe"},
                    {"label": "16h - 19h. Social currencies. Local economy", "value": "local-economy"}
            {
                "values": [
                    {
                        "label": "10h - 13h. Presentation/demo. TPV to manage the cafe",
                        "value": "tpv-cafe",
                    },
                    {
                        "label": "16h - 19h. Social currencies. Local economy",
                        "value": "local-economy",
                    },
                ],
                "label": "Wednesday 26th", "type": "checkbox-group", "name": "checkbox-group-1563697624123"
                "label": "Wednesday 26th",
                "type": "checkbox-group",
                "name": "checkbox-group-1563697624123",
            },
            {"values":
                [
                    {"label": "10h - 13h. Computer Lab management with Free software", "value": "fog-project"},
                    {"label": "18h - 20h. GIT for beginners Session 2", "value": "git-session-2"}
            {
                "values": [
                    {
                        "label": "10h - 13h. Computer Lab management with Free software",
                        "value": "fog-project",
                    },
                    {
                        "label": "18h - 20h. GIT for beginners Session 2",
                        "value": "git-session-2",
                    },
                ],
                "label": "Thursday 27th", "type": "checkbox-group", "name": "checkbox-group-1563698001314"
                "label": "Thursday 27th",
                "type": "checkbox-group",
                "name": "checkbox-group-1563698001314",
            },
            {
                "className": "form-control",
                "name": "text-1563692878245",
                "type": "text",
                "required": "true",
                "label": "Your name",
                "subtype": "text",
            },
            {"className": "form-control", "name": "text-1563692878245", "type": "text", "required": "true", "label": "Your name", "subtype": "text"},
            {"className": "form-control", "name": "text-1563692901766", "type": "text", "subtype": "email", "label": "Your email", "required": "true"}
        ]
            {
                "className": "form-control",
                "name": "text-1563692901766",
                "type": "text",
                "subtype": "email",
                "label": "Your email",
                "required": "true",
            },
        ],
    },
    {
        'id': "tpl-3",
        'name': _("Save our shelter"),
        'description': _("Petition citizen support for your local initiative."),
        'structure': [
        "id": "tpl-3",
        "name": _("Save our shelter"),
        "description": _("Petition citizen support for your local initiative."),
        "structure": [
            {"type": "header", "subtype": "h1", "label": _("Save our shelter")},
            {"type": "paragraph", "subtype": "p", "label": "<div>During the civil war neighbors sought shelter from the aerial bombing. Some time ago the local Church purchased the property and have recently started demolition to build a new parking lot.</div><div><br></div><div>If you want to save our local heritage, please give your support.</div><div><br> </div>"},
            {"className": "form-control", "name": "text-1563737790717", "type": "text", "required": "true", "label": _("ID number"), "subtype": "text"},
            {"type": "text", "className": "form-control", "subtype": "email", "name": "text-1563737806028", "label": _("Email")},
            {"type": "textarea", "subtype": "textarea", "label": _("Comments"), "name": "textarea-1563737836394", "className": "form-control"}
        ]
    }
            {
                "type": "paragraph",
                "subtype": "p",
                "label": "<div>During the civil war neighbors sought shelter from the aerial bombing. Some time ago the local Church purchased the property and have recently started demolition to build a new parking lot.</div><div><br></div><div>If you want to save our local heritage, please give your support.</div><div><br> </div>",
            },
            {
                "className": "form-control",
                "name": "text-1563737790717",
                "type": "text",
                "required": "true",
                "label": _("ID number"),
                "subtype": "text",
            },
            {
                "type": "text",
                "className": "form-control",
                "subtype": "email",
                "name": "text-1563737806028",
                "label": _("Email"),
            },
            {
                "type": "textarea",
                "subtype": "textarea",
                "label": _("Comments"),
                "name": "textarea-1563737836394",
                "className": "form-control",
            },
        ],
    },
]

M gngforms/models.py => gngforms/models.py +265 -240
@@ 18,7 18,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

from flask import flash, request, g
from flask_babel import gettext 
from flask_babel import gettext
from urllib.parse import urlparse
import os, string, random, datetime, json, markdown, csv
from mongoengine import QuerySet


@@ 27,22 27,22 @@ from gngforms import app, db
from gngforms.utils.utils import *
from gngforms.utils.migrate import migrateMongoSchema

#from pprint import pprint as pp
# from pprint import pprint as pp


class HostnameQuerySet(QuerySet):
    def ensure_hostname(self, **kwargs):
        if not g.isRootUser and not 'hostname' in kwargs:
            kwargs={'hostname':g.site.hostname, **kwargs}
        #print("ensure_hostname kwargs: %s" % kwargs)
        if not g.isRootUser and not "hostname" in kwargs:
            kwargs = {"hostname": g.site.hostname, **kwargs}
        # print("ensure_hostname kwargs: %s" % kwargs)
        return self.filter(**kwargs)


class User(db.Document):
    meta = {'collection': 'users', 'queryset_class': HostnameQuerySet}
    meta = {"collection": "users", "queryset_class": HostnameQuerySet}
    username = db.StringField(required=True)
    email = db.StringField(required=True)
    password_hash =db.StringField(db_field="password", required=True)
    password_hash = db.StringField(db_field="password", required=True)
    language = db.StringField(required=True)
    hostname = db.StringField(required=True)
    blocked = db.BooleanField()


@@ 50,16 50,16 @@ class User(db.Document):
    validatedEmail = db.BooleanField()
    created = db.StringField(required=True)
    token = db.DictField(required=False)
    

    def __init__(self, *args, **kwargs):
        db.Document.__init__(self, *args, **kwargs)

    def __str__(self):
        return pformat({'User': get_obj_values_as_dict(self)})
    
        return pformat({"User": get_obj_values_as_dict(self)})

    @classmethod
    def create(cls, newUserData):
        newUser=User(**newUserData)
        newUser = User(**newUserData)
        newUser.save()
        return newUser



@@ 69,11 69,11 @@ class User(db.Document):

    @classmethod
    def findAll(cls, *args, **kwargs):
        if 'token' in kwargs:
            kwargs={"token__token": kwargs['token'], **kwargs}
            kwargs.pop('token')
        if "token" in kwargs:
            kwargs = {"token__token": kwargs["token"], **kwargs}
            kwargs.pop("token")
        return cls.objects.ensure_hostname(**kwargs)
    

    @property
    def enabled(self):
        if not self.validatedEmail:


@@ 91,14 91,14 @@ class User(db.Document):
        return Form.findAll(author_id=str(self.id))

    def isAdmin(self):
        return True if self.admin['isAdmin']==True else False
        return True if self.admin["isAdmin"] == True else False

    def isRootUser(self):
        return True if self.email in app.config['ROOT_USERS'] else False
    
        return True if self.email in app.config["ROOT_USERS"] else False

    def verifyPassword(self, password):
        return verifyPassword(password, self.password_hash)
        

    def deleteUser(self):
        forms = Form.findAll(author_id=str(self.id))
        for form in forms:


@@ 108,64 108,62 @@ class User(db.Document):
            del form.editors[str(self.id)]
            form.save()
        self.delete()
    

    def setToken(self, **kwargs):
        self.token=createToken(User, **kwargs)
        self.token = createToken(User, **kwargs)
        self.save()

    def deleteToken(self):
        self.token={}
        self.token = {}
        self.save()

    def toggleBlocked(self):
        if self.isRootUser():
            self.blocked=False
            self.blocked = False
        else:
            self.blocked=False if self.blocked else True
            self.blocked = False if self.blocked else True
        self.save()
        return self.blocked
            

    def toggleAdmin(self):
        if self.isRootUser():
            return self.isAdmin()
        self.admin['isAdmin']=False if self.isAdmin() else True
        self.admin["isAdmin"] = False if self.isAdmin() else True
        self.save()
        return self.isAdmin()

    @staticmethod
    def defaultAdminSettings():
        return {
            "isAdmin": False,
            "notifyNewUser": False,
            "notifyNewForm": False
        }
        return {"isAdmin": False, "notifyNewUser": False, "notifyNewForm": False}

    """
    send this admin an email when a new user registers at the site
    """

    def toggleNewUserNotification(self):
        if not self.isAdmin():
            return False
        self.admin['notifyNewUser']=False if self.admin['notifyNewUser'] else True
        self.admin["notifyNewUser"] = False if self.admin["notifyNewUser"] else True
        self.save()
        return self.admin['notifyNewUser']
        return self.admin["notifyNewUser"]

    """
    send this admin an email when a new form is created
    """

    def toggleNewFormNotification(self):
        if not self.isAdmin():
            return False
        self.admin['notifyNewForm']=False if self.admin['notifyNewForm'] else True
        self.admin["notifyNewForm"] = False if self.admin["notifyNewForm"] else True
        self.save()
        return self.admin['notifyNewForm']    
        return self.admin["notifyNewForm"]

    def canInspectForm(self, form):
        return True if (str(self.id) in form.editors or self.isAdmin()) else False
    


class Form(db.Document):
    meta = {'collection': 'forms', 'queryset_class': HostnameQuerySet}
    meta = {"collection": "forms", "queryset_class": HostnameQuerySet}
    created = db.StringField(required=True)
    hostname = db.StringField(required=True)
    slug = db.StringField(required=True)


@@ 196,29 194,32 @@ class Form(db.Document):

    def __init__(self, *args, **kwargs):
        db.Document.__init__(self, *args, **kwargs)
        self.site=Site.objects(hostname=self.hostname).first()
   
        self.site = Site.objects(hostname=self.hostname).first()

    def __str__(self):
        return pformat({'Form': get_obj_values_as_dict(self)})
        
        return pformat({"Form": get_obj_values_as_dict(self)})

    @classmethod
    def find(cls, **kwargs):
        return cls.findAll(**kwargs).first()

    @classmethod
    def findAll(cls, **kwargs):
        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')
        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")
        return cls.objects.ensure_hostname(**kwargs)

    @property
    def user(self):
        return self.author
        

    @property
    def author(self):
        return User.find(id=self.author_id)


@@ 230,8 231,8 @@ class Form(db.Document):
            if self.isEditor(self.author):
                del self.editors[self.author_id]
            else:
                return False # this should never happen
            self.author_id=str(new_author.id)
                return False  # this should never happen
            self.author_id = str(new_author.id)
            if not self.isEditor(new_author):
                self.addEditor(new_author)
            self.save()


@@ 244,39 245,39 @@ class Form(db.Document):
        We remove all HTML tags from label before the form is saved,
        but gngform versions before 1.2.1 did not do this.
        """
        result=[]
        result = []
        for field in self.fieldIndex:
            if 'removed' in field and not with_deleted_columns:
            if "removed" in field and not with_deleted_columns:
                continue
            item={'label': stripHTMLTags(field['label']), 'name': field['name']}
            item = {"label": stripHTMLTags(field["label"]), "name": field["name"]}
            result.append(item)
        if self.isDataConsentRequired():
            # insert dynamic DPL field
            result.insert(1, {"name": "DPL", "label": gettext("DPL")})
        return result
    

    def hasRemovedFields(self):
        return any('removed' in field for field in self.fieldIndex)
        return any("removed" in field for field in self.fieldIndex)

    @property
    def totalEntries(self):
        return len(self.entries)

    def isEnabled(self):
        if not (self.author.enabled and self.adminPreferences['public']):
        if not (self.author.enabled and self.adminPreferences["public"]):
            return False
        return self.enabled

    @classmethod
    def newEditorPreferences(cls):
        return {'notification': {'newEntry': False, 'expiredForm': True}}
        return {"notification": {"newEntry": False, "expiredForm": True}}

    def addEditor(self, editor):
        if not editor.enabled:
            return False
        editor_id=str(editor.id)
        editor_id = str(editor.id)
        if not editor_id in self.editors:
            self.editors[editor_id]=Form.newEditorPreferences()
            self.editors[editor_id] = Form.newEditorPreferences()
            self.save()
            return True
        return False


@@ 289,10 290,10 @@ class Form(db.Document):
            self.save()
            return editor_id
        return None
   

    @property
    def url(self):
        return "%s%s" % (self.site.host_url, self.slug)  
        return "%s%s" % (self.site.host_url, self.slug)

    @property
    def embed_url(self):


@@ 306,58 307,65 @@ class Form(db.Document):

    @property
    def dataConsentHTML(self):
        if self.dataConsent['html']:
            return self.dataConsent['html']
        if self.dataConsent["html"]:
            return self.dataConsent["html"]
        if self.site.isPersonalDataConsentEnabled():
            return self.site.personalDataConsent['html']
            return self.site.personalDataConsent["html"]
        return Installation.fallbackDPL()["html"]

    @property
    def dataConsentMarkdown(self):
        if self.dataConsent['markdown']:
            return self.dataConsent['markdown']
        if self.site.isPersonalDataConsentEnabled() and self.site.personalDataConsent['markdown']:
            return self.site.personalDataConsent['markdown']
        if self.dataConsent["markdown"]:
            return self.dataConsent["markdown"]
        if (
            self.site.isPersonalDataConsentEnabled()
            and self.site.personalDataConsent["markdown"]
        ):
            return self.site.personalDataConsent["markdown"]
        return Installation.fallbackDPL()["markdown"]

    def saveDataConsentText(self, MDtext):
        self.dataConsent = {'markdown':escapeMarkdown(MDtext),
                            'html':markdown2HTML(MDtext),
                            'required': self.dataConsent['required']}
        self.dataConsent = {
            "markdown": escapeMarkdown(MDtext),
            "html": markdown2HTML(MDtext),
            "required": self.dataConsent["required"],
        }
        self.save()

    @property
    def lastEntryDate(self):
        if self.entries:
            last_entry = self.entries[-1] 
            last_entry = self.entries[-1]
            return last_entry["created"]
        else:
            return ""

    def getAvailableNumberTypeFields(self):
        result={}
        result = {}
        for element in json.loads(self.structure):
            if "type" in element and element["type"] == "number":
                if element["name"] in self.fieldConditions:
                    result[element["name"]]=self.fieldConditions[element["name"]]
                    result[element["name"]] = self.fieldConditions[element["name"]]
                else:
                    result[element["name"]]={"type":"number", "condition": None}
                    result[element["name"]] = {"type": "number", "condition": None}
        return result

    def getMultiChoiceFields(self):
        result=[]
        result = []
        for element in json.loads(self.structure):
            if "type" in element:
                if  element["type"] == "checkbox-group" or \
                    element["type"] == "radio-group" or \
                    element["type"] == "select":
                if (
                    element["type"] == "checkbox-group"
                    or element["type"] == "radio-group"
                    or element["type"] == "select"
                ):
                    result.append(element)
        return result        
        return result

    def getFieldLabel(self, fieldName):
        for element in json.loads(self.structure):
            if 'name' in element and element['name']==fieldName:
                return element['label']
            if "name" in element and element["name"] == fieldName:
                return element["label"]
        return None

    @property


@@ 365,38 373,38 @@ class Form(db.Document):
        return self.expiryConditions["fields"]

    def getConditionalFieldPositions(self):
        conditionalFieldPositions=[]
        conditionalFieldPositions = []
        for fieldName, condition in self.fieldConditions.items():
            if condition['type'] == 'number':
            if condition["type"] == "number":
                for position, field in enumerate(self.fieldIndex):
                    if field['name'] == fieldName:
                    if field["name"] == fieldName:
                        conditionalFieldPositions.append(position)
                        break
        return conditionalFieldPositions

    @classmethod
    def saveNewForm(cls, formData):
        if formData['slug'] in app.config['RESERVED_SLUGS']:
        if formData["slug"] in app.config["RESERVED_SLUGS"]:
            return None
        new_form=Form(**formData)
        new_form = Form(**formData)
        new_form.save()
        return new_form

    def deleteEntries(self):
        self.entries=[]
        self.entries = []
        self.save()
    

    def isAuthor(self, user):
        return True if self.author_id == user.id else False
        

    def isEditor(self, user):
        return True if str(user.id) in self.editors else False

    def getEditors(self):
        editors=[]
        editors = []
        for editor_id in self.editors:
            #print (editor_id)
            user=User.find(id=editor_id)
            # print (editor_id)
            user = User.find(id=editor_id)
            if user:
                editors.append(user)
            else:


@@ 410,28 418,30 @@ class Form(db.Document):
        if self.expiryConditions["fields"]:
            return True
        return False
    

    def hasExpired(self):
        if not self.willExpire():
            return False
        if self.expiryConditions["expireDate"] and not isFutureDate(self.expiryConditions["expireDate"]):
        if self.expiryConditions["expireDate"] and not isFutureDate(
            self.expiryConditions["expireDate"]
        ):
            return True
        for fieldName, value in self.fieldConditions.items():
            if value['type'] == 'number':
                total=self.tallyNumberField(fieldName)
                if total >= int(value['condition']):
            if value["type"] == "number":
                total = self.tallyNumberField(fieldName)
                if total >= int(value["condition"]):
                    return True
        return False

    def tallyNumberField(self, fieldName):
        total=0
        total = 0
        for entry in self.entries:
            try:
                total = total + int(entry[fieldName])
            except:
                continue
        return total
                

    def isPublic(self):
        if not self.isEnabled() or self.expired:
            return False


@@ 444,113 454,117 @@ class Form(db.Document):
        if len(self.editors) > 1:
            return True
        return False
    

    def areEntriesShared(self):
        return self.sharedEntries['enabled']
    
        return self.sharedEntries["enabled"]

    def getSharedEntriesURL(self, part="results"):
        return "%s/%s/%s" % (self.url, part, self.sharedEntries['key'])
        return "%s/%s/%s" % (self.url, part, self.sharedEntries["key"])

    def getEntries(self):
        result=[]
        result = []
        for saved_entry in self.entries:
            entry={}
            entry = {}
            for field in self.getFieldIndexForDataDisplay():
                value=saved_entry[field['name']] if field['name'] in saved_entry else ""
                entry[field['label']]=value
                value = (
                    saved_entry[field["name"]] if field["name"] in saved_entry else ""
                )
                entry[field["label"]] = value
            result.append(entry)
        return result
        

    def getChartData(self):
        chartable_time_fields=[]
        total={'entries':0}
        time_data={'entries':[]}
        chartable_time_fields = []
        total = {"entries": 0}
        time_data = {"entries": []}
        for field in self.getAvailableNumberTypeFields():
            label=self.getFieldLabel(field)
            total[label]=0
            time_data[label]=[]
            chartable_time_fields.append({'name':field, 'label':label})
            
        multichoice_fields=self.getMultiChoiceFields()
        multi_choice_data={}
            label = self.getFieldLabel(field)
            total[label] = 0
            time_data[label] = []
            chartable_time_fields.append({"name": field, "label": label})

        multichoice_fields = self.getMultiChoiceFields()
        multi_choice_data = {}
        for field in multichoice_fields:
            multi_choice_data[field['label']]={}
            multi_choice_data[field['label']]['axis_1']=[]
            multi_choice_data[field['label']]['axis_2']=[]
            for value in field['values']:
                multi_choice_data[field['label']]['axis_1'].append(value['label'])
                multi_choice_data[field['label']]['axis_2'].append(0)

        for entry in sorted(self.entries, key=lambda k: k['created']):
            #pp(entry)
            total['entries']+=1
            time_data['entries'].append({   'x': entry['created'],
                                            'y': total['entries']})
            multi_choice_data[field["label"]] = {}
            multi_choice_data[field["label"]]["axis_1"] = []
            multi_choice_data[field["label"]]["axis_2"] = []
            for value in field["values"]:
                multi_choice_data[field["label"]]["axis_1"].append(value["label"])
                multi_choice_data[field["label"]]["axis_2"].append(0)

        for entry in sorted(self.entries, key=lambda k: k["created"]):
            # pp(entry)
            total["entries"] += 1
            time_data["entries"].append({"x": entry["created"], "y": total["entries"]})
            for field in chartable_time_fields:
                try:
                    total[field['label']]+=int(entry[field['name']])
                    time_data[field['label']].append({  'x': entry['created'],
                                                        'y': total[field['label']]})
                    total[field["label"]] += int(entry[field["name"]])
                    time_data[field["label"]].append(
                        {"x": entry["created"], "y": total[field["label"]]}
                    )
                except:
                    continue

            for field in multichoice_fields:
                if not (field['name'] in entry and entry[field['name']]):
                if not (field["name"] in entry and entry[field["name"]]):
                    continue
                entry_values=entry[field['name']].split(', ')
                for idx, field_value in enumerate(field['values']):
                    if field_value['value'] in entry_values:
                        multi_choice_data[field['label']]['axis_2'][idx]+=1
        #pp(multi_choice_data)

        result={}
        result['multi_choice']=multi_choice_data
        result['time_chart']=time_data
                entry_values = entry[field["name"]].split(", ")
                for idx, field_value in enumerate(field["values"]):
                    if field_value["value"] in entry_values:
                        multi_choice_data[field["label"]]["axis_2"][idx] += 1
        # pp(multi_choice_data)

        result = {}
        result["multi_choice"] = multi_choice_data
        result["time_chart"] = time_data
        return result

    def toggleEnabled(self):
        if self.expired or self.adminPreferences['public']==False:
        if self.expired or self.adminPreferences["public"] == False:
            return False
        else:
            self.enabled = False if self.enabled else True
            self.save()
            return self.enabled
            

    def toggleAdminFormPublic(self):
        self.adminPreferences['public'] = False if self.adminPreferences['public'] else True
        self.adminPreferences["public"] = (
            False if self.adminPreferences["public"] else True
        )
        self.save()
        return self.adminPreferences['public']
    
        return self.adminPreferences["public"]

    def toggleSharedEntries(self):
        self.sharedEntries['enabled'] = False if self.sharedEntries['enabled'] else True
        self.sharedEntries["enabled"] = False if self.sharedEntries["enabled"] else True
        self.save()
        return self.sharedEntries['enabled']
        return self.sharedEntries["enabled"]

    def toggleRestrictedAccess(self):
        self.restrictedAccess = False if self.restrictedAccess else True
        self.save()
        return self.restrictedAccess
        

    def toggleNotification(self):
        editor_id=str(g.current_user.id)
        editor_id = str(g.current_user.id)
        if editor_id in self.editors:
            if self.editors[editor_id]['notification']['newEntry']:
                self.editors[editor_id]['notification']['newEntry']=False
            if self.editors[editor_id]["notification"]["newEntry"]:
                self.editors[editor_id]["notification"]["newEntry"] = False
            else:
                self.editors[editor_id]['notification']['newEntry']=True
                self.editors[editor_id]["notification"]["newEntry"] = True
            self.save()
            return self.editors[editor_id]['notification']['newEntry']
            return self.editors[editor_id]["notification"]["newEntry"]
        return False

    def toggleExpirationNotification(self):
        editor_id=str(g.current_user.id)
        editor_id = str(g.current_user.id)
        if editor_id in self.editors:
            if self.editors[editor_id]['notification']['expiredForm']:
                self.editors[editor_id]['notification']['expiredForm']=False
            if self.editors[editor_id]["notification"]["expiredForm"]:
                self.editors[editor_id]["notification"]["expiredForm"] = False
            else:
                self.editors[editor_id]['notification']['expiredForm']=True
                self.editors[editor_id]["notification"]["expiredForm"] = True
            self.save()
            return self.editors[editor_id]['notification']['expiredForm']
            return self.editors[editor_id]["notification"]["expiredForm"]
        return False

    def toggleRequireDataConsent(self):


@@ 560,37 574,41 @@ class Form(db.Document):

    def addLog(self, message, anonymous=False):
        if anonymous:
            actor="system"
            actor = "system"
        else:
            actor=g.current_user.username if g.current_user else "system"
        logTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            actor = g.current_user.username if g.current_user else "system"
        logTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.log.insert(0, (logTime, actor, message))
        self.save()

    def writeCSV(self, with_deleted_columns=False):
        fieldnames=[]
        fieldheaders={}
        fieldnames = []
        fieldheaders = {}
        for field in self.getFieldIndexForDataDisplay(with_deleted_columns):
            fieldnames.append(field['name'])
            fieldheaders[field['name']]=field['label']
        csv_name='%s/%s.csv' % (app.config['TMP_DIR'], self.slug)
        with open(csv_name, mode='w') as csv_file:
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames, extrasaction='ignore')
            fieldnames.append(field["name"])
            fieldheaders[field["name"]] = field["label"]
        csv_name = "%s/%s.csv" % (app.config["TMP_DIR"], self.slug)
        with open(csv_name, mode="w") as csv_file:
            writer = csv.DictWriter(
                csv_file, fieldnames=fieldnames, extrasaction="ignore"
            )
            writer.writerow(fieldheaders)
            for entry in self.entries:
                writer.writerow(entry)
        return csv_name
        

    @staticmethod
    def defaultIntroductionText():
        title=gettext("Form title")
        context=gettext("Context")
        content=gettext(" * Describe your form.\n * Add relevant content, links, images, etc.")
        title = gettext("Form title")
        context = gettext("Context")
        content = gettext(
            " * Describe your form.\n * Add relevant content, links, images, etc."
        )
        return "## {}\n\n### {}\n\n{}".format(title, context, content)


class Site(db.Document):
    meta = {'collection': 'sites', 'queryset_class': HostnameQuerySet}
    meta = {"collection": "sites", "queryset_class": HostnameQuerySet}
    hostname = db.StringField(required=True)
    port = db.StringField(required=False)
    siteName = db.StringField(required=True)


@@ 600,41 618,40 @@ class Site(db.Document):
    personalDataConsent = db.DictField(required=False)
    smtpConfig = db.DictField(required=True)

    def __init__(self, *args, **kwargs):        
    def __init__(self, *args, **kwargs):
        db.Document.__init__(self, *args, **kwargs)
    

    def __str__(self):
        return pformat({'Site': get_obj_values_as_dict(self)})
    
        return pformat({"Site": get_obj_values_as_dict(self)})

    @classmethod
    def create(cls):
        hostname=urlparse(request.host_url).hostname
        with open('%s/default_blurb.md' % os.path.dirname(os.path.realpath(__file__)), 'r') as defaultBlurb:
            defaultMD=defaultBlurb.read()
        blurb = {
            'markdown': defaultMD,
            'html': markdown.markdown(defaultMD)
        }
        newSiteData={
        hostname = urlparse(request.host_url).hostname
        with open(
            "%s/default_blurb.md" % os.path.dirname(os.path.realpath(__file__)), "r"
        ) as defaultBlurb:
            defaultMD = defaultBlurb.read()
        blurb = {"markdown": defaultMD, "html": markdown.markdown(defaultMD)}
        newSiteData = {
            "hostname": hostname,
            "port": None,
            "scheme": urlparse(request.host_url).scheme,
            "blurb": blurb,
            "invitationOnly": True,
            "siteName": "gng-forms!",
            "personalDataConsent": {"markdown": "", "html": "", "enabled": False },
            "personalDataConsent": {"markdown": "", "html": "", "enabled": False},
            "smtpConfig": {
                "host": "smtp.%s" % hostname,
                "port": 25,
                "encryption": "",
                "user": "",
                "password": "",
                "noreplyAddress": "no-reply@%s" % hostname
            }
                "noreplyAddress": "no-reply@%s" % hostname,
            },
        }
        new_site=Site(**newSiteData)
        new_site = Site(**newSiteData)
        new_site.save()
        #create the Installation if it doesn't exist
        # create the Installation if it doesn't exist
        Installation.get()
        return new_site



@@ 642,7 659,7 @@ class Site(db.Document):
    def find(cls, *args, **kwargs):
        site = cls.findAll(*args, **kwargs).first()
        if not site:
            site=Site.create()
            site = Site.create()
        return site

    @classmethod


@@ 651,42 668,44 @@ class Site(db.Document):

    @property
    def host_url(self):
        url= "%s://%s" % (self.scheme, self.hostname)
        url = "%s://%s" % (self.scheme, self.hostname)
        if self.port:
            url = "%s:%s" % (url, self.port)
        return url+'/'
        return url + "/"

    def faviconURL(self):
        path="%s%s_favicon.png" % (app.config['FAVICON_FOLDER'], self.hostname)
        path = "%s%s_favicon.png" % (app.config["FAVICON_FOLDER"], self.hostname)
        if os.path.exists(path):
            return "/static/images/favicon/%s_favicon.png" % self.hostname
        else:
            return "/static/images/favicon/default-favicon.png"

    def deleteFavicon(self):
        path="%s%s_favicon.png" % (app.config['FAVICON_FOLDER'], self.hostname)
        path = "%s%s_favicon.png" % (app.config["FAVICON_FOLDER"], self.hostname)
        if os.path.exists(path):
            os.remove(path)
            return True
        return False

    def saveBlurb(self, MDtext):
        self.blurb = {'markdown':escapeMarkdown(MDtext), 'html':markdown2HTML(MDtext)}
        self.blurb = {"markdown": escapeMarkdown(MDtext), "html": markdown2HTML(MDtext)}
        self.save()

    def savePersonalDataConsentText(self, MDtext):
        self.personalDataConsent = {'markdown':escapeMarkdown(MDtext),
                                    'html':markdown2HTML(MDtext),
                                    'enabled': self.personalDataConsent['enabled']}
        self.personalDataConsent = {
            "markdown": escapeMarkdown(MDtext),
            "html": markdown2HTML(MDtext),
            "enabled": self.personalDataConsent["enabled"],
        }
        self.save()

    def saveSMTPconfig(self, **kwargs):
        self.smtpConfig=kwargs
        self.smtpConfig = kwargs
        self.save()

    def isPersonalDataConsentEnabled(self):
        return self.personalDataConsent["enabled"]
                

    @property
    def totalUsers(self):
        return User.findAll(hostname=self.hostname).count()


@@ 694,28 713,30 @@ class Site(db.Document):
    @property
    def admins(self):
        return User.findAll(admin__isAdmin=True, hostname=self.hostname)
    

    @property
    def totalForms(self):
        return Form.findAll(hostname=self.hostname).count()
    

    def toggleInvitationOnly(self):
        self.invitationOnly = False if self.invitationOnly else True
        self.save()
        return self.invitationOnly

    def togglePersonalDataConsentEnabled(self):
        self.personalDataConsent["enabled"] = False if self.personalDataConsent["enabled"] else True
        self.personalDataConsent["enabled"] = (
            False if self.personalDataConsent["enabled"] else True
        )
        self.save()
        return self.personalDataConsent["enabled"]

    def toggleScheme(self):
        self.scheme = 'https' if self.scheme=='http' else 'http'
        self.scheme = "https" if self.scheme == "http" else "http"
        self.save()
        return self.scheme

    def deleteSite(self):
        users=User.findAll(hostname=self.hostname)
        users = User.findAll(hostname=self.hostname)
        for user in users:
            user.deleteUser()
        invites = Invite.findAll(hostname=self.hostname)


@@ 725,94 746,98 @@ class Site(db.Document):


class Invite(db.Document):
    meta = {'collection': 'invites', 'queryset_class': HostnameQuerySet}
    meta = {"collection": "invites", "queryset_class": HostnameQuerySet}
    hostname = db.StringField(required=True)
    email = db.EmailField(required=True)
    message = db.StringField(required=False)
    token = db.DictField(required=True)
    admin = db.BooleanField()
   
    def __init__(self, *args, **kwargs):        

    def __init__(self, *args, **kwargs):
        db.Document.__init__(self, *args, **kwargs)

    def __str__(self):
        return pformat({'Invite': get_obj_values_as_dict(self)})
        return pformat({"Invite": get_obj_values_as_dict(self)})

    @classmethod
    def create(cls, hostname, email, message, admin=False):
        data={
        data = {
            "hostname": hostname,
            "email": email,
            "message": message,
            "token": createToken(Invite),
            "admin": admin
            "admin": admin,
        }
        newInvite=Invite(**data)
        newInvite = Invite(**data)
        newInvite.save()
        return newInvite

    @classmethod
    def find(cls, **kwargs):
        if 'token' in kwargs:
            kwargs={"token__token": kwargs['token'], **kwargs}
            kwargs.pop('token')
        if "token" in kwargs:
            kwargs = {"token__token": kwargs["token"], **kwargs}
            kwargs.pop("token")
        return cls.findAll(**kwargs).first()

    @classmethod
    def findAll(cls, **kwargs):
        return cls.objects.ensure_hostname(**kwargs)
    

    def setToken(self, **kwargs):
        self.invite['token']=createToken(Invite, **kwargs)
        self.invite["token"] = createToken(Invite, **kwargs)
        self.save()
        


class Installation(db.Document):
    name = db.StringField(required=True)
    schemaVersion = db.IntField(required=True)
    created = db.StringField(required=True)
   

    def __init__(self, *args, **kwargs):
        db.Document.__init__(self, *args, **kwargs)

    def __str__(self):
        return pformat({'Installation': get_obj_values_as_dict(self)})
        return pformat({"Installation": get_obj_values_as_dict(self)})

    @classmethod
    def get(cls):
        installation=cls.objects.first()
        installation = cls.objects.first()
        if not installation:
            installation=Installation.create()
            installation = Installation.create()
        return installation
    

    @classmethod
    def create(cls):
        if cls.objects.first():
            return
        data={  "name": "GNGforms",
                "schemaVersion": app.config['SCHEMA_VERSION'],
                "created": datetime.date.today().strftime("%Y-%m-%d")}
        new_installation=cls(**data)
        data = {
            "name": "GNGforms",
            "schemaVersion": app.config["SCHEMA_VERSION"],
            "created": datetime.date.today().strftime("%Y-%m-%d"),
        }
        new_installation = cls(**data)
        new_installation.save()
        return new_installation
    

    def isSchemaUpToDate(self):
        return True if self.schemaVersion == app.config['SCHEMA_VERSION'] else False
        return True if self.schemaVersion == app.config["SCHEMA_VERSION"] else False

    def updateSchema(self):
        if not self.isSchemaUpToDate():
            migrated_up_to=migrateMongoSchema(self.schemaVersion)
            self.schemaVersion=migrated_up_to
            migrated_up_to = migrateMongoSchema(self.schemaVersion)
            self.schemaVersion = migrated_up_to
            self.save()
            return True if self.isSchemaUpToDate() else False
        else:
            True
    

    @staticmethod
    def isUser(email):
        return True if User.objects(email=email).first() else False
        

    @staticmethod
    def fallbackDPL():
        text=gettext("We take your data protection seriously. Please contact us for any inquiries.")
        return {"markdown": text, "html": "<p>"+text+"</p>"}
        text = gettext(
            "We take your data protection seriously. Please contact us for any inquiries."
        )
        return {"markdown": text, "html": "<p>" + text + "</p>"}

M gngforms/utils/email.py => gngforms/utils/email.py +96 -63
@@ 25,49 25,62 @@ from threading import Thread
from gngforms import app
from gngforms.models import Site, User


def createSmtpObj():
    config=g.site.smtpConfig
    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        
            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'))         
            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')
                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)
    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:


@@ 75,88 88,108 @@ def sendConfirmEmail(user, newEmail=None):


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)
    
    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)
    
    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))
    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 = "%s\n%s: %s" % (message, data[0], data[1])
    message = "%s\n" % message

    message='Subject: {}\n\n{}'.format(gettext("GNGforms. New form entry"), 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)
    
    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(form):
    emails=[]
    criteria={  'blocked':False,
                'hostname': form.hostname,
                'validatedEmail':True,
                'admin__isAdmin':True,
                'admin__notifyNewForm':True}
    admins=User.findAll(**criteria)
    emails = []
    criteria = {
        "blocked": False,
        "hostname": form.hostname,
        "validatedEmail": True,
        "admin__isAdmin": True,
        "admin__notifyNewForm": True,
    }
    admins = User.findAll(**criteria)
    for admin in admins:
        emails.append(admin['email'])
    rootUsers=User.objects(__raw__={'email': {"$in": app.config['ROOT_USERS']},
                                    'admin.notifyNewForm':True})
        emails.append(admin["email"])
    rootUsers = User.objects(
        __raw__={
            "email": {"$in": app.config["ROOT_USERS"]},
            "admin.notifyNewForm": True,
        }
    )
    for rootUser in rootUsers:
        if not rootUser['email'] in emails:
            emails.append(rootUser['email'])
        if not rootUser["email"] in emails:
            emails.append(rootUser["email"])

    message=gettext("New form '%s' created at %s" % (form.slug, form.hostname))
    message='Subject: {}\n\n{}'.format(gettext("GNGforms. New form notification"), message)
    message = gettext("New form '%s' created at %s" % (form.slug, form.hostname))
    message = "Subject: {}\n\n{}".format(
        gettext("GNGforms. New form notification"), message
    )
    for email in emails:
        sendMail(email, message)


def sendNewUserNotification(user):
    emails=[]
    criteria={  'blocked':False,
                'hostname': user.hostname,
                'validatedEmail': True,
                'admin__isAdmin':True,
                'admin__notifyNewUser':True}
    admins=User.findAll(**criteria)
    emails = []
    criteria = {
        "blocked": False,
        "hostname": user.hostname,
        "validatedEmail": True,
        "admin__isAdmin": True,
        "admin__notifyNewUser": True,
    }
    admins = User.findAll(**criteria)
    for admin in admins:
        emails.append(admin['email'])
    rootUsers=User.objects(__raw__={'email':{"$in": app.config['ROOT_USERS']},
                                    'admin.notifyNewUser':True})
        emails.append(admin["email"])
    rootUsers = User.objects(
        __raw__={
            "email": {"$in": app.config["ROOT_USERS"]},
            "admin.notifyNewUser": True,
        }
    )
    for rootUser in rootUsers:
        if not rootUser['email'] in emails:
            emails.append(rootUser['email'])
        if not rootUser["email"] in emails:
            emails.append(rootUser["email"])

    message=gettext("New user '%s' created at %s" % (user.username, user.hostname))
    message='Subject: {}\n\n{}'.format(gettext("GNGforms. New user notification"), message)    
    message = gettext("New user '%s' created at %s" % (user.username, user.hostname))
    message = "Subject: {}\n\n{}".format(
        gettext("GNGforms. New user notification"), message
    )
    for email in emails:
        sendMail(email, message)


def sendTestEmail(email):
    message=gettext("Congratulations!")
    message='Subject: {}\n\n{}'.format(gettext("SMTP test"), message)
    
    message = gettext("Congratulations!")
    message = "Subject: {}\n\n{}".format(gettext("SMTP test"), message)

    return sendMail(email, message)

M gngforms/utils/migrate.py => gngforms/utils/migrate.py +20 -9
@@ 18,14 18,15 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

from gngforms import models
#from pprint import pprint as pp

# from pprint import pprint as pp


def migrateMongoSchema(schemaVersion):

    if schemaVersion == 13:
        query = models.Form.objects()
        query.update(set__introductionText={"markdown":"", "html":""})
        query.update(set__introductionText={"markdown": "", "html": ""})
        schemaVersion = 14

    if schemaVersion == 14:


@@ 34,13 35,23 @@ def migrateMongoSchema(schemaVersion):
            # was removed from models.Forms in this version of gngforms.
            collection = models.Form._get_collection()
            for f in collection.find():
                consent=f["requireDataConsent"]
                collection.update_one(  {"_id": f["_id"]},
                                        {"$set": { "requireDataConsent": {  "markdown":"",
                                                                            "html":"",
                                                                            "required": consent}}})
                collection.update_one(  {"_id": f["_id"]},
                                        {"$rename": {"requireDataConsent": "dataConsent"} })
                consent = f["requireDataConsent"]
                collection.update_one(
                    {"_id": f["_id"]},
                    {
                        "$set": {
                            "requireDataConsent": {
                                "markdown": "",
                                "html": "",
                                "required": consent,
                            }
                        }
                    },
                )
                collection.update_one(
                    {"_id": f["_id"]},
                    {"$rename": {"requireDataConsent": "dataConsent"}},
                )
        except:
            return schemaVersion
        schemaVersion = 15

M gngforms/utils/session.py => gngforms/utils/session.py +26 -23
@@ 20,30 20,33 @@ 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 'introductionTextMD' in session:
        session['introductionTextMD'] = ''
    if not 'afterSubmitTextMD' in session:
        session['afterSubmitTextMD'] = ''
        
    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 "introductionTextMD" in session:
        session["introductionTextMD"] = ""
    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['introductionTextMD'] = form.introductionText['markdown']
    session['afterSubmitTextMD'] = form.afterSubmitText['markdown']
    # session['form_id'] = str(form._id)
    session["slug"] = form.slug
    session["formFieldIndex"] = form.fieldIndex
    session["formStructure"] = form.structure
    session["introductionTextMD"] = form.introductionText["markdown"]
    session["afterSubmitTextMD"] = form.afterSubmitText["markdown"]


def clearSessionFormData():
    session['slug'] = ""
    session['form_id']=None
    session['formFieldIndex'] = []
    session['formStructure'] = json.dumps([])
    session['introductionTextMD'] = ''
    session['afterSubmitTextMD'] = ''
    session["slug"] = ""
    session["form_id"] = None
    session["formFieldIndex"] = []
    session["formStructure"] = json.dumps([])
    session["introductionTextMD"] = ""
    session["afterSubmitTextMD"] = ""

M gngforms/utils/utils.py => gngforms/utils/utils.py +62 -36
@@ 17,10 17,10 @@ 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, babel #, models
from gngforms import app, babel  # , models

from flask import Response, redirect, request, url_for
from flask import g, flash #, has_app_context
from flask import g, flash  # , has_app_context
from flask_babel import gettext
from unidecode import unidecode
import json, time, re, string, random, datetime, csv


@@ 33,86 33,102 @@ from pprint import pformat

def get_obj_values_as_dict(obj):
    values = {}
    fields = type(obj).__dict__['_fields']
    fields = type(obj).__dict__["_fields"]
    for key, _ in fields.items():
        value = getattr(obj, key, None)
        values[key] = value
    return values


def make_url_for(function, **kwargs):
    kwargs["_external"]=True
    if 'site' in g:
        kwargs["_scheme"]=g.site.scheme
    kwargs["_external"] = True
    if "site" in g:
        kwargs["_scheme"] = g.site.scheme
    return url_for(function, **kwargs)


@babel.localeselector
def get_locale():
    if 'current_user' in g and g.current_user:
    if "current_user" in g and g.current_user:
        return g.current_user.language
    else:
        return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
        return request.accept_languages.best_match(app.config["LANGUAGES"].keys())


"""
Used to respond to Ajax requests
"""


def JsonResponse(json_response="1", status_code=200):
    response = Response(json_response, 'application/json; charset=utf-8')
    response.headers.add('content-length', len(json_response))
    response.status_code=status_code
    response = Response(json_response, "application/json; charset=utf-8")
    response.headers.add("content-length", len(json_response))
    response.status_code = status_code
    return response



""" ######## Sanitizers ######## """


def sanitizeString(string):
    string = unidecode(string)
    string = string.replace(" ", "") 
    return re.sub('[^A-Za-z0-9\-]', '', 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 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)
    return re.sub("[^a-z0-9]", "", string)


TAG_RE = re.compile(r"<[^>]+>")

TAG_RE = re.compile(r'<[^>]+>')

def escapeMarkdown(MDtext):
    return TAG_RE.sub('', MDtext)
    return TAG_RE.sub("", MDtext)


def markdown2HTML(MDtext):
    MDtext=escapeMarkdown(MDtext)
    return markdown.markdown(MDtext, extensions=['nl2br'])
    MDtext = escapeMarkdown(MDtext)
    return markdown.markdown(MDtext, extensions=["nl2br"])


def stripHTMLTags(text):
    #return TAG_RE.sub(' ', text).strip(' ')
    text=html.unescape(text) 
    soup=BeautifulSoup(text, features="html.parser")
    # return TAG_RE.sub(' ', text).strip(' ')
    text = html.unescape(text)
    soup = BeautifulSoup(text, features="html.parser")
    return soup.get_text()
    


def cleanLabel(text):
    # We should change this to use a whitelist
    text=html.unescape(text) 
    soup=BeautifulSoup(text, features="html.parser")
    text = html.unescape(text)
    soup = BeautifulSoup(text, features="html.parser")
    for script in soup.find_all("script"):
        script.decompose()
    for style in soup.find_all("style"):


@@ 130,6 146,7 @@ pwd_policy = PasswordPolicy.from_names(
    nonletters=1,  # need min. 2 non-letter characters (digits, specials, anything)
)


def hashPassword(password):
    return pbkdf2_sha256.hash(password, rounds=200000, salt_size=16)



@@ 140,39 157,47 @@ def verifyPassword(password, hash):

""" ######## fieldIndex helpers ######## """


def getFieldByNameInIndex(index, name):
    for field in index:
        if 'name' in field and field['name'] == name:
        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))
    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} 
    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']:
    token_age = datetime.datetime.now() - tokenData["created"]
    if token_age.total_seconds() > app.config["TOKEN_EXPIRATION"]:
        return False
    return True


""" ######## Dates ######## """


def isValidExpireDate(date):
    try:
        datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")


@@ 180,7 205,8 @@ def isValidExpireDate(date):
    except:
        return False


def isFutureDate(date):
    now=time.time()
    future=int(datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S").strftime("%s"))
    now = time.time()
    future = int(datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S").strftime("%s"))
    return True if future > now else False

M gngforms/utils/wraps.py => gngforms/utils/wraps.py +39 -20
@@ 21,51 21,62 @@ from flask import g, render_template
from gngforms.utils.utils import *
from functools import wraps


def login_required(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        if g.current_user:
            return f(*args, **kwargs)
        else:
            return redirect(url_for('main_bp.index'))
            return redirect(url_for("main_bp.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('main_bp.index'))
            return redirect(url_for("main_bp.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('main_bp.index'))
            return redirect(url_for("main_bp.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('main_bp.index'))
            return redirect(url_for("main_bp.index"))

    return wrap


def anon_required(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        if g.current_user:
            return redirect(url_for('main_bp.index'))
            return redirect(url_for("main_bp.index"))
        else:
            return f(*args, **kwargs)

    return wrap


"""
def queriedForm_editor_required(f):
    @wraps(f)


@@ 79,42 90,50 @@ def queriedForm_editor_required(f):
    return wrap
"""


def sanitized_slug_required(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        if not 'slug' in 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']:
                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']):
                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
                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 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
                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 "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
                flash(gettext("That's a nasty token!"), "warning")
            return render_template("page_not_found.html"), 404
        else:
            return f(*args, **kwargs)

    return wrap

M gngforms/utils/wtf.py => gngforms/utils/wtf.py +35 -19
@@ 1,5 1,13 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, IntegerField, SelectField, PasswordField, BooleanField, RadioField
from wtforms import (
    StringField,
    TextAreaField,
    IntegerField,
    SelectField,
    PasswordField,
    BooleanField,
    RadioField,
)
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from flask import g
from flask_babel import lazy_gettext as _


@@ 13,13 21,15 @@ class NewUser(FlaskForm):
    username = StringField(_("Username"), validators=[DataRequired()])
    email = StringField(_("Email"), validators=[DataRequired(), Email()])
    password = PasswordField(_("Password"), validators=[DataRequired()])
    password2 = PasswordField(_("Password again"), validators=[DataRequired(), EqualTo('password')])
    password2 = PasswordField(
        _("Password again"), validators=[DataRequired(), EqualTo("password")]
    )

    def validate_username(self, username):
        if username.data != sanitizeUsername(username.data):
            raise ValidationError(_("Username is not valid"))
            return False
        if username.data in app.config['RESERVED_USERNAMES']:
        if username.data in app.config["RESERVED_USERNAMES"]:
            raise ValidationError(_("Please use a different username"))
            return False
        if User.find(username=username.data):


@@ 28,41 38,44 @@ class NewUser(FlaskForm):
    def validate_email(self, email):
        if User.find(email=email.data):
            raise ValidationError(_("Please use a different email address"))
        elif email.data in app.config['ROOT_USERS'] and Installation.isUser(email.data):
        elif email.data in app.config["ROOT_USERS"] and Installation.isUser(email.data):
            # a root_user email can only be used once across all sites.
            raise ValidationError(_("Please use a different email address"))
            

    def validate_password(self, password):
        if pwd_policy.test(password.data):
            raise ValidationError(_("Your password is weak"))
            
            


class Login(FlaskForm):
    username = StringField(_("Username"), validators=[DataRequired()])
    password = PasswordField(_("Password"), validators=[DataRequired()])
    

    def validate_username(self, username):
        if username.data != sanitizeUsername(username.data):
            return False


class GetEmail(FlaskForm):
    email = StringField(_("Email address"), validators=[DataRequired(), Email()])
    
    


class ChangeEmail(FlaskForm):
    email = StringField(_("New email address"), validators=[DataRequired(), Email()])
    

    def validate_email(self, email):
        if User.find(email=email.data):
            raise ValidationError(_("Please use a different email address"))
        elif email.data in app.config['ROOT_USERS'] and Installation.isUser(email.data):
        elif email.data in app.config["ROOT_USERS"] and Installation.isUser(email.data):
            # a root_user email can only be used once across all sites.
            raise ValidationError(_("Please use a different email address"))


class ResetPassword(FlaskForm):
    password = PasswordField(_("Password"), validators=[DataRequired()])
    password2 = PasswordField(_("Password again"), validators=[DataRequired(), EqualTo('password')])
    password2 = PasswordField(
        _("Password again"), validators=[DataRequired(), EqualTo("password")]
    )

    def validate_password(self, password):
        if pwd_policy.test(password.data):


@@ 72,12 85,15 @@ class ResetPassword(FlaskForm):
class smtpConfig(FlaskForm):
    host = StringField(_("Email server"), validators=[DataRequired()])
    port = IntegerField(_("Port"))
    encryption = SelectField(_("Encryption"), choices=[ ('None', 'None'),
                                                        ('SSL', 'SSL'),
                                                        ('STARTTLS', 'STARTTLS (maybe)')])
    encryption = SelectField(
        _("Encryption"),
        choices=[("None", "None"), ("SSL", "SSL"), ("STARTTLS", "STARTTLS (maybe)")],
    )
    user = StringField(_("User"))
    password = StringField(_("Password"))
    noreplyAddress = StringField(_("Sender address"), validators=[DataRequired(), Email()])
    noreplyAddress = StringField(
        _("Sender address"), validators=[DataRequired(), Email()]
    )


class NewInvite(FlaskForm):


@@ 85,10 101,10 @@ class NewInvite(FlaskForm):
    message = TextAreaField(_("Include message"))
    admin = BooleanField(_("Make the new user an Admin"))
    hostname = StringField(_("hostname"), validators=[DataRequired()])
    

    def validate_email(self, email):
        if User.find(email=email.data, hostname=self.hostname.data):
            raise ValidationError(_("Please use a different email address"))
        elif email.data in app.config['ROOT_USERS'] and Installation.isUser(email.data):
        elif email.data in app.config["ROOT_USERS"] and Installation.isUser(email.data):
            # a root_user email can only be used once across all sites.
            raise ValidationError(_("Please use a different email address"))

M gngforms/views/admin.py => gngforms/views/admin.py +120 -86
@@ 27,174 27,208 @@ from gngforms.utils.utils import *
import gngforms.utils.wtf as wtf
import gngforms.utils.email as smtp

admin_bp = Blueprint('admin_bp', __name__,
                    template_folder='../templates/admin')
admin_bp = Blueprint("admin_bp", __name__, template_folder="../templates/admin")


""" User management """

@admin_bp.route('/admin', methods=['GET'])
@admin_bp.route('/admin/users', methods=['GET'])

@admin_bp.route("/admin", methods=["GET"])
@admin_bp.route("/admin/users", methods=["GET"])
@admin_required
def list_users():
    return render_template('list-users.html', users=User.findAll()) 
    return render_template("list-users.html", users=User.findAll())


@admin_bp.route('/admin/users/<string:id>', methods=['GET'])
#@admin_bp.route('/admin/users/id/<string:id>', methods=['GET'])
@admin_bp.route("/admin/users/<string:id>", methods=["GET"])
# @admin_bp.route('/admin/users/id/<string:id>', methods=['GET'])
@admin_required
def inspect_user(id):
    user=User.find(id=id)
    user = User.find(id=id)
    if not user:
        flash(gettext("User not found"), 'warning')
        return redirect(make_url_for('admin_bp.list_users'))
    return render_template('inspect-user.html', user=user) 
        flash(gettext("User not found"), "warning")
        return redirect(make_url_for("admin_bp.list_users"))
    return render_template("inspect-user.html", user=user)


@admin_bp.route('/admin/users/toggle-blocked/<string:id>', methods=['POST'])
@admin_bp.route("/admin/users/toggle-blocked/<string:id>", methods=["POST"])
@admin_required
def toggle_user_blocked(id):       
    user=User.find(id=id)
def toggle_user_blocked(id):
    user = User.find(id=id)
    if not user:
        return JsonResponse(json.dumps())
    if user.id == g.current_user.id:
        # current_user cannot disable themself
        blocked=user.blocked
        blocked = user.blocked
    else:
        blocked=user.toggleBlocked()
    return JsonResponse(json.dumps({'blocked':blocked}))
        blocked = user.toggleBlocked()
    return JsonResponse(json.dumps({"blocked": blocked}))


@admin_bp.route('/admin/users/toggle-admin/<string:id>', methods=['POST'])
@admin_bp.route("/admin/users/toggle-admin/<string:id>", methods=["POST"])
@admin_required
def toggle_admin(id):       
    user=User.find(id=id)
def toggle_admin(id):
    user = User.find(id=id)
    if not user:
        return JsonResponse(json.dumps())
    if user.username == g.current_user.username:
        # current_user cannot remove their own admin permission
        isAdmin=True
        isAdmin = True
    else:
        isAdmin=user.toggleAdmin()
    return JsonResponse(json.dumps({'admin':isAdmin}))
        isAdmin = user.toggleAdmin()
    return JsonResponse(json.dumps({"admin": isAdmin}))


@admin_bp.route('/admin/users/delete/<string:id>', methods=['GET', 'POST'])
@admin_bp.route("/admin/users/delete/<string:id>", methods=["GET", "POST"])
@admin_required
def delete_user(id):       
    user=User.find(id=id)
def delete_user(id):
    user = User.find(id=id)
    if not user:
        flash(gettext("User not found"), 'warning')
        return redirect(make_url_for('admin_bp.list_users'))
  
    if request.method == 'POST' and 'username' in request.form:
        flash(gettext("User not found"), "warning")
        return redirect(make_url_for("admin_bp.list_users"))

    if request.method == "POST" and "username" in request.form:
        if user.isRootUser():
            flash(gettext("Cannot delete root user"), 'warning')
            return redirect(make_url_for('admin_bp.inspect_user', id=user.id)) 
            flash(gettext("Cannot delete root user"), "warning")
            return redirect(make_url_for("admin_bp.inspect_user", id=user.id))
        if user.id == g.current_user.id:
            flash(gettext("Cannot delete yourself"), 'warning')
            return redirect(make_url_for('admin_bp.inspect_user', username=user.username)) 
        if user.username == request.form['username']:
            flash(gettext("Cannot delete yourself"), "warning")
            return redirect(
                make_url_for("admin_bp.inspect_user", username=user.username)
            )
        if user.username == request.form["username"]:
            user.deleteUser()
            flash(gettext("Deleted user '%s'" % (user.username)), 'success')
            return redirect(make_url_for('admin_bp.list_users'))
            flash(gettext("Deleted user '%s'" % (user.username)), "success")
            return redirect(make_url_for("admin_bp.list_users"))
        else:
            flash(gettext("Username does not match"), 'warning')
    return render_template('delete-user.html', user=user)

            flash(gettext("Username does not match"), "warning")
    return render_template("delete-user.html", user=user)


""" Form management """

@admin_bp.route('/admin/forms', methods=['GET'])

@admin_bp.route("/admin/forms", methods=["GET"])
@admin_required
def list_forms():
    return render_template('list-forms.html', forms=Form.findAll()) 
    return render_template("list-forms.html", forms=Form.findAll())


@admin_bp.route('/admin/forms/toggle-public/<string:id>', methods=['GET'])
@admin_bp.route("/admin/forms/toggle-public/<string:id>", methods=["GET"])
@admin_required
def toggle_form_public_admin_prefs(id):
    queriedForm = Form.find(id=id)
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('my_forms'))
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("my_forms"))
    queriedForm.toggleAdminFormPublic()
    return redirect(make_url_for('form_bp.inspect_form', id=id))
    return redirect(make_url_for("form_bp.inspect_form", id=id))


@admin_bp.route('/admin/forms/change-author/<string:id>', methods=['GET', 'POST'])
@admin_bp.route("/admin/forms/change-author/<string:id>", methods=["GET", "POST"])
@admin_required
def change_author(id):
    queriedForm = Form.find(id=id)
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('user_bp.my_forms'))
    if request.method == 'POST':
        if not ('old_author_username' in request.form and request.form['old_author_username']==queriedForm.author.username):
            flash(gettext("Current author incorrect"), 'warning')
            return render_template('change-author.html', form=queriedForm)
        if 'new_author_username' in request.form:
            new_author=User.find(username=request.form['new_author_username'], hostname=queriedForm.hostname)
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("user_bp.my_forms"))
    if request.method == "POST":
        if not (
            "old_author_username" in request.form
            and request.form["old_author_username"] == queriedForm.author.username
        ):
            flash(gettext("Current author incorrect"), "warning")
            return render_template("change-author.html", form=queriedForm)
        if "new_author_username" in request.form:
            new_author = User.find(
                username=request.form["new_author_username"],
                hostname=queriedForm.hostname,
            )
            if new_author:
                if new_author.enabled:
                    old_author=queriedForm.author
                    old_author = queriedForm.author
                    if queriedForm.changeAuthor(new_author):
                        queriedForm.addLog(gettext("Changed author from %s to %s" % (old_author.username, new_author.username)))
                        flash(gettext("Changed author OK"), 'success')
                        return redirect(make_url_for('form_bp.inspect_form', id=queriedForm.id))
                        queriedForm.addLog(
                            gettext(
                                "Changed author from %s to %s"
                                % (old_author.username, new_author.username)
                            )
                        )
                        flash(gettext("Changed author OK"), "success")
                        return redirect(
                            make_url_for("form_bp.inspect_form", id=queriedForm.id)
                        )
                else:
                    flash(gettext("Cannot use %s. The user is not enabled" % (request.form['new_author_username'])), 'warning')
                    flash(
                        gettext(
                            "Cannot use %s. The user is not enabled"
                            % (request.form["new_author_username"])
                        ),
                        "warning",
                    )
            else:
                flash(gettext("Can't find username %s" % (request.form['new_author_username'])), 'warning')
    return render_template('change-author.html', form=queriedForm)

                flash(
                    gettext(
                        "Can't find username %s" % (request.form["new_author_username"])
                    ),
                    "warning",
                )
    return render_template("change-author.html", form=queriedForm)


""" Invitations """

@admin_bp.route('/admin/invites/new', methods=['GET', 'POST'])

@admin_bp.route("/admin/invites/new", methods=["GET", "POST"])
@admin_required
def new_invite():
    wtform=wtf.NewInvite()
    if wtform.validate_on_submit():  
        message=wtform.message.data
    wtform = wtf.NewInvite()
    if wtform.validate_on_submit():
        message = wtform.message.data
        if not message:
            message=gettext("You have been invited to %s." % wtform.hostname.data)
        invite=Invite.create(wtform.hostname.data, wtform.email.data, message, wtform.admin.data)
            message = gettext("You have been invited to %s." % wtform.hostname.data)
        invite = Invite.create(
            wtform.hostname.data, wtform.email.data, message, wtform.admin.data
        )
        smtp.sendInvite(invite)
        flash(gettext("We've sent an invitation to %s") % invite.email, 'success')
        return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    return render_template('new-invite.html', wtform=wtform, sites=Site.findAll())
        flash(gettext("We've sent an invitation to %s") % invite.email, "success")
        return redirect(
            make_url_for("user_bp.user_settings", username=g.current_user.username)
        )
    return render_template("new-invite.html", wtform=wtform, sites=Site.findAll())


@admin_bp.route('/admin/invites/delete/<string:id>', methods=['GET'])
@admin_bp.route("/admin/invites/delete/<string:id>", methods=["GET"])
@admin_required
def delete_invite(id):
    invite=Invite.find(id=id)
    invite = Invite.find(id=id)
    if invite:
        invite.delete()
    else:
        flash(gettext("Opps! We can't find that invitation"), 'error')
    return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))

        flash(gettext("Opps! We can't find that invitation"), "error")
    return redirect(
        make_url_for("user_bp.user_settings", username=g.current_user.username)
    )


""" Personal Admin preferences """

@admin_bp.route('/admin/toggle-newuser-notification', methods=['POST'])

@admin_bp.route("/admin/toggle-newuser-notification", methods=["POST"])
@admin_required
def toggle_newUser_notification(): 
    return json.dumps({'notify': g.current_user.toggleNewUserNotification()})
def toggle_newUser_notification():
    return json.dumps({"notify": g.current_user.toggleNewUserNotification()})


@admin_bp.route('/admin/toggle-newform-notification', methods=['POST'])
@admin_bp.route("/admin/toggle-newform-notification", methods=["POST"])
@admin_required
def toggle_newForm_notification(): 
    return json.dumps({'notify': g.current_user.toggleNewFormNotification()})
def toggle_newForm_notification():
    return json.dumps({"notify": g.current_user.toggleNewFormNotification()})


@admin_bp.route('/admin/toggle-dataprotection', methods=['POST'])
@admin_bp.route("/admin/toggle-dataprotection", methods=["POST"])
@admin_required
def toggle_site_data_consent(): 
    return json.dumps({'dataprotection_enabled': g.site.togglePersonalDataConsentEnabled()})
def toggle_site_data_consent():
    return json.dumps(
        {"dataprotection_enabled": g.site.togglePersonalDataConsentEnabled()}
    )

M gngforms/views/entries.py => gngforms/views/entries.py +120 -84
@@ 28,198 28,234 @@ from gngforms.utils.wraps import *
from gngforms.utils.utils import *


entries_bp = Blueprint('entries_bp', __name__,
                    template_folder='../templates/entries')
    
entries_bp = Blueprint("entries_bp", __name__, template_folder="../templates/entries")

""" Form entries """

@entries_bp.route('/forms/entries/<string:id>', methods=['GET'])

@entries_bp.route("/forms/entries/<string:id>", methods=["GET"])
@enabled_user_required
def list_entries(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    return render_template('list-entries.html',
                            form=queriedForm,
                            with_deleted_columns=request.args.get('with_deleted_columns'),
                            edit_mode=request.args.get('edit_mode'))
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    return render_template(
        "list-entries.html",
        form=queriedForm,
        with_deleted_columns=request.args.get("with_deleted_columns"),
        edit_mode=request.args.get("edit_mode"),
    )


@entries_bp.route('/forms/entries/stats/<string:id>', methods=['GET'])
@entries_bp.route("/forms/entries/stats/<string:id>", methods=["GET"])
@enabled_user_required
def entry_stats(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    return render_template('chart-entries.html', form=queriedForm)
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    return render_template("chart-entries.html", form=queriedForm)


@entries_bp.route('/forms/csv/<string:id>', methods=['GET'])
@entries_bp.route("/forms/csv/<string:id>", methods=["GET"])
@enabled_user_required
def csv_form(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    csv_file=queriedForm.writeCSV(with_deleted_columns=request.args.get('with_deleted_columns'))
    
    @after_this_request 
    def remove_file(response): 
        os.remove(csv_file) 
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    csv_file = queriedForm.writeCSV(
        with_deleted_columns=request.args.get("with_deleted_columns")
    )

    @after_this_request
    def remove_file(response):
        os.remove(csv_file)
        return response

    return send_file(csv_file, mimetype="text/csv", as_attachment=True)


@entries_bp.route('/forms/delete-entry/<string:id>', methods=['POST'])
@entries_bp.route("/forms/delete-entry/<string:id>", methods=["POST"])
@enabled_user_required
def delete_entry(id):
    queriedForm=Form.find(id=id, editor_id=str(g.current_user.id))
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return json.dumps({'deleted': False})
        return json.dumps({"deleted": False})
    # we going to use the entry's "created" value as a unique value (gulps).
    if not "created" in request.json:
        return json.dumps({'deleted': False})
    foundEntries = [entry for entry in queriedForm.entries if entry['created'] == request.json["created"]]
        return json.dumps({"deleted": False})
    foundEntries = [
        entry
        for entry in queriedForm.entries
        if entry["created"] == request.json["created"]
    ]
    if not foundEntries or len(foundEntries) > 1:
        """ If there are two entries with the same 'created' value, we don't delete anything """
        return json.dumps({'deleted': False})
    queriedForm.entries.remove(foundEntries[0])    
        return json.dumps({"deleted": False})
    queriedForm.entries.remove(foundEntries[0])
    queriedForm.expired = queriedForm.hasExpired()
    queriedForm.save()
    queriedForm.addLog(gettext("Deleted an entry"))
    return json.dumps({'deleted': True})
    return json.dumps({"deleted": True})


@entries_bp.route('/forms/undo-delete-entry/<string:id>', methods=['POST'])
@entries_bp.route("/forms/undo-delete-entry/<string:id>", methods=["POST"])
@enabled_user_required
def undo_delete_entry(id):
    queriedForm=Form.find(id=id, editor_id=str(g.current_user.id))
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return json.dumps({'undone': False})
        return json.dumps({"undone": False})
    # check we have a "created" key
    created_pos=next((i for i,field in enumerate(request.json) if "name" in field and field["name"] == "created"), None)
    created_pos = next(
        (
            i
            for i, field in enumerate(request.json)
            if "name" in field and field["name"] == "created"
        ),
        None,
    )
    if not isinstance(created_pos, int):
        return json.dumps({'undone': False})
    foundEntries = [entry for entry in queriedForm.entries if entry['created'] == request.json[created_pos]["value"]]
        return json.dumps({"undone": False})
    foundEntries = [
        entry
        for entry in queriedForm.entries
        if entry["created"] == request.json[created_pos]["value"]
    ]
    if foundEntries:
        """ There is already an entry in the DB with the same 'created' value, we don't do anything """
        return json.dumps({'undone': False})
    entry={}
        return json.dumps({"undone": False})
    entry = {}
    for field in request.json:
        try:
            entry[field["name"]]=field["value"]
            entry[field["name"]] = field["value"]
        except:
            return json.dumps({'undone': False})
            return json.dumps({"undone": False})
    queriedForm.entries.append(entry)
    queriedForm.expired = queriedForm.hasExpired()
    queriedForm.save()
    queriedForm.addLog(gettext("Undeleted an entry"))
    return json.dumps({'undone': True})
    return json.dumps({"undone": True})


@entries_bp.route('/forms/change-entry-field-value/<string:id>', methods=['POST'])
@entries_bp.route("/forms/change-entry-field-value/<string:id>", methods=["POST"])
@enabled_user_required
def change_entry(id):
    queriedForm=Form.find(id=id, editor_id=str(g.current_user.id))
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return json.dumps({'saved': False})
        
        return json.dumps({"saved": False})

    # get the 'created' field position
    created_pos=next((i for i,field in enumerate(request.json) if "name" in field and field["name"] == "created"), None)
    created_pos = next(
        (
            i
            for i, field in enumerate(request.json)
            if "name" in field and field["name"] == "created"
        ),
        None,
    )
    if not isinstance(created_pos, int):
        return json.dumps({'saved': False})
    foundEntries = [entry for entry in queriedForm.entries if entry['created'] == request.json[created_pos]["value"]]
        return json.dumps({"saved": False})
    foundEntries = [
        entry
        for entry in queriedForm.entries
        if entry["created"] == request.json[created_pos]["value"]
    ]
    if not foundEntries or len(foundEntries) > 1:
        """ If there are two entries with the same 'created' value, we don't change anything """
        return json.dumps({'saved': False})
        return json.dumps({"saved": False})
    try:
        entry_pos = [pos for pos, entry in enumerate(queriedForm.entries) if entry == foundEntries[0]][0]
        entry_pos = [
            pos
            for pos, entry in enumerate(queriedForm.entries)
            if entry == foundEntries[0]
        ][0]
    except:
        return json.dumps({'saved': False})
    modifiedEntry={}
        return json.dumps({"saved": False})
    modifiedEntry = {}
    for field in request.json:
        try:
            modifiedEntry[field["name"]]=field["value"]
            modifiedEntry[field["name"]] = field["value"]
        except:
            return json.dumps({'saved': False})
            return json.dumps({"saved": False})
    del queriedForm.entries[entry_pos]
    queriedForm.entries.insert(entry_pos, modifiedEntry)
    queriedForm.expired = queriedForm.hasExpired()
    queriedForm.save()
    queriedForm.addLog(gettext("Modified an entry"))
    return json.dumps({'saved': True})
    return json.dumps({"saved": True})


@entries_bp.route('/forms/delete-entries/<string:id>', methods=['GET', 'POST'])
@entries_bp.route("/forms/delete-entries/<string:id>", methods=["GET", "POST"])
@enabled_user_required
def delete_entries(id):
    queriedForm=Form.find(id=id, editor_id=str(g.current_user.id))
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    if request.method == 'POST':
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    if request.method == "POST":
        try:
            totalEntries = int(request.form['totalEntries'])
            totalEntries = int(request.form["totalEntries"])
        except:
            flash(gettext("We expected a number"), 'warning')
            return render_template('delete-entries.html', form=queriedForm)
            flash(gettext("We expected a number"), "warning")
            return render_template("delete-entries.html", form=queriedForm)
        if queriedForm.totalEntries == totalEntries:
            queriedForm.deleteEntries()
            if not queriedForm.hasExpired() and queriedForm.expired:
                queriedForm.expired=False
                queriedForm.expired = False
                queriedForm.save()
            flash(gettext("Deleted %s entries" % totalEntries), 'success')
            return redirect(make_url_for('entries_bp.list_entries', id=queriedForm.id))
            flash(gettext("Deleted %s entries" % totalEntries), "success")
            return redirect(make_url_for("entries_bp.list_entries", id=queriedForm.id))
        else:
            flash(gettext("Number of entries does not match"), 'warning')
    return render_template('delete-entries.html', form=queriedForm)
            flash(gettext("Number of entries does not match"), "warning")
    return render_template("delete-entries.html", form=queriedForm)


@entries_bp.route('/<string:slug>/results/<string:key>', methods=['GET'])
@entries_bp.route("/<string:slug>/results/<string:key>", methods=["GET"])
@sanitized_slug_required
@sanitized_key_required
def view_entries(slug, key):
    queriedForm = Form.find(slug=slug, key=key)
    if not queriedForm or not queriedForm.areEntriesShared():
        return render_template('page-not-found.html'), 400
        return render_template("page-not-found.html"), 400
    if queriedForm.restrictedAccess and not g.current_user:
        return render_template('page-not-found.html'), 400
    return render_template('view-results.html', form=queriedForm, language=get_locale())    
        return render_template("page-not-found.html"), 400
    return render_template("view-results.html", form=queriedForm, language=get_locale())


@entries_bp.route('/<string:slug>/stats/<string:key>', methods=['GET'])
@entries_bp.route("/<string:slug>/stats/<string:key>", methods=["GET"])
@sanitized_slug_required
@sanitized_key_required
def view_stats(slug, key):
    queriedForm = Form.find(slug=slug, key=key)
    if not queriedForm or not queriedForm.areEntriesShared():
        return render_template('page-not-found.html'), 400
        return render_template("page-not-found.html"), 400
    if queriedForm.restrictedAccess and not g.current_user:
        return render_template('page-not-found.html'), 400
    return render_template('chart-entries.html', form=queriedForm, shared=True)
        return render_template("page-not-found.html"), 400
    return render_template("chart-entries.html", form=queriedForm, shared=True)


@entries_bp.route('/<string:slug>/csv/<string:key>', methods=['GET'])
@entries_bp.route("/<string:slug>/csv/<string:key>", methods=["GET"])
@sanitized_slug_required
@sanitized_key_required
def view_csv(slug, key):
    queriedForm = Form.find(slug=slug, key=key)
    if not queriedForm or not queriedForm.areEntriesShared():
        return render_template('page-not-found.html'), 400
        return render_template("page-not-found.html"), 400
    if queriedForm.restrictedAccess and not g.current_user:
        return render_template('page-not-found.html'), 400
        return render_template("page-not-found.html"), 400
    csv_file = queriedForm.writeCSV()
    
    @after_this_request 
    def remove_file(response): 
        os.remove(csv_file) 

    @after_this_request
    def remove_file(response):
        os.remove(csv_file)
        return response

    return send_file(csv_file, mimetype="text/csv", as_attachment=True)


@entries_bp.route('/<string:slug>/json/<string:key>', methods=['GET'])
@entries_bp.route("/<string:slug>/json/<string:key>", methods=["GET"])
@sanitized_slug_required
@sanitized_key_required
def view_json(slug, key):

M gngforms/views/form.py => gngforms/views/form.py +311 -273
@@ 35,80 35,84 @@ from form_templates import formTemplates

from pprint import pprint as pp

form_bp = Blueprint('form_bp', __name__,
                    template_folder='../templates/form')
form_bp = Blueprint("form_bp", __name__, template_folder="../templates/form")


@form_bp.route('/forms', methods=['GET'])
@form_bp.route("/forms", methods=["GET"])
@enabled_user_required
def my_forms():
    return render_template('my-forms.html', forms=g.current_user.forms) 
    return render_template("my-forms.html", forms=g.current_user.forms)


@form_bp.route('/forms/templates', methods=['GET'])
@form_bp.route("/forms/templates", methods=["GET"])
@login_required
def list_form_templates():
    return render_template('form_templates.html', templates=formTemplates)
    return render_template("form_templates.html", templates=formTemplates)


@form_bp.route('/forms/new', methods=['GET'])
@form_bp.route('/forms/new/<string:templateID>', methods=['GET'])
@form_bp.route("/forms/new", methods=["GET"])
@form_bp.route("/forms/new/<string:templateID>", methods=["GET"])
@enabled_user_required
def new_form(templateID=None):
    clearSessionFormData()
    if templateID:
        template = list(filter(lambda template: template['id'] == templateID, formTemplates))
        template = list(
            filter(lambda template: template["id"] == templateID, formTemplates)
        )
        if template:
            session['formStructure']=template[0]['structure']
    if not session['formStructure']:
        session['introductionTextMD'] = "## {}\n\n{}".format(template['title'], template['introduction'])
            session["formStructure"] = template[0]["structure"]
    if not session["formStructure"]:
        session["introductionTextMD"] = "## {}\n\n{}".format(
            template["title"], template["introduction"]
        )
    else:
        session['introductionTextMD'] = Form.defaultIntroductionText()
    session['afterSubmitTextMD'] = "## %s" % gettext("Thank you!!")
    return render_template('edit-form.html', host_url=g.site.host_url)
        session["introductionTextMD"] = Form.defaultIntroductionText()
    session["afterSubmitTextMD"] = "## %s" % gettext("Thank you!!")
    return render_template("edit-form.html", host_url=g.site.host_url)


@form_bp.route('/forms/edit', methods=['GET', 'POST'])
@form_bp.route('/forms/edit/<string:id>', methods=['GET', 'POST'])
@form_bp.route("/forms/edit", methods=["GET", "POST"])
@form_bp.route("/forms/edit/<string:id>", methods=["GET", "POST"])
@enabled_user_required
def edit_form(id=None):
    ensureSessionFormKeys()
    session['form_id']=None
    queriedForm=None
    session["form_id"] = None
    queriedForm = None
    if id:
        queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
        if not queriedForm:
            flash(gettext("You can't edit that form"), 'warning')
            return redirect(make_url_for('form_bp.my_forms'))
        session['form_id'] = str(queriedForm.id)
            flash(gettext("You can't edit that form"), "warning")
            return redirect(make_url_for("form_bp.my_forms"))
        session["form_id"] = str(queriedForm.id)

    if request.method == 'POST':
    if request.method == "POST":
        if queriedForm:
            session['slug'] = queriedForm.slug
        elif 'slug' in request.form:
            session['slug'] = sanitizeSlug(request.form['slug'])
        if not session['slug']:
            flash(gettext("Something went wrong. No slug!"), 'error')
            return redirect(make_url_for('form_bp.my_forms'))
            session["slug"] = queriedForm.slug
        elif "slug" in request.form:
            session["slug"] = sanitizeSlug(request.form["slug"])
        if not session["slug"]:
            flash(gettext("Something went wrong. No slug!"), "error")
            return redirect(make_url_for("form_bp.my_forms"))

        """ formStructure is generated by formBuilder. """
        formStructure = json.loads(request.form['structure']) 
        
        session['formFieldIndex']=[]   
        formStructure = json.loads(request.form["structure"])

        session["formFieldIndex"] = []
        for element in formStructure:
            if 'name' in element:
            if "name" in element:
                # Create a fieldIndex from the submitted structure
                if "label" not in element:
                    element['label']=""
                    element["label"] = ""
                else:
                    element['label']=stripHTMLTags(element['label'])
                session['formFieldIndex'].append({  'name': element['name'],
                                                    'label': element['label']})
                    element["label"] = stripHTMLTags(element["label"])
                session["formFieldIndex"].append(
                    {"name": element["name"], "label": element["label"]}
                )
            # repair some things if needed.
            if "type" in element:
                if element['type'] == 'paragraph':
                if element["type"] == "paragraph":
                    # remove unwanted HTML tags from paragraph text
                    element["label"]=cleanLabel(element["label"])
                    element["label"] = cleanLabel(element["label"])
                    continue
                # formBuilder does not save select dropdown correctly
                if element["type"] == "select" and "multiple" in element:


@@ 116,72 120,88 @@ def edit_form(id=None):
                        del element["multiple"]
                # formBuilder does not enforce values for checkbox groups, radio groups and selects.
                # we add a value when missing, and sanitize values (eg. a comma would be bad).
                if  element["type"] == "checkbox-group" or \
                    element["type"] == "radio-group" or \
                    element["type"] == "select":
                if (
                    element["type"] == "checkbox-group"
                    or element["type"] == "radio-group"
                    or element["type"] == "select"
                ):
                    for input_type in element["values"]:
                        if not input_type["value"] and input_type["label"]:
                            input_type["value"] = input_type["label"]
                        input_type["value"] = sanitizeString(input_type["value"].replace(" ", "-"))
    
        session['formStructure'] = json.dumps(formStructure)
        session['introductionTextMD'] = escapeMarkdown(request.form['introductionTextMD'])
        session['afterSubmitTextMD'] = escapeMarkdown(request.form['afterSubmitTextMD'])
        
        return redirect(make_url_for('form_bp.preview_form'))
    return render_template('edit-form.html', host_url=g.site.host_url)
                        input_type["value"] = sanitizeString(
                            input_type["value"].replace(" ", "-")
                        )

        session["formStructure"] = json.dumps(formStructure)
        session["introductionTextMD"] = escapeMarkdown(
            request.form["introductionTextMD"]
        )
        session["afterSubmitTextMD"] = escapeMarkdown(request.form["afterSubmitTextMD"])

        return redirect(make_url_for("form_bp.preview_form"))
    return render_template("edit-form.html", host_url=g.site.host_url)

@form_bp.route('/forms/check-slug-availability', methods=['POST'])

@form_bp.route("/forms/check-slug-availability", methods=["POST"])
@enabled_user_required
def is_slug_available():    
    if 'slug' in request.form and request.form['slug']:
        slug=request.form['slug']
def is_slug_available():
    if "slug" in request.form and request.form["slug"]:
        slug = request.form["slug"]
    else:
        return JsonResponse(json.dumps({'slug':"", 'available':False}))
        return JsonResponse(json.dumps({"slug": "", "available": False}))
    available = True
    slug=sanitizeSlug(slug)
    slug = sanitizeSlug(slug)
    if not slug:
        available = False
    elif Form.find(slug=slug, hostname=g.site.hostname):
        available = False
    elif slug in app.config['RESERVED_SLUGS']:
    elif slug in app.config["RESERVED_SLUGS"]:
        available = False
    # we return a sanitized slug as a suggestion for the user.
    return JsonResponse(json.dumps({'slug':slug, 'available':available}))
    return JsonResponse(json.dumps({"slug": slug, "available": available}))


@form_bp.route('/forms/preview', methods=['GET'])
@form_bp.route("/forms/preview", methods=["GET"])
@enabled_user_required
def preview_form():
    if not ('slug' in session and 'formStructure' in session):
        return redirect(make_url_for('form_bp.my_forms'))
    return render_template( 'preview-form.html',
                            slug=session['slug'],
                            introductionText=markdown2HTML(session['introductionTextMD']),
                            afterSubmitMsg=markdown2HTML(session['afterSubmitTextMD']))


@form_bp.route('/forms/save', methods=['POST'])
@form_bp.route('/forms/save/<string:id>', methods=['POST'])
    if not ("slug" in session and "formStructure" in session):
        return redirect(make_url_for("form_bp.my_forms"))
    return render_template(
        "preview-form.html",
        slug=session["slug"],
        introductionText=markdown2HTML(session["introductionTextMD"]),
        afterSubmitMsg=markdown2HTML(session["afterSubmitTextMD"]),
    )


@form_bp.route("/forms/save", methods=["POST"])
@form_bp.route("/forms/save/<string:id>", methods=["POST"])
@enabled_user_required
def save_form(id=None):
    

    """ We prepend the reserved field 'Created' to the index
        app.config['RESERVED_FORM_ELEMENT_NAMES'] = ['created']
    """
    session['formFieldIndex'].insert(0, {'label':gettext("Created"), 'name':'created'})
    introductionText={  'markdown':escapeMarkdown(session['introductionTextMD']),
                        'html':markdown2HTML(session['introductionTextMD'])} 
    afterSubmitText={   'markdown':escapeMarkdown(session['afterSubmitTextMD']),
                        'html':markdown2HTML(session['afterSubmitTextMD'])} 
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id)) if id else None    
    session["formFieldIndex"].insert(
        0, {"label": gettext("Created"), "name": "created"}
    )
    introductionText = {
        "markdown": escapeMarkdown(session["introductionTextMD"]),
        "html": markdown2HTML(session["introductionTextMD"]),
    }
    afterSubmitText = {
        "markdown": escapeMarkdown(session["afterSubmitTextMD"]),
        "html": markdown2HTML(session["afterSubmitTextMD"]),
    }
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id)) if id else None
    if queriedForm:
        # update form.fieldConditions
        savedConditionalFields = [field for field in queriedForm.fieldConditions]
        availableConditionalFields=[element["name"] 
                                    for element in json.loads(session['formStructure'])
                                    if "name" in element]
        availableConditionalFields = [
            element["name"]
            for element in json.loads(session["formStructure"])
            if "name" in element
        ]
        for field in savedConditionalFields:
            if not field in availableConditionalFields:
                del queriedForm.fieldConditions[field]


@@ 190,13 210,13 @@ def save_form(id=None):
            # We want to remove the fields the editor has deleted,
            # but we don't want to remove fields that already contain data in the DB.
            for field in queriedForm.fieldIndex:
                if not getFieldByNameInIndex(session['formFieldIndex'], field['name']):
                if not getFieldByNameInIndex(session["formFieldIndex"], field["name"]):
                    # This field was removed by the editor. Can we safely remove it?
                    can_delete=True
                    can_delete = True
                    for entry in queriedForm.entries:
                        if field['name'] in entry and entry[field['name']]:
                        if field["name"] in entry and entry[field["name"]]:
                            # This field contains data
                            can_delete=False
                            can_delete = False
                            break
                    if can_delete:
                        # A pseudo delete. We drop the field (it's reference) from the index.


@@ 204,148 224,157 @@ def save_form(id=None):
                        pass
                    else:
                        # We maintain this field in the index because it contains data
                        field['removed']=True
                        session['formFieldIndex'].append(field)

        queriedForm.structure=session["formStructure"]
        queriedForm.fieldIndex=session["formFieldIndex"]
        
        queriedForm.introductionText=introductionText
        queriedForm.afterSubmitText=afterSubmitText
                        field["removed"] = True
                        session["formFieldIndex"].append(field)

        queriedForm.structure = session["formStructure"]
        queriedForm.fieldIndex = session["formFieldIndex"]

        queriedForm.introductionText = introductionText
        queriedForm.afterSubmitText = afterSubmitText
        queriedForm.save()
        
        flash(gettext("Updated form OK"), 'success')

        flash(gettext("Updated form OK"), "success")
        queriedForm.addLog(gettext("Form edited"))
        return redirect(make_url_for('form_bp.inspect_form', id=queriedForm.id))
        return redirect(make_url_for("form_bp.inspect_form", id=queriedForm.id))
    else:
        if not session['slug']:
        if not session["slug"]:
            # just in case!
            flash(gettext("Slug is missing."), 'error')
            return redirect(make_url_for('form_bp.edit_form'))
        if Form.find(slug=session['slug'], hostname=g.site.hostname):
            flash(gettext("Slug is not unique. %s" % (session['slug'])), 'error')
            return redirect(make_url_for('form_bp.edit_form'))

        newFormData={
                    "created": datetime.date.today().strftime("%Y-%m-%d"),
                    "author_id": str(g.current_user.id),
                    "editors": {str(g.current_user.id): Form.newEditorPreferences()},
                    "postalCode": "08014",
                    "enabled": False,
                    "expired": False,
                    "expiryConditions": {"expireDate": False, "fields": {}},
                    "hostname": g.site.hostname,
                    "slug": session['slug'],
                    "structure": session['formStructure'],
                    "fieldIndex": session['formFieldIndex'],
                    "entries": [],
                    "sharedEntries": {  "enabled": False,
                                        "key": getRandomString(32),
                                        "password": False,
                                        "expireDate": False},
                    "introductionText": introductionText,
                    "afterSubmitText": afterSubmitText,
                    "dataConsent": {"markdown":"",
                                    "html":"",
                                    "required": g.site.isPersonalDataConsentEnabled()},
                    "log": [],
                    "restrictedAccess": False,
                    "adminPreferences": { "public": True }
                }
        newForm=Form.saveNewForm(newFormData)
            flash(gettext("Slug is missing."), "error")
            return redirect(make_url_for("form_bp.edit_form"))
        if Form.find(slug=session["slug"], hostname=g.site.hostname):
            flash(gettext("Slug is not unique. %s" % (session["slug"])), "error")
            return redirect(make_url_for("form_bp.edit_form"))

        newFormData = {
            "created": datetime.date.today().strftime("%Y-%m-%d"),
            "author_id": str(g.current_user.id),
            "editors": {str(g.current_user.id): Form.newEditorPreferences()},
            "postalCode": "08014",
            "enabled": False,
            "expired": False,
            "expiryConditions": {"expireDate": False, "fields": {}},
            "hostname": g.site.hostname,
            "slug": session["slug"],
            "structure": session["formStructure"],
            "fieldIndex": session["formFieldIndex"],
            "entries": [],
            "sharedEntries": {
                "enabled": False,
                "key": getRandomString(32),
                "password": False,
                "expireDate": False,
            },
            "introductionText": introductionText,
            "afterSubmitText": afterSubmitText,
            "dataConsent": {
                "markdown": "",
                "html": "",
                "required": g.site.isPersonalDataConsentEnabled(),
            },
            "log": [],
            "restrictedAccess": False,
            "adminPreferences": {"public": True},
        }
        newForm = Form.saveNewForm(newFormData)
        clearSessionFormData()
        newForm.addLog(gettext("Form created"))
        flash(gettext("Saved form OK"), 'success')
        flash(gettext("Saved form OK"), "success")
        # notify form.site.admins
        thread = Thread(target=smtp.sendNewFormNotification(newForm))
        thread.start()
        return redirect(make_url_for('form_bp.inspect_form', id=newForm.id))
        return redirect(make_url_for("form_bp.inspect_form", id=newForm.id))
    clearSessionFormData()
    return redirect(make_url_for('form_bp.my_forms'))
    return redirect(make_url_for("form_bp.my_forms"))


@form_bp.route('/forms/save-consent-text/<string:id>', methods=['POST'])
@form_bp.route("/forms/save-consent-text/<string:id>", methods=["POST"])
@enabled_user_required
def save_data_consent_text(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    MDtext=request.form['DPLMD'].strip()
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    MDtext = request.form["DPLMD"].strip()
    if MDtext:
        queriedForm.saveDataConsentText(MDtext)
        flash(gettext("Text saved OK"), 'success')
    return redirect(make_url_for('form_bp.inspect_form', id=queriedForm.id))
    
        flash(gettext("Text saved OK"), "success")
    return redirect(make_url_for("form_bp.inspect_form", id=queriedForm.id))


@form_bp.route('/forms/delete/<string:id>', methods=['GET', 'POST'])
@form_bp.route("/forms/delete/<string:id>", methods=["GET", "POST"])
@enabled_user_required
def delete_form(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    if request.method == 'POST':
        if queriedForm.slug == request.form['slug']:
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    if request.method == "POST":
        if queriedForm.slug == request.form["slug"]:
            entries = queriedForm.totalEntries
            queriedForm.delete()
            flash(gettext("Deleted '%s' and %s entries" % (queriedForm.slug, entries)), 'success')
            return redirect(make_url_for('form_bp.my_forms'))
            flash(
                gettext("Deleted '%s' and %s entries" % (queriedForm.slug, entries)),
                "success",
            )
            return redirect(make_url_for("form_bp.my_forms"))
        else:
            flash(gettext("Form name does not match"), 'warning')
    return render_template('delete-form.html', form=queriedForm)
            flash(gettext("Form name does not match"), "warning")
    return render_template("delete-form.html", form=queriedForm)


@form_bp.route('/forms/view/<string:id>', methods=['GET'])
@form_bp.route("/forms/view/<string:id>", methods=["GET"])
@enabled_user_required
def inspect_form(id):
    queriedForm = Form.find(id=id)
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    #pp(queriedForm)
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    # pp(queriedForm)
    if not g.current_user.canInspectForm(queriedForm):
        flash(gettext("Permission needed to view form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
        flash(gettext("Permission needed to view form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    # We populate the 'session' because /forms/edit uses it.
    populateSessionFormData(queriedForm)
    return render_template('inspect-form.html', form=queriedForm)
    return render_template("inspect-form.html", form=queriedForm)


@form_bp.route('/forms/share/<string:id>', methods=['GET'])
@form_bp.route("/forms/share/<string:id>", methods=["GET"])
@enabled_user_required
def share_form(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    return render_template('share-form.html', form=queriedForm, wtform=wtf.GetEmail())
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    return render_template("share-form.html", form=queriedForm, wtform=wtf.GetEmail())


@form_bp.route('/forms/add-editor/<string:id>', methods=['POST'])
@form_bp.route("/forms/add-editor/<string:id>", methods=["POST"])
@enabled_user_required
def add_editor(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    wtform=wtf.GetEmail()
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    wtform = wtf.GetEmail()
    if wtform.validate():
        newEditor=User.find(email=wtform.email.data, hostname=queriedForm.hostname)
        if not newEditor or newEditor.enabled==False:
            flash(gettext("Can't find a user with that email"), 'warning')
            return redirect(make_url_for('form_bp.share_form', id=queriedForm.id))
        newEditor = User.find(email=wtform.email.data, hostname=queriedForm.hostname)
        if not newEditor or newEditor.enabled == False:
            flash(gettext("Can't find a user with that email"), "warning")
            return redirect(make_url_for("form_bp.share_form", id=queriedForm.id))
        if str(newEditor.id) in queriedForm.editors:
            flash(gettext("%s is already an editor" % newEditor.email), 'warning')
            return redirect(make_url_for('form_bp.share_form', id=queriedForm.id))
        
            flash(gettext("%s is already an editor" % newEditor.email), "warning")
            return redirect(make_url_for("form_bp.share_form", id=queriedForm.id))

        if queriedForm.addEditor(newEditor):
            flash(gettext("New editor added ok"), 'success')
            flash(gettext("New editor added ok"), "success")
            queriedForm.addLog(gettext("Added editor %s" % newEditor.email))
    return redirect(make_url_for('form_bp.share_form', id=queriedForm.id))
    return redirect(make_url_for("form_bp.share_form", id=queriedForm.id))


@form_bp.route('/forms/remove-editor/<string:form_id>/<string:editor_id>', methods=['POST'])
@form_bp.route(
    "/forms/remove-editor/<string:form_id>/<string:editor_id>", methods=["POST"]
)
@enabled_user_required
def remove_editor(form_id, editor_id):
    queriedForm = Form.find(id=form_id, editor_id=str(g.current_user.id))


@@ 353,166 382,169 @@ def remove_editor(form_id, editor_id):
        return json.dumps(False)
    if editor_id == queriedForm.author_id:
        return json.dumps(False)
    removedEditor_id=queriedForm.removeEditor(editor_id)
    removedEditor_id = queriedForm.removeEditor(editor_id)
    try:
        editor=User.find(id=removedEditor_id).email
        editor = User.find(id=removedEditor_id).email
    except:
        editor=removedEditor_id
        editor = removedEditor_id
    queriedForm.addLog(gettext("Removed editor %s" % editor))
    return json.dumps(removedEditor_id)


@form_bp.route('/forms/expiration/<string:id>', methods=['GET', 'POST'])
@form_bp.route("/forms/expiration/<string:id>", methods=["GET", "POST"])
@enabled_user_required
def set_expiration_date(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    if request.method == 'POST':
        if 'date' in request.form and 'time' in request.form:
            if request.form['date'] and request.form['time']:
                expireDate="%s %s:00" % (request.form['date'], request.form['time'])
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    if request.method == "POST":
        if "date" in request.form and "time" in request.form:
            if request.form["date"] and request.form["time"]:
                expireDate = "%s %s:00" % (request.form["date"], request.form["time"])
                if not isValidExpireDate(expireDate):
                    flash(gettext("Date-time is not valid"), 'warning')
                    flash(gettext("Date-time is not valid"), "warning")
                else:
                    queriedForm.expiryConditions['expireDate']=expireDate
                    queriedForm.expired=queriedForm.hasExpired()
                    queriedForm.expiryConditions["expireDate"] = expireDate
                    queriedForm.expired = queriedForm.hasExpired()
                    queriedForm.save()
                    queriedForm.addLog(gettext("Expiry date set to: %s" % expireDate))
            elif not request.form['date'] and not request.form['time']:
                if queriedForm.expiryConditions['expireDate']:
                    queriedForm.expiryConditions['expireDate']=False
                    queriedForm.expired=queriedForm.hasExpired()
            elif not request.form["date"] and not request.form["time"]:
                if queriedForm.expiryConditions["expireDate"]:
                    queriedForm.expiryConditions["expireDate"] = False
                    queriedForm.expired = queriedForm.hasExpired()
                    queriedForm.save()
                    queriedForm.addLog(gettext("Expiry date cancelled"))
            else:
                flash(gettext("Missing date or time"), 'warning')
    return render_template('expiration.html', form=queriedForm)
                flash(gettext("Missing date or time"), "warning")
    return render_template("expiration.html", form=queriedForm)


@form_bp.route('/forms/set-field-condition/<string:id>', methods=['POST'])
@form_bp.route("/forms/set-field-condition/<string:id>", methods=["POST"])
@enabled_user_required
def set_field_condition(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps({'condition': False}))

    availableFields=queriedForm.getAvailableNumberTypeFields()
    if not request.form['field_name'] in availableFields:
        return JsonResponse(json.dumps({'condition': False}))
    
    if not request.form['condition']:
        if request.form['field_name'] in queriedForm.fieldConditions:
            del queriedForm.fieldConditions[request.form['field_name']]
            queriedForm.expired=queriedForm.hasExpired()
        return JsonResponse(json.dumps({"condition": False}))

    availableFields = queriedForm.getAvailableNumberTypeFields()
    if not request.form["field_name"] in availableFields:
        return JsonResponse(json.dumps({"condition": False}))

    if not request.form["condition"]:
        if request.form["field_name"] in queriedForm.fieldConditions:
            del queriedForm.fieldConditions[request.form["field_name"]]
            queriedForm.expired = queriedForm.hasExpired()
            queriedForm.save()
        return JsonResponse(json.dumps({'condition': False}))
    
    fieldType=availableFields[request.form['field_name']]['type']
        return JsonResponse(json.dumps({"condition": False}))

    fieldType = availableFields[request.form["field_name"]]["type"]
    if fieldType == "number":
        try:
            queriedForm.fieldConditions[request.form['field_name']]={
                                                            "type": fieldType,
                                                            "condition": int(request.form['condition'])
                                                            }
            queriedForm.expired=queriedForm.hasExpired()
            queriedForm.fieldConditions[request.form["field_name"]] = {
                "type": fieldType,
                "condition": int(request.form["condition"]),
            }
            queriedForm.expired = queriedForm.hasExpired()
            queriedForm.save()
        except:
            return JsonResponse(json.dumps({'condition': False}))
    return JsonResponse(json.dumps({'condition': request.form['condition']}))
            return JsonResponse(json.dumps({"condition": False}))
    return JsonResponse(json.dumps({"condition": request.form["condition"]}))


@form_bp.route('/forms/duplicate/<string:id>', methods=['GET'])
@form_bp.route("/forms/duplicate/<string:id>", methods=["GET"])
@enabled_user_required
#@queriedForm_editor_required
def duplicate_form(id): #, queriedForm):
# @queriedForm_editor_required
def duplicate_form(id):  # , queriedForm):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    clearSessionFormData()
    populateSessionFormData(queriedForm)
    session['slug']=""
    flash(gettext("You can edit the duplicate now"), 'info')
    return render_template('edit-form.html', host_url=g.site.host_url)
    session["slug"] = ""
    flash(gettext("You can edit the duplicate now"), "info")
    return render_template("edit-form.html", host_url=g.site.host_url)


@form_bp.route('/forms/log/list/<string:id>', methods=['GET'])
@form_bp.route("/forms/log/list/<string:id>", methods=["GET"])
@enabled_user_required
def list_log(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        flash(gettext("Can't find that form"), 'warning')
        return redirect(make_url_for('form_bp.my_forms'))
    return render_template('list-log.html', form=queriedForm)

        flash(gettext("Can't find that form"), "warning")
        return redirect(make_url_for("form_bp.my_forms"))
    return render_template("list-log.html", form=queriedForm)


""" Form settings """

@form_bp.route('/form/toggle-enabled/<string:id>', methods=['POST'])

@form_bp.route("/form/toggle-enabled/<string:id>", methods=["POST"])
@enabled_user_required
def toggle_form_enabled(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps())
    enabled=queriedForm.toggleEnabled()
    enabled = queriedForm.toggleEnabled()
    queriedForm.addLog(gettext("Public set to: %s" % enabled))
    return JsonResponse(json.dumps({'enabled': enabled}))
    return JsonResponse(json.dumps({"enabled": enabled}))


@form_bp.route('/form/toggle-shared-entries/<string:id>', methods=['POST'])
@form_bp.route("/form/toggle-shared-entries/<string:id>", methods=["POST"])
@enabled_user_required
def toggle_shared_entries(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps())
    shared=queriedForm.toggleSharedEntries()
    shared = queriedForm.toggleSharedEntries()
    queriedForm.addLog(gettext("Shared entries set to: %s" % shared))
    return JsonResponse(json.dumps({'enabled':shared}))
    return JsonResponse(json.dumps({"enabled": shared}))


@form_bp.route('/form/toggle-restricted-access/<string:id>', methods=['POST'])
@form_bp.route("/form/toggle-restricted-access/<string:id>", methods=["POST"])
@enabled_user_required
def toggle_restricted_access(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps())
    access=queriedForm.toggleRestrictedAccess()
    access = queriedForm.toggleRestrictedAccess()
    queriedForm.addLog(gettext("Restricted access set to: %s" % access))
    return JsonResponse(json.dumps({'restricted':access}))
    return JsonResponse(json.dumps({"restricted": access}))

@form_bp.route('/form/toggle-notification/<string:id>', methods=['POST'])

@form_bp.route("/form/toggle-notification/<string:id>", methods=["POST"])
@enabled_user_required
def toggle_form_notification(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps())
    return JsonResponse(json.dumps({'notification':queriedForm.toggleNotification()}))
    return JsonResponse(json.dumps({"notification": queriedForm.toggleNotification()}))


@form_bp.route('/form/toggle-data-consent/<string:id>', methods=['POST'])
@form_bp.route("/form/toggle-data-consent/<string:id>", methods=["POST"])
@enabled_user_required
def toggle_form_dataconsent(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps())
    dataConsentBool=queriedForm.toggleRequireDataConsent()
    dataConsentBool = queriedForm.toggleRequireDataConsent()
    queriedForm.addLog(gettext("Data protection consent set to: %s" % dataConsentBool))
    return JsonResponse(json.dumps({'consent':dataConsentBool}))
    return JsonResponse(json.dumps({"consent": dataConsentBool}))


@form_bp.route('/form/toggle-expiration-notification/<string:id>', methods=['POST'])
@form_bp.route("/form/toggle-expiration-notification/<string:id>", methods=["POST"])
@enabled_user_required
def toggle_form_expiration_notification(id):
    queriedForm = Form.find(id=id, editor_id=str(g.current_user.id))
    if not queriedForm:
        return JsonResponse(json.dumps())
    return JsonResponse(json.dumps({'notification':queriedForm.toggleExpirationNotification()}))
    
    return JsonResponse(
        json.dumps({"notification": queriedForm.toggleExpirationNotification()})
    )


@form_bp.route('/embed/<string:slug>', methods=['GET', 'POST'])
@form_bp.route("/embed/<string:slug>", methods=["GET", "POST"])
@anon_required
@csrf.exempt
@sanitized_slug_required


@@ 520,74 552,80 @@ def view_embedded_form(slug):
    return view_form(slug=slug, embedded=True)


@form_bp.route('/<string:slug>', methods=['GET', 'POST'])
@form_bp.route("/<string:slug>", methods=["GET", "POST"])
@sanitized_slug_required
def view_form(slug, embedded=False):
    queriedForm = Form.find(slug=slug, hostname=g.site.hostname)
    if not queriedForm:
        if g.current_user:
            flash(gettext("Can't find that form"), 'warning')
            return redirect(make_url_for('form_bp.my_forms'))
            flash(gettext("Can't find that form"), "warning")
            return redirect(make_url_for("form_bp.my_forms"))
        else:
            return render_template('page-not-found.html'), 400
            return render_template("page-not-found.html"), 400
    if not queriedForm.isPublic():
        if g.current_user:
            if queriedForm.expired:
                flash(gettext("That form has expired"), 'warning')
                flash(gettext("That form has expired"), "warning")
            else:
                flash(gettext("That form is not public"), 'warning')
            return redirect(make_url_for('form_bp.my_forms'))
                flash(gettext("That form is not public"), "warning")
            return redirect(make_url_for("form_bp.my_forms"))
        if queriedForm.expired:
            return render_template('form-has-expired.html'), 400
            return render_template("form-has-expired.html"), 400
        else:
            return render_template('page-not-found.html'), 400
            return render_template("page-not-found.html"), 400
    if queriedForm.restrictedAccess and not g.current_user:
        return render_template('page-not-found.html'), 400
        return render_template("page-not-found.html"), 400

    if request.method == 'POST':
        formData=request.form.to_dict(flat=False)
    if request.method == "POST":
        formData = request.form.to_dict(flat=False)
        entry = {}
        entry["created"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        

        for key in formData:
            if key=='csrf_token':
            if key == "csrf_token":
                continue
            value = formData[key]
            if isinstance(value, list): # A checkboxes-group contains multiple values 
                value=', '.join(value) # convert list of values to a string
                key=key.rstrip('[]') # remove tailing '[]' from the name attrib (appended by formbuilder)
            entry[key]=value
            if isinstance(value, list):  # A checkboxes-group contains multiple values
                value = ", ".join(value)  # convert list of values to a string
                key = key.rstrip(
                    "[]"
                )  # remove tailing '[]' from the name attrib (appended by formbuilder)
            entry[key] = value
        queriedForm.entries.append(entry)
        

        if not queriedForm.expired and queriedForm.hasExpired():
            queriedForm.expired=True
            emails=[]
            queriedForm.expired = True
            emails = []
            for editor_id, preferences in queriedForm.editors.items():
                if preferences["notification"]["expiredForm"]:
                    user=User.find(id=editor_id)
                    user = User.find(id=editor_id)
                    if user and user.enabled:
                        emails.append(user.email)
            if emails:

                def sendExpiredFormNotification():
                    smtp.sendExpiredFormNotification(emails, queriedForm)

                thread = Thread(target=sendExpiredFormNotification())
                thread.start()
        queriedForm.save()
            
        emails=[]

        emails = []
        for editor_id, preferences in queriedForm.editors.items():
            if preferences["notification"]["newEntry"]:
                user=User.find(id=editor_id)
                user = User.find(id=editor_id)
                if user and user.enabled:
                    emails.append(user.email)
        if emails:

            def sendEntryNotification():
                data=[]
                data = []
                for field in queriedForm.fieldIndex:
                    if field['name'] in entry:
                        data.append( (field['label'], entry[field['name']]) )
                    if field["name"] in entry:
                        data.append((field["label"], entry[field["name"]]))
                smtp.sendNewFormEntryNotification(emails, data, queriedForm.slug)

            thread = Thread(target=sendEntryNotification())
            thread.start()
        return render_template('thankyou.html', form=queriedForm, embedded=embedded)
    return render_template('view-form.html', form=queriedForm, embedded=embedded)
        return render_template("thankyou.html", form=queriedForm, embedded=embedded)
    return render_template("view-form.html", form=queriedForm, embedded=embedded)

M gngforms/views/main.py => gngforms/views/main.py +30 -27
@@ 22,8 22,8 @@ from flask import request, render_template, redirect, flash
from flask import g, session, Blueprint
from flask_wtf.csrf import CSRFError

#from flask import current_app
#from flask import ctx, current_app, has_app_context, app_ctx_globals_class
# from flask import current_app
# from flask import ctx, current_app, has_app_context, app_ctx_globals_class

from gngforms import app
from gngforms.models import Site, User


@@ 31,11 31,10 @@ from gngforms.utils.wraps import *
from gngforms.utils.utils import *
import gngforms.utils.wtf as wtf

#app = current_app
# app = current_app

main_bp = Blueprint('main_bp', __name__,
                    template_folder='../templates/main')
#main_bp.before_request(shared_functions.before_request)
main_bp = Blueprint("main_bp", __name__, template_folder="../templates/main")
# main_bp.before_request(shared_functions.before_request)


def print_g(string=None):


@@ 44,50 43,54 @@ def print_g(string=None):
    for e in iter(g):
        print(e)


@app.before_request
def before_request():
    g.site=None
    g.current_user=None
    g.isRootUser=False
    g.isAdmin=False
    if request.path[0:7] == '/static':
    g.site = None
    g.current_user = None
    g.isRootUser = False
    g.isAdmin = False
    if request.path[0:7] == "/static":
        return
    g.site=Site.find(hostname=urlparse(request.host_url).hostname)
    if 'user_id' in session and session["user_id"] != None:
        g.current_user=User.find(id=session["user_id"], hostname=g.site.hostname)
    g.site = Site.find(hostname=urlparse(request.host_url).hostname)
    if "user_id" in session and session["user_id"] != None:
        g.current_user = User.find(id=session["user_id"], hostname=g.site.hostname)
        if not g.current_user:
            session.pop("user_id")
            return
        if g.current_user.isRootUser():
            g.isRootUser=True
            g.isRootUser = True
        if g.current_user.isAdmin():
            g.isAdmin=True
            g.isAdmin = True


@app.errorhandler(404)
def page_not_found(error):
    #print('404!!!!')
    return render_template('page-not-found.html', error=error), 400
    # print('404!!!!')
    return render_template("page-not-found.html", error=error), 400


@app.errorhandler(500)
def server_error(error):
    return render_template('server-error.html', error=error), 500
    return render_template("server-error.html", error=error), 500


@app.errorhandler(CSRFError)
def handle_csrf_error(e):
    flash(e.description, 'error')
    #if g.current_user: # throw '_AppCtxGlobals' error. need to investigate
    flash(e.description, "error")
    # if g.current_user: # throw '_AppCtxGlobals' error. need to investigate
    #    #flash(e.description, 'error')
    #    return redirect(make_url_for('main_bp.index'))
    #else:
    return render_template('server-error.html', error=e.description), 500
    # else:
    return render_template("server-error.html", error=e.description), 500


@main_bp.route('/', methods=['GET'])
@main_bp.route("/", methods=["GET"])
def index():
    return render_template('index.html', site=g.site, wtform=wtf.Login())
    return render_template("index.html", site=g.site, wtform=wtf.Login())


@enabled_user_required
@main_bp.route('/test', methods=['GET'])
@main_bp.route("/test", methods=["GET"])
def test():
    return render_template('test.html', sites=[])
    return render_template("test.html", sites=[])

M gngforms/views/site.py => gngforms/views/site.py +99 -80
@@ 30,149 30,168 @@ from gngforms.utils.utils import *
import gngforms.utils.wtf as wtf
import gngforms.utils.email as smtp

site_bp = Blueprint('site_bp', __name__,
                    template_folder='../templates/site')
site_bp = Blueprint("site_bp", __name__, template_folder="../templates/site")


@site_bp.route('/site/save-blurb', methods=['POST'])
@site_bp.route("/site/save-blurb", methods=["POST"])
@admin_required
def save_blurb():
    if 'editor' in request.form:            
        g.site.saveBlurb(request.form['editor'])
        flash(gettext("Text saved OK"), 'success')
    return redirect(make_url_for('main_bp.index'))
    if "editor" in request.form:
        g.site.saveBlurb(request.form["editor"])
        flash(gettext("Text saved OK"), "success")
    return redirect(make_url_for("main_bp.index"))


@site_bp.route('/site/save-personal-data-consent-text', methods=['POST'])
@site_bp.route("/site/save-personal-data-consent-text", methods=["POST"])
@admin_required
def save_data_consent():
    if 'editor' in request.form:            
        g.site.savePersonalDataConsentText(request.form['editor'])
        flash(gettext("Text saved OK"), 'success')
    return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    if "editor" in request.form:
        g.site.savePersonalDataConsentText(request.form["editor"])
        flash(gettext("Text saved OK"), "success")
    return redirect(
        make_url_for("user_bp.user_settings", username=g.current_user.username)
    )


@site_bp.route('/site/change-sitename', methods=['GET', 'POST'])
@site_bp.route("/site/change-sitename", methods=["GET", "POST"])
@admin_required
def change_siteName():
    if request.method == 'POST' and 'sitename' in request.form:
        g.site.siteName=request.form['sitename']
    if request.method == "POST" and "sitename" in request.form:
        g.site.siteName = request.form["sitename"]
        g.site.save()
        flash(gettext("Site name changed OK"), 'success')
        return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    return render_template('change-sitename.html', site=g.site)
        flash(gettext("Site name changed OK"), "success")
        return redirect(
            make_url_for("user_bp.user_settings", username=g.current_user.username)
        )
    return render_template("change-sitename.html", site=g.site)


@site_bp.route('/site/change-favicon', methods=['GET', 'POST'])
@site_bp.route("/site/change-favicon", methods=["GET", "POST"])
@admin_required
def change_site_favicon():
    if request.method == 'POST':
        if not request.files['file']:
            flash(gettext("Required file is missing"), 'warning')
            return render_template('change-site-favicon.html')
        file=request.files['file']
    if request.method == "POST":
        if not request.files["file"]:
            flash(gettext("Required file is missing"), "warning")
            return render_template("change-site-favicon.html")
        file = request.files["file"]
        # need to lower filename
        if len(file.filename) > 4 and file.filename[-4:] == ".png":
            filename="%s_favicon.png" % g.site.hostname
            file.save(os.path.join(app.config['FAVICON_FOLDER'], filename))
            filename = "%s_favicon.png" % g.site.hostname
            file.save(os.path.join(app.config["FAVICON_FOLDER"], filename))
        else:
            flash(gettext("Bad file name. PNG only"), 'warning')
            return render_template('change-site-favicon.html')
        flash(gettext("Favicon changed OK. Refresh with  &lt;F5&gt;"), 'success')
        return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    return render_template('change-site-favicon.html')
            flash(gettext("Bad file name. PNG only"), "warning")
            return render_template("change-site-favicon.html")
        flash(gettext("Favicon changed OK. Refresh with  &lt;F5&gt;"), "success")
        return redirect(
            make_url_for("user_bp.user_settings", username=g.current_user.username)
        )
    return render_template("change-site-favicon.html")


@site_bp.route('/site/reset-favicon', methods=['GET'])
@site_bp.route("/site/reset-favicon", methods=["GET"])
@admin_required
def reset_site_favicon():
    if g.site.deleteFavicon():
        flash(gettext("Favicon reset OK. Refresh with  &lt;F5&gt;"), 'success')
    return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
        flash(gettext("Favicon reset OK. Refresh with  &lt;F5&gt;"), "success")
    return redirect(
        make_url_for("user_bp.user_settings", username=g.current_user.username)
    )


@site_bp.route('/admin/toggle-invitation-only', methods=['POST'])
@site_bp.route("/admin/toggle-invitation-only", methods=["POST"])
@admin_required
def toggle_invitation_only(): 
    return json.dumps({'invite': g.site.toggleInvitationOnly()})
def toggle_invitation_only():
    return json.dumps({"invite": g.site.toggleInvitationOnly()})


@site_bp.route('/site/email/config', methods=['GET', 'POST'])
@site_bp.route("/site/email/config", methods=["GET", "POST"])
@admin_required
def smtp_config():
    wtf_smtp=wtf.smtpConfig(**g.site.smtpConfig)
    wtf_smtp = wtf.smtpConfig(**g.site.smtpConfig)
    if wtf_smtp.validate_on_submit():
        config={}
        config['host'] = wtf_smtp.host.data
        config['port'] = wtf_smtp.port.data
        config['encryption']=wtf_smtp.encryption.data if not wtf_smtp.encryption.data=="None" else ""
        config['user'] = wtf_smtp.user.data
        config['password'] = wtf_smtp.password.data
        config['noreplyAddress'] = wtf_smtp.noreplyAddress.data
        config = {}
        config["host"] = wtf_smtp.host.data
        config["port"] = wtf_smtp.port.data
        config["encryption"] = (
            wtf_smtp.encryption.data if not wtf_smtp.encryption.data == "None" else ""
        )
        config["user"] = wtf_smtp.user.data
        config["password"] = wtf_smtp.password.data
        config["noreplyAddress"] = wtf_smtp.noreplyAddress.data
        g.site.saveSMTPconfig(**config)
        flash(gettext("Confguration saved OK"), 'success')
    wtf_email=wtf.GetEmail()
    return render_template('smtp-config.html', wtf_smtp=wtf_smtp, wtf_email=wtf_email)
        flash(gettext("Confguration saved OK"), "success")
    wtf_email = wtf.GetEmail()
    return render_template("smtp-config.html", wtf_smtp=wtf_smtp, wtf_email=wtf_email)


@site_bp.route('/site/email/test-config', methods=['POST'])
@site_bp.route("/site/email/test-config", methods=["POST"])
@admin_required
def test_smtp():
    wtform=wtf.GetEmail()
    wtform = wtf.GetEmail()
    if wtform.validate():
        if smtp.sendTestEmail(wtform.email.data):
            flash(gettext("SMTP config works!"), 'success')
            flash(gettext("SMTP config works!"), "success")
    else:
        flash("Email not valid", 'warning')
    return redirect(make_url_for('site_bp.smtp_config'))
        flash("Email not valid", "warning")
    return redirect(make_url_for("site_bp.smtp_config"))


@site_bp.route('/admin/sites/edit/<string:hostname>', methods=['GET'])
@site_bp.route("/admin/sites/edit/<string:hostname>", methods=["GET"])
@rootuser_required
def edit_site(hostname):
    queriedSite=Site.find(hostname=hostname)
    return render_template('edit-site.html', site=queriedSite)
    queriedSite = Site.find(hostname=hostname)
    return render_template("edit-site.html", site=queriedSite)


@site_bp.route('/admin/sites/toggle-scheme/<string:hostname>', methods=['POST'])
@site_bp.route("/admin/sites/toggle-scheme/<string:hostname>", methods=["POST"])
@rootuser_required
def toggle_site_scheme(hostname): 
    queriedSite=Site.find(hostname=hostname)
    return json.dumps({'scheme': queriedSite.toggleScheme()})
def toggle_site_scheme(hostname):
    queriedSite = Site.find(hostname=hostname)
    return json.dumps({"scheme": queriedSite.toggleScheme()})


@site_bp.route('/admin/sites/change-port/<string:hostname>/', methods=['POST'])
@site_bp.route('/admin/sites/change-port/<string:hostname>/<string:port>', methods=['POST'])
@site_bp.route("/admin/sites/change-port/<string:hostname>/", methods=["POST"])
@site_bp.route(
    "/admin/sites/change-port/<string:hostname>/<string:port>", methods=["POST"]
)
@rootuser_required
def change_site_port(hostname, port=None):
    queriedSite=Site.find(hostname=hostname)
    queriedSite = Site.find(hostname=hostname)
    if not port:
        queriedSite.port=None
        queriedSite.port = None
    else:
        try:
            int(port)
            queriedSite.port=port
            queriedSite.port = port
        except:
            pass
    queriedSite.save()
    return json.dumps({'port': queriedSite.port})
    
    return json.dumps({"port": queriedSite.port})

@site_bp.route('/admin/sites/delete/<string:hostname>', methods=['GET', 'POST'])

@site_bp.route("/admin/sites/delete/<string:hostname>", methods=["GET", "POST"])
@rootuser_required
def delete_site(hostname):
    queriedSite=Site.find(hostname=hostname)
    queriedSite = Site.find(hostname=hostname)
    if not queriedSite:
        flash(gettext("Site not found"), 'warning')
        return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    if request.method == 'POST' and 'hostname' in request.form:
        if queriedSite.hostname == request.form['hostname']:
        flash(gettext("Site not found"), "warning")
        return redirect(
            make_url_for("user_bp.user_settings", username=g.current_user.username)
        )
    if request.method == "POST" and "hostname" in request.form:
        if queriedSite.hostname == request.form["hostname"]:
            if g.site.hostname == queriedSite.hostname:
                flash(gettext("Cannot delete current site"), 'warning')
                return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
                flash(gettext("Cannot delete current site"), "warning")
                return redirect(
                    make_url_for(
                        "user_bp.user_settings", username=g.current_user.username
                    )
                )
            queriedSite.deleteSite()
            flash(gettext("Deleted %s" % (queriedSite.host_url)), 'success')
            return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))       
            flash(gettext("Deleted %s" % (queriedSite.host_url)), "success")
            return redirect(
                make_url_for("user_bp.user_settings", username=g.current_user.username)
            )
        else:
            flash(gettext("Site name does not match"), 'warning')
    return render_template('delete-site.html', site=queriedSite)
            flash(gettext("Site name does not match"), "warning")
    return render_template("delete-site.html", site=queriedSite)

M gngforms/views/user.py => gngforms/views/user.py +138 -118
@@ 29,248 29,268 @@ from gngforms.utils.utils import *
import gngforms.utils.wtf as wtf
import gngforms.utils.email as smtp

user_bp = Blueprint('user_bp', __name__,
                    template_folder='../templates/user')
user_bp = Blueprint("user_bp", __name__, template_folder="../templates/user")


@user_bp.route('/user/new', methods=['GET', 'POST'])
@user_bp.route('/user/new/<string:token>', methods=['GET', 'POST'])
@user_bp.route("/user/new", methods=["GET", "POST"])
@user_bp.route("/user/new/<string:token>", methods=["GET", "POST"])
@sanitized_token
def new_user(token=None):
    if g.site.invitationOnly and not token:
        return redirect(make_url_for('main_bp.index'))
        return redirect(make_url_for("main_bp.index"))

    invite=None
    invite = None
    if token:
        invite=Invite.find(token=token)
        invite = Invite.find(token=token)
        if not invite:
            flash(gettext("Invitation not found"), 'warning')
            return redirect(make_url_for('main_bp.index'))
            flash(gettext("Invitation not found"), "warning")
            return redirect(make_url_for("main_bp.index"))
        if not isValidToken(invite.token):
            flash(gettext("Your petition has expired"), 'warning')
            flash(gettext("Your petition has expired"), "warning")
            invite.delete()
            return redirect(make_url_for('main_bp.index'))
    
    wtform=wtf.NewUser()
            return redirect(make_url_for("main_bp.index"))

    wtform = wtf.NewUser()
    if wtform.validate_on_submit():
        validatedEmail=False
        adminSettings=User.defaultAdminSettings()        
        validatedEmail = False
        adminSettings = User.defaultAdminSettings()
        if invite:
            if invite.email == wtform.email.data:
                validatedEmail=True
                validatedEmail = True
            if invite.admin == True:
                adminSettings['isAdmin']=True
                adminSettings["isAdmin"] = True
                # the first admin of a new Site needs to config. SMTP before we can send emails.
                # when validatedEmail=False, a validation email fails to be sent because SMTP is not congifured.
                if not g.site.admins:
                    validatedEmail=True
        if wtform.email.data in app.config['ROOT_USERS']:
            adminSettings["isAdmin"]=True
            validatedEmail=True
        
                    validatedEmail = True
        if wtform.email.data in app.config["ROOT_USERS"]:
            adminSettings["isAdmin"] = True
            validatedEmail = True

        newUserData = {
            "username": wtform.username.data,
            "email": wtform.email.data,
            "password_hash": hashPassword(wtform.password.data),
            "language": app.config['DEFAULT_LANGUAGE'],
            "language": app.config["DEFAULT_LANGUAGE"],
            "hostname": g.site.hostname,
            "blocked": False,
            "admin": adminSettings,
            "validatedEmail": validatedEmail,
            "created": datetime.date.today().strftime("%Y-%m-%d"),
            "token": {}
            "token": {},
        }
        user = User.create(newUserData)
        if not user:
            flash(gettext("Opps! An error ocurred when creating the user"), 'error')
            return render_template('new-user.html')
            flash(gettext("Opps! An error ocurred when creating the user"), "error")
            return render_template("new-user.html")
        if invite:
            invite.delete()           
            invite.delete()

        thread = Thread(target=smtp.sendNewUserNotification(user))
        thread.start()
        

        if validatedEmail == True:
            # login an invited user
            session["user_id"]=str(user.id)
            flash(gettext("Welcome!"), 'success')
            return redirect(make_url_for('form_bp.my_forms'))
            session["user_id"] = str(user.id)
            flash(gettext("Welcome!"), "success")
            return redirect(make_url_for("form_bp.my_forms"))
        else:
            user.setToken()
            smtp.sendConfirmEmail(user)
            return render_template('new-user.html', site=g.site, created=True)
            return render_template("new-user.html", site=g.site, created=True)

    if "user_id" in session:
        session.pop("user_id")
    return render_template('new-user.html', wtform=wtform)
    return render_template("new-user.html", wtform=wtform)


@user_bp.route('/user/<string:username>', methods=['GET'])
@user_bp.route("/user/<string:username>", methods=["GET"])
@login_required
def user_settings(username):
    if username != g.current_user.username:
        return redirect(make_url_for('form_bp.my_forms'))
    user=g.current_user
    invites=[]
        return redirect(make_url_for("form_bp.my_forms"))
    user = g.current_user
    invites = []
    if user.isAdmin():
        invites=Invite.findAll()
    sites=None
    installation=None
        invites = Invite.findAll()
    sites = None
    installation = None
    if user.isRootUser():
        sites=Site.findAll()
        installation=Installation.get()
        sites = Site.findAll()
        installation = Installation.get()
    context = {
        'user': user,
        'invites': invites,
        'site': g.site,
        'sites': sites,
        'installation': installation
        "user": user,
        "invites": invites,
        "site": g.site,
        "sites": sites,
        "installation": installation,
    }
    return render_template('user-settings.html', **context)
 
    return render_template("user-settings.html", **context)

@user_bp.route('/user/send-validation', methods=['GET'])

@user_bp.route("/user/send-validation", methods=["GET"])
@login_required
def send_validation_email():   
def send_validation_email():
    g.current_user.setToken(email=g.current_user.email)
    smtp.sendConfirmEmail(g.current_user, g.current_user.email)
    flash(gettext("We've sent an email to %s") % g.current_user.email, 'info')
    return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    
    flash(gettext("We've sent an email to %s") % g.current_user.email, "info")
    return redirect(
        make_url_for("user_bp.user_settings", username=g.current_user.username)
    )


""" Personal user settings """

@user_bp.route('/user/change-language', methods=['GET', 'POST'])

@user_bp.route("/user/change-language", methods=["GET", "POST"])
@login_required
def change_language():
    if request.method == 'POST':
        if 'language' in request.form and request.form['language'] in app.config['LANGUAGES']:
            g.current_user.language=request.form['language']
    if request.method == "POST":
        if (
            "language" in request.form
            and request.form["language"] in app.config["LANGUAGES"]
        ):
            g.current_user.language = request.form["language"]
            g.current_user.save()
            refresh()
            flash(gettext("Language updated OK"), 'success')
            return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    return render_template('change-language.html')
            flash(gettext("Language updated OK"), "success")
            return redirect(
                make_url_for("user_bp.user_settings", username=g.current_user.username)
            )
    return render_template("change-language.html")


@user_bp.route('/user/change-email', methods=['GET', 'POST'])
@user_bp.route("/user/change-email", methods=["GET", "POST"])
@login_required
def change_email():
    wtform=wtf.ChangeEmail()
    wtform = wtf.ChangeEmail()
    if wtform.validate_on_submit():
        g.current_user.setToken(email=wtform.email.data)
        smtp.sendConfirmEmail(g.current_user, wtform.email.data)
        flash(gettext("We've sent an email to %s") % wtform.email.data, 'info')
        return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    return render_template('change-email.html', wtform=wtform)
    
        flash(gettext("We've sent an email to %s") % wtform.email.data, "info")
        return redirect(
            make_url_for("user_bp.user_settings", username=g.current_user.username)
        )
    return render_template("change-email.html", wtform=wtform)


@user_bp.route('/user/reset-password', methods=['GET', 'POST'])
@user_bp.route("/user/reset-password", methods=["GET", "POST"])
@login_required
def reset_password():
    wtform=wtf.ResetPassword()
    wtform = wtf.ResetPassword()
    if wtform.validate_on_submit():
        g.current_user.password_hash=hashPassword(wtform.password.data)
        g.current_user.password_hash = hashPassword(wtform.password.data)
        g.current_user.save()
        flash(gettext("Password changed OK"), 'success')
        return redirect(make_url_for('user_bp.user_settings', username=g.current_user.username))
    return render_template('reset-password.html', wtform=wtform)
        flash(gettext("Password changed OK"), "success")
        return redirect(
            make_url_for("user_bp.user_settings", username=g.current_user.username)
        )
    return render_template("reset-password.html", wtform=wtform)



@user_bp.route('/site/recover-password', methods=['GET', 'POST'])
@user_bp.route('/site/recover-password/<string:token>', methods=['GET'])
@user_bp.route("/site/recover-password", methods=["GET", "POST"])
@user_bp.route("/site/recover-password/<string:token>", methods=["GET"])
@anon_required
@sanitized_token
def recover_password(token=None):
    if token:
        user = User.find(token=token)
        if not user:
            flash(gettext("Couldn't find that token"), 'warning')
            return redirect(make_url_for('main_bp.index'))
            flash(gettext("Couldn't find that token"), "warning")
            return redirect(make_url_for("main_bp.index"))
        if not isValidToken(user.token):
            flash(gettext("Your petition has expired"), 'warning')
            flash(gettext("Your petition has expired"), "warning")
            user.deleteToken()
            return redirect(make_url_for('main_bp.index'))
            return redirect(make_url_for("main_bp.index"))
        if user.blocked:
            user.deleteToken()
            flash(gettext("Your account has been blocked"), 'warning')
            return redirect(make_url_for('main_bp.index'))
            flash(gettext("Your account has been blocked"), "warning")
            return redirect(make_url_for("main_bp.index"))
        user.deleteToken()
        user.validatedEmail=True
        user.validatedEmail = True
        user.save()
        # login the user
        session['user_id']=str(user.id)
        return redirect(make_url_for('user_bp.reset_password'))
    
    wtform=wtf.GetEmail()
        session["user_id"] = str(user.id)
        return redirect(make_url_for("user_bp.reset_password"))

    wtform = wtf.GetEmail()
    if wtform.validate_on_submit():
        user = User.find(email=wtform.email.data, blocked=False)
        if user:
            user.setToken()
            smtp.sendRecoverPassword(user)
        if not user and wtform.email.data in app.config['ROOT_USERS']:
        if not user and wtform.email.data in app.config["ROOT_USERS"]:
            # root_user emails are only good for one account, across all sites.
            if not Installation.isUser(wtform.email.data):
                # auto invite root users
                message="New root user at %s." % g.site.hostname
                invite=Invite.create(g.site.hostname, wtform.email.data, message, True)
                return redirect(make_url_for('user_bp.new_user', token=invite.token['token']))
        flash(gettext("We may have sent you an email"), 'info')
        return redirect(make_url_for('main_bp.index'))
    return render_template('recover-password.html', wtform=wtform)

                message = "New root user at %s." % g.site.hostname
                invite = Invite.create(
                    g.site.hostname, wtform.email.data, message, True
                )
                return redirect(
                    make_url_for("user_bp.new_user", token=invite.token["token"])
                )
        flash(gettext("We may have sent you an email"), "info")
        return redirect(make_url_for("main_bp.index"))
    return render_template("recover-password.html", wtform=wtform)


"""
This may be used to validate a New user's email, or an existing user's Change email request
"""
@user_bp.route('/user/validate-email/<string:token>', methods=['GET'])


@user_bp.route("/user/validate-email/<string:token>", methods=["GET"])
@sanitized_token
def validate_email(token):
    user = User.find(token=token)
    if not user:
        flash(gettext("We couldn't find that petition"), 'warning')
        return redirect(make_url_for('main_bp.index'))
        flash(gettext("We couldn't find that petition"), "warning")
        return redirect(make_url_for("main_bp.index"))
    if not isValidToken(user.token):
        flash(gettext("Your petition has expired"), 'warning')
        flash(gettext("Your petition has expired"), "warning")
        user.deleteToken()
        return redirect(make_url_for('main_bp.index'))
        return redirect(make_url_for("main_bp.index"))
    # On a Change email request, the new email address is saved in the token.
    if 'email' in user.token:
        user.email = user.token['email']
    
    if "email" in user.token:
        user.email = user.token["email"]

    user.deleteToken()
    user.validatedEmail=True
    user.validatedEmail = True
    user.save()
    #login the user
    session['user_id']=str(user.id)
    flash(gettext("Your email address is valid"), 'success')
    return redirect(make_url_for('user_bp.user_settings', username=user.username))
    # login the user
    session["user_id"] = str(user.id)
    flash(gettext("Your email address is valid"), "success")
    return redirect(make_url_for("user_bp.user_settings", username=user.username))


""" Login / Logout """

@user_bp.route('/user/login', methods=['POST'])

@user_bp.route("/user/login", methods=["POST"])
@anon_required
def login():
    wtform=wtf.Login()
    wtform = wtf.Login()
    if wtform.validate():
        user=User.find(hostname=g.site.hostname, username=wtform.username.data, blocked=False)
        user = User.find(
            hostname=g.site.hostname, username=wtform.username.data, blocked=False
        )
        if user and user.verifyPassword(wtform.password.data):
            session["user_id"]=str(user.id)
            session["user_id"] = str(user.id)
            if not user.validatedEmail:
                return redirect(make_url_for('user_bp.user_settings', username=user.username))
                return redirect(
                    make_url_for("user_bp.user_settings", username=user.username)
                )
            else:
                return redirect(make_url_for('form_bp.my_forms'))
                return redirect(make_url_for("form_bp.my_forms"))
    if "user_id" in session:
        session.pop("user_id")
    flash(gettext("Bad credentials"), 'warning')
    return redirect(make_url_for('main_bp.index'))
    flash(gettext("Bad credentials"), "warning")
    return redirect(make_url_for("main_bp.index"))


@user_bp.route('/user/logout', methods=['GET', 'POST'])
@user_bp.route("/user/logout", methods=["GET", "POST"])
@login_required
def logout():
    session.pop("user_id")
    return redirect(make_url_for('main_bp.index'))
    return redirect(make_url_for("main_bp.index"))

M gunicorn.py => gunicorn.py +5 -5
@@ 1,6 1,6 @@
pythonpath = '/opt/GNGforms'
command = './venv/bin/gunicorn'
#If your nginx proxy is on another machine, try 0.0.0.0 instead
bind = '127.0.0.1:5000'
pythonpath = "/opt/GNGforms"
command = "./venv/bin/gunicorn"
# If your nginx proxy is on another machine, try 0.0.0.0 instead
bind = "127.0.0.1:5000"
workers = 3
user = 'www-data'
user = "www-data"

M migrate.py => migrate.py +7 -4
@@ 3,18 3,21 @@ from gngforms.models import Installation
from gngforms.utils import migrate


installation=Installation.get()
installation = Installation.get()

print("Schema version before upgrade is {}".format(installation.schemaVersion))
if not installation.isSchemaUpToDate():
    updated=installation.updateSchema()
    updated = installation.updateSchema()
    if updated:
        print("Updated database schema to version %s" % installation.schemaVersion)
        print("OK")
    else:
        print("Error")
        print("Current database schema version is {} but should be {}".format(installation.schemaVersion,
                                                                              app.config['SCHEMA_VERSION']))
        print(
            "Current database schema version is {} but should be {}".format(
                installation.schemaVersion, app.config["SCHEMA_VERSION"]
            )
        )

else:
    print("Database schema is already up to date")

M run.py => run.py +2 -1
@@ 20,4 20,5 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
# use for development only

from gngforms import app
app.run(host='127.0.0.1', port=5000, debug=True)

app.run(host="127.0.0.1", port=5000, debug=True)