Got emails working, added some form fields, added pdf generation, some refactoring

master
msinkec 3 years ago
parent 6174cf1c3e
commit 36dbe487ed

@ -1,7 +1,10 @@
FROM python:3.9
COPY app.py /usr/src/portal-webapp/
COPY config.ini /usr/src/portal-webapp/
COPY templates /usr/src/portal-webapp/templates
COPY static /usr/src/portal-webapp/static
COPY contract/ /usr/src/portal-webapp/contract
WORKDIR /usr/src/portal-webapp
RUN pip install --no-cache-dir flask flask-dropzone gunicorn

277
app.py

@ -2,31 +2,110 @@ import os
import re
import hashlib
import time
import ssl
import configparser
from pathlib import Path
from flask import Flask, render_template, request
from flask_dropzone import Dropzone
from smtplib import SMTP_SSL as SMTP
enabled_filetypes = ['txt', 'csv', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
regex_email = re.compile('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$')
basedir = Path(__file__).resolve().parent
upload_dir = basedir / 'uploads'
if not upload_dir.exists:
upload_dir.mkdir()
import imaplib
from smtplib import SMTP_SSL
import email
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
import pdfkit
from jinja2 import Environment, FileSystemLoader
class ContractCreator:
def __init__(self):
template_loader = FileSystemLoader(searchpath="./")
template_env = Environment(loader=template_loader)
self.template = template_env.get_template('contract/template.html')
self.pdfkit_options = {
'page-size': 'A4',
'margin-top': '0.75in',
'margin-right': '0.75in',
'margin-bottom': '0.75in',
'margin-left': '0.75in',
'encoding': "UTF-8",
'custom-header' : [
('Accept-Encoding', 'gzip')
]
}
def fill_template(self, **kwargs):
return self.template.render(**kwargs)
def create_pdf(self, out_f, fields_dict):
html_str = self.fill_template(**fields_dict)
pdfkit.from_string(html_str, out_f, options=self.pdfkit_options)
ENABLED_FILETYPES = ['txt', 'csv', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
REGEX_EMAIL = re.compile('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$')
######################
# Load configuration #
######################
config = configparser.ConfigParser()
config.read('config.ini')
config = config['DEFAULT']
MAIL_HOST = config['MAIL_HOST']
MAIL_LOGIN = config['MAIL_LOGIN']
MAIL_PASS = config['MAIL_PASS']
SMTP_PORT = int(config['SMTP_PORT'])
IMAP_PORT = int(config['IMAP_PORT'])
MAX_UPLOAD_SIZE = int(config['MAX_UPLOAD_SIZE']) # Bytes
CONTRACT_CLIENT_CONTACT = config['CONTRACT_CLIENT_CONTACT']
if 'BASE_DIR' in config:
BASE_DIR = Path(config['BASE_DIR'])
else:
BASE_DIR = Path(__file__).resolve().parent
# Override configs with environment variables, if set
if 'PORTALDS4DS1_MAIL_HOST' in os.environ:
MAIL_HOST = os.environ('PORTALDS4DS1_MAIL_HOST')
if 'PORTALDS4DS1_MAIL_LOGIN' in os.environ:
MAIL_LOGIN = os.environ('PORTALDS4DS1_MAIL_LOGIN')
if 'PORTALDS4DS1_MAIL_PASS' in os.environ:
MAIL_PASS = os.environ('PORTALDS4DS1_MAIL_PASS')
if 'PORTALDS4DS1_SMTP_PORT' in os.environ:
SMTP_PORT = int(os.environ('PORTALDS4DS1_SMTP_PORT'))
if 'PORTALDS4DS1_IMAP_PORT' in os.environ:
IMAP_PORT = int(os.environ('PORTALDS4DS1_IMAP_PORT'))
if 'MAX_UPLOAD_SIZE' in os.environ:
MAX_UPLOAD_SIZE = int(os.environ('PORTALDS4DS1_MAX_UPLOAD_SIZE'))
if 'CONTRACT_CLIENT_CONTACT' in os.environ:
CONTRACT_CLIENT_CONTACT = os.environ('PORTALDS4DS1_CONTRACT_CLIENT_CONTACT')
UPLOAD_DIR = BASE_DIR / 'uploads'
if not UPLOAD_DIR.exists:
UPLOAD_DIR.mkdir(parents=True)
######################
app = Flask(__name__)
app.config.update(
UPLOADED_PATH = upload_dir,
MAX_CONTENT_LENGTH = 1000000000, # 1GB
UPLOADED_PATH = UPLOAD_DIR,
MAX_CONTENT_LENGTH = MAX_UPLOAD_SIZE,
TEMPLATES_AUTO_RELOAD = True
)
dropzone = Dropzone(app)
contract_creator = ContractCreator()
@app.route('/')
def index():
@ -49,28 +128,75 @@ def handle_upload():
if err:
return err, 400
file_hashes = create_file_hashes(files)
store_metadata(request.form, file_hashes)
store_datafiles(files, file_hashes)
send_confirm_mail(request.form.get('email'))
upload_metadata = get_upload_metadata(request)
contract_file_name = generate_contract_pdf(upload_metadata)
# Add contract_file_name to metadata TODO: move somewhere else
upload_metadata['contract'] = contract_file_name
store_datafiles(files, upload_metadata)
store_metadata(upload_metadata)
send_confirm_mail(upload_metadata)
return 'Uspešno ste oddali datotek(e). Št. datotek: {}'.format(len(files))
def get_upload_metadata(request):
upload_metadata = dict()
file_hashes = create_file_hashes(request.files)
form_data = request.form.copy()
upload_timestamp = int(time.time())
upload_id = create_upload_id(form_data, upload_timestamp, file_hashes)
upload_metadata['form_data'] = form_data
upload_metadata['upload_id'] = upload_id
upload_metadata['timestamp'] = upload_timestamp
upload_metadata['file_hashes'] = file_hashes
return upload_metadata
def check_suffixes(files):
for key, f in files.items():
if key.startswith('file'):
suffix = f.filename.split('.')[-1]
if suffix not in enabled_filetypes:
if suffix not in ENABLED_FILETYPES:
return 'Datoteka "{}" ni pravilnega formata.'.format(f.filename)
return None
def get_subdir(dir_name):
subdir = app.config['UPLOADED_PATH'] / dir_name
if not subdir.exists():
subdir.mkdir()
return subdir
def create_upload_id(form_data, upload_timestamp, file_hashes):
tip = form_data.get('tip')
ime = form_data.get('ime')
podjetje = form_data.get('podjetje')
naslov = form_data.get('naslov')
posta = form_data.get('posta')
email = form_data.get('email')
telefon = form_data.get('telefon')
# This hash serves as an unique identifier for the whole upload.
metahash = hashlib.md5((tip+ime+podjetje+naslov+posta+email+telefon).encode())
# Include file hashes to avoid metafile name collisions if they have the same form values,
# but different data files. Sort hashes first so upload order doesn't matter.
sorted_f_hashes = list(file_hashes.values())
sorted_f_hashes.sort()
metahash.update(''.join(sorted_f_hashes).encode())
metahash = metahash.hexdigest()
return metahash
def check_form(form):
tip = form.get('tip')
ime = form.get('ime')
podjetje = form.get('podjetje')
naslov = form.get('naslov')
posta = form.get('posta')
email = form.get('email')
telefon = form.get('telefon')
@ -81,16 +207,22 @@ def check_form(form):
return 'Predolgo ime.'
if len(podjetje) > 100:
return 'Predolgo ime institucije'
return 'Predolgo ime institucije.'
if len(email) > 100:
return 'Predolgi email naslov'
elif not re.search(regex_email, email):
elif not re.search(REGEX_EMAIL, email):
return 'Email napačnega formata.'
if len(telefon) > 100:
return 'Predolga telefonska št.'
if len(naslov) > 100:
return 'Predolg naslov.'
if len(posta) > 100:
return 'Predolga pošta'
return None
@ -105,42 +237,35 @@ def create_file_hashes(files):
return res
def store_metadata(form, file_hashes):
base = app.config['UPLOADED_PATH'] / 'meta'
if not base.exists():
base.mkdir()
def store_metadata(upload_metadata):
base = get_subdir('meta')
tip = form.get('tip')
ime = form.get('ime')
podjetje = form.get('podjetje')
email = form.get('email')
telefon = form.get('telefon')
timestamp = upload_metadata['timestamp']
upload_id = upload_metadata['upload_id']
form_data = upload_metadata['form_data']
email = form_data['email']
file_hashes = upload_metadata['file_hashes']
contract = upload_metadata['contract']
filename = str(timestamp) + '-' + email + '-' + upload_id + '.meta'
# This hash serves as an identifier for the whole upload.
metahash = hashlib.md5((tip+ime+podjetje+email+telefon).encode())
# Include file hashes to avoid metafile name collisions if they have the same form values,
# but different data files. Sort hashes first so upload order doesn't matter.
sorted_f_hashes = list(file_hashes.values())
sorted_f_hashes.sort()
metahash.update(''.join(sorted_f_hashes).encode())
metahash = metahash.hexdigest()
timestamp = int(time.time())
filename = str(timestamp) + '-' + email + '-' + metahash + '.meta'
path = base / filename
with path.open('w') as f:
f.write('tip=' + tip)
f.write('\nime=' + ime)
f.write('\npodjetje=' + podjetje)
f.write('\nemail=' + email)
f.write('tip=' + form_data['tip'])
f.write('\nime=' + form_data['ime'])
f.write('\npodjetje=' + form_data['podjetje'])
f.write('\nnaslov=' + form_data['naslov'])
f.write('\nposta=' + form_data['posta'])
f.write('\nemail=' + form_data['email'])
f.write('\ndatoteke=' + str(sorted_f_hashes))
f.write('\npogodba=' + contract)
def store_datafiles(files, file_hashes):
base = app.config['UPLOADED_PATH'] / 'files'
if not base.exists():
base.mkdir()
def store_datafiles(files, upload_metadata):
base = get_subdir('files')
file_hashes = upload_metadata['file_hashes']
for key, f in files.items():
if key.startswith('file'):
@ -149,19 +274,55 @@ def store_datafiles(files, file_hashes):
path.mkdir()
f.save(path / f.filename)
def send_confirm_mail(email):
#msg = MIMEText(content, text_subtype)
#msg['Subject'] = "TEST"
#msg['From'] = sender # some SMTP servers will do this automatically, not all
#conn = SMTP(SMTPserver)
#conn.set_debuglevel(False)
#conn.login(USERNAME, PASSWORD)
#try:
# conn.sendmail(sender, destination, msg.as_string())
#finally:
# conn.quit()
pass
def generate_contract_pdf(upload_metadata):
base = get_subdir('contracts')
contract_file_name = upload_metadata['upload_id'] + '.pdf'
form_data = upload_metadata['form_data']
data = {
'ime_priimek': form_data['ime'],
'naslov': form_data['naslov'],
'posta': form_data['posta'],
'kontakt_narocnik': CONTRACT_CLIENT_CONTACT,
'kontakt_imetnikpravic': form_data['ime']
}
contract_creator.create_pdf(base / contract_file_name, data)
return contract_file_name
def send_confirm_mail(upload_metadata):
body = 'Usprešno ste oddali besedila. V prilogi vam pošiljamo pogodbo.'
message = MIMEMultipart()
message['From'] = MAIL_LOGIN
message['To'] = upload_metadata['form_data']['email']
message['Subject'] = 'Pogodba za oddana besedila ' + upload_metadata['upload_id']
message.attach(MIMEText(body, "plain"))
contracts_dir = get_subdir('contracts')
base_name = upload_metadata['contract']
contract_file = contracts_dir / base_name
with open(contract_file, "rb") as f:
part = MIMEApplication(
f.read(),
Name = base_name
)
part['Content-Disposition'] = 'attachment; filename="%s"' % base_name
message.attach(part)
text = message.as_string()
# Create a secure SSL context
context = ssl.create_default_context()
with SMTP_SSL(MAIL_HOST, SMTP_PORT, context=context) as server:
server.login(MAIL_LOGIN, MAIL_PASS)
server.sendmail(message['From'], message['To'], text)
# Save copy of sent mail in Sent mailbox
imap = imaplib.IMAP4_SSL(MAIL_HOST, IMAP_PORT)
imap.login(MAIL_LOGIN, MAIL_PASS)
imap.append('Sent', '\\Seen', imaplib.Time2Internaldate(time.time()), text.encode('utf8'))
imap.logout()
if __name__ == '__main__':

@ -0,0 +1,9 @@
[DEFAULT]
MAIL_HOST=posta.cjvt.si
MAIL_LOGIN=oddaja-besedil@cjvt.si
MAIL_PASS=randompass123
SMTP_PORT=465
IMAP_PORT=993
MAX_UPLOAD_SIZE=1000000000
BASE_DIR=./
CONTRACT_CLIENT_CONTACT=Testko Tester

Binary file not shown.

@ -0,0 +1,43 @@
import pdfkit
from jinja2 import Environment, FileSystemLoader
class ContractCreator:
def __init__(self):
template_loader = FileSystemLoader(searchpath="./")
template_env = Environment(loader=template_loader)
self.template = template_env.get_template('template.html')
self.pdfkit_options = {
'page-size': 'A4',
'margin-top': '0.75in',
'margin-right': '0.75in',
'margin-bottom': '0.75in',
'margin-left': '0.75in',
'encoding': "UTF-8",
'custom-header' : [
('Accept-Encoding', 'gzip')
]
}
def fill_template(self, **kwargs):
return self.template.render(**kwargs)
def create_pdf(self, out_f, fields_dict):
html_str = self.fill_template(**fields_dict)
pdfkit.from_string(html_str, out_f, options=self.pdfkit_options)
if __name__ == '__main__':
test_data = {
'ime_priimek': 'Testko Tester',
'naslov': 'Testovci 10',
'posta': '1123',
'kontakt_narocnik': 'Testica Testkovič',
'kontakt_imetnikpravic': 'Testko Tester',
'date': '16.2.2021'
}
contract_creator = ContractCreator()
contract_creator.create_pdf('out.pdf', test_data)

@ -0,0 +1,202 @@
<!DOCTYPE html>
<html>
<head>
<style>
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
table {
width: 80%;
margin: 0 auto;
}
</style>
</head>
<body>
<p><b>Univerza v Ljubljani<br>
Kongresni trg 12<br>
1000 Ljubljana<br>
matična številka: 5085063000<br>
davčna številka: 54162513<br></b>
<br>
(v nadaljevanju <b>naročnik</b>)<br>
<br>
in<br>
<br>
<b>
{{ime_priimek}}<br>
{{naslov}}<br>
{{posta}}<br>
</b>
<br>
(v nadaljevanju <b>imetnik pravic</b>)<br>
<br>
v nadaljevanju skupaj <b>stranki</b><br>
<br>
sklepata naslednjo<br>
</b>
</p>
<h2 style="text-align: center;">POGODBO O PRENOSU AVTORSKIH PRAVIC</h2>
<h3 style="text-align: center;">UVODNE DOLOČBE</h3>
<h4 style="text-align: center;"><b>1. člen</b></h4>
<p>1.1. Stranki uvodoma ugotavljata, da naročnik izvaja projekt Razvoj slovenščine v digitalnem
okolju RSDO (v nadaljevanju projekt RSDO), ki je bil na javnem razpisu Razvoj slovenščine v
digitalnem okolju jezikovni viri in tehnologije (JR-ESRR-Razvoj slovenščine v digitalnem
okolju), objavljenem v Uradnem listu RS št. 70/19 dne 29. 11. 2019, sprejet v sofinanciranje
in katerega vsebina je razvidna s spletnih strani https://slovenscina.eu.</p>
<p>1.2. Stranki uvodoma ugotavljata, da bo naročnik v okviru projekta RSDO:
- izdelal osrednjo digitalno slovarsko bazo, ki združuje različne tipe jezikovnih podatkov o
slovenščini v odprtem dostopu,
- izdelal terminološki portal z integriranim iskalnikom po slovenskih terminoloških virih, zlasti
terminoloških slovarjih,
- izdelal korpus prevodov po različnih domenah za učenje strojnega prevajalnika za jezikovni
par angleščina-slovenščina in slovenščina-angleščina.</p>
<p>1.3. Stranki uvodoma ugotavljata, da bo naročnik pri projektu RSDO za vse zgoraj opisane
namene zbiral in uporabil besedilne vire, ki so navedeni v prilogi k tej pogodbi in ki so lahko
avtorska dela ali drugi predmeti varstva v skladu z Zakonom o avtorski in sorodnih pravicah
(Uradni list RS, št. 16/07 uradno prečiščeno besedilo, 68/08, 110/13, 56/15, 63/16 ZKUASP
in 59/19; ZASP) in na katerih ima imetnik pravic avtorske, avtorski sorodne ali druge pravice v
skladu z ZASP (v nadaljevanju avtorska dela).</p>
<p>1.4. Stranki ugotavljata, da bodo avtorska dela in vse njihove morebitne spremembe in
predelave, ter zbirke podatkov, ki bodo med izvajanjem projekta RSDO nastale, javno
dostopni pod pogoji prostih licenc (npr. CC BY-SA) in bodo na voljo za nekomercialen in
komercialen razvoj tehnologij, za raziskave in za druge raziskovalne namene
posameznikom, raziskovalnim in izobraževalnim institucijam, neprofitnim organizacijam,
državnim organom, organizacijam z javnimi pooblastili in gospodarskim družbam v Sloveniji
in tujini.</p>
<h3 style="text-align: center;">PREDMET POGODBE</h3>
<h4 style="text-align: center;"><b>2. člen</b></h4>
<p>2.1. Predmet pogodbe so vsa avtorska dela imetnika pravic, ki so navedena v prilogi k tej
pogodbi.</p>
<p>2.2. S podpisom te pogodbe imetnik avtorskih pravic na naročnika prenaša avtorske pravice
na avtorskih delih na način in v obsegu, kakor je navedeno v 3. členu te pogodbe.</p>
<h3 style="text-align: center;">PRENOS AVTORSKIH PRAVIC</h3>
<h4 style="text-align: center;"><b>3. člen</b></h4>
<p>3.1. S podpisom te pogodbe imetnik pravic na avtorskih delih, ki so predmet te pogodbe, na
naročnika neizključno, brez časovnih in teritorialnih omejitev prenaša vse materialne avtorske
pravice, avtorski sorodne pravice in druge pravice avtorja v skladu z ZASP, zlasti pravico
reproduciranja (23. člen ZASP), distribuiranja (24. člena ZASP), dajanja v najem (25. člen ZASP),
priobčitve javnosti (26. do 32.a člen ZASP), vključno s pravico dajanja na voljo javnosti (32.a
člen ZASP) in pravico predelave (33. člen ZASP).</p>
<p>3.2. S podpisom te pogodbe imetnik pravic izrecno soglaša, da naročnik pravice iz točke 3.1.
prenaša naprej na tretje osebe brez omejitev.</p>
<h3 style="text-align: center;">JAMČEVANJE IMETNIKA PRAVIC</h3>
<h4 style="text-align: center;"><b>4. člen</b></h4>
<p>4.1. S podpisom te pogodbe imetnik pravic jamči, da je na avtorskih delih, ki so predmet te
pogodbe, imetnik vseh avtorskih pravic, avtorski sorodnih pravic in drugih pravic avtorja v
skladu z ZASP, ki so potrebne za prenos pravic po tej pogodbi, in da na avtorskih delih ne
obstajajo pravice tretjih oseb, ki bi naročniku preprečevale njihovo uporabo.</p>
<p>4.2. Določbe te pogodbe ne vplivajo na prenos moralnih avtorskih pravic, ki so v skladu z
določbami ZASP neprenosljive.</p>
<h3 style="text-align: center;">OSEBNI PODATKI</h3>
<h4 style="text-align: center;"><b>5. člen</b></h4>
<p>6.1. Stranki se zavezujeta, da bosta vse morebitne osebne podatke, ki jih bosta obdelovali za
namene izvajanja te pogodbe, obdelovali na način, da bosta upoštevali vse veljavne predpise
o varstvu osebnih podatkov in da bosta posameznikom, na katere se osebni podatki nanašajo,
zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatkov.<p>
<h3 style="text-align: center;">KONTAKTNE OSEBE</h3>
<h4 style="text-align: center;"><b>6. člen</b></h4>
<p>7.1 Kontaktna oseba za izvedbo te pogodbe na strani naročnika je {{kontakt_narocnik}}.</p>
<p>7.2. Kontaktna oseba za izvedbo te pogodbe na strani imetnika pravic {{kontakt_imetnikpravic}}.</p>
<h3 style="text-align: center;">KONČNE DOLOČBE</h3>
<h4 style="text-align: center;"><b>7. člen</b></h4>
<p>8.1. Če je katerakoli določba te pogodbe nična, ostanejo druga določila te pogodbe v veljavi.</p>
<h4 style="text-align: center;"><b>8. člen</b></h4>
<p>9.1. Za razmerja v zvezi s to pogodbo se uporabljajo pravni predpisi Republike Slovenije.</p>
<p>9.2. Spore iz te pogodbe bosta stranki reševali po mirni poti. V primeru, da mirna rešitev ne
bo mogoča, je za vse spore v zvezi s to pogodbo pristojno sodišče v Ljubljani.</p>
<h4 style="text-align: center;"><b>9. člen</b></h4>
<p>10.1. Ta pogodba nadomešča vsa predhodna pogajanja, ponudbe in druge dogovore med
strankama.</p>
<p>10.2. Ta pogodba je sestavljena v [dveh] istovetnih izvodih, od katerih prejme vsaka stranka
po enega.</p>
<p>10.3. Pogodbeni stranki s podpisom potrjujeta veljavnost te pogodbe.</p>
<p><b>V Ljubljani, dne {{date}}</b></p>
<br>
<br>
<div>
<div style="float: left;">
<p>Naročnik:</p>
<br><br>
<p>_____________________</p>
</div>
<div style="float: right; margin-right: 80px;">
<p>Imetnik pravic:</p>
<br><br>
<p>_____________________</p>
</div>
</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<p style="text-align: center;"><b>Priloga k pogodbi o prenosu avtorskih pravic: seznam avtorskih del, ki so predmet pogodbe</b></p>
<div style="width: 100%;">
<table>
<tr>
<td style="text-align: center;"><b>Ime, naslov ali oznaka dela</b></td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
</table>
</div>
</body>
</html>

@ -78,7 +78,7 @@ label {
background: #006cb7;
border-radius: 29px;
border: 0px;
top: 430px;
top: 530px;
font-family: Roboto;
font-style: normal;
@ -164,7 +164,7 @@ input {
margin-bottom: 16px;
min-height: 100px;
max-height: 500px;
top: -430px;
top: -530px;
overflow-x: hidden;
overflow-y: auto;
}
@ -172,7 +172,7 @@ input {
#rect1 {
position: relative;
width: 388px;
height: 531px;
height: 631px;
background: #f5f5f5;
box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.25);

@ -8,7 +8,7 @@
{{ dropzone.style('position: absolute;
top: -0.5px;
width: 388px;
height: 532px;
height: 632px;
left: 385px;
background: linear-gradient(198.62deg, rgba(255, 255, 255, 0.49) -1.62%, rgba(255, 255, 255, 0.73) -1.61%, rgba(255, 255, 255, 0.41) 79.34%);
box-shadow: 20px 4px 40px rgba(0, 0, 0, 0.25);
@ -43,6 +43,12 @@
<label for="podjetje">Podjetje / institucija:</label>
<input type="text" id="podjetje" name="podjetje"/>
<label for="naslov">Naslov:</label>
<input type="text" id="naslov" name="naslov"/>
<label for="posta">Pošta:</label>
<input type="text" id="posta" name="posta"/>
<label for="email">* Email:</label>
<input type="text" id="email" name="email" required="required"/>

Loading…
Cancel
Save