From 36dbe487ede57c24e78d80c7c32c25dff8243a5b Mon Sep 17 00:00:00 2001 From: msinkec Date: Fri, 19 Feb 2021 19:03:01 +0100 Subject: [PATCH] Got emails working, added some form fields, added pdf generation, some refactoring --- Dockerfile | 3 + app.py | 277 ++++++++++++++++++++++++++++++++--------- config.ini | 9 ++ contract/out.pdf | Bin 0 -> 28387 bytes contract/pdf.py | 43 +++++++ contract/template.html | 202 ++++++++++++++++++++++++++++++ static/style.css | 6 +- templates/index.html | 8 +- uploads/.gitkeep | 0 9 files changed, 486 insertions(+), 62 deletions(-) create mode 100644 config.ini create mode 100644 contract/out.pdf create mode 100644 contract/pdf.py create mode 100644 contract/template.html delete mode 100644 uploads/.gitkeep diff --git a/Dockerfile b/Dockerfile index b0fd78d..0509ad2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/app.py b/app.py index 81da579..da32e53 100644 --- a/app.py +++ b/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__': diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..d340504 --- /dev/null +++ b/config.ini @@ -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 diff --git a/contract/out.pdf b/contract/out.pdf new file mode 100644 index 0000000000000000000000000000000000000000..34c3b9ab5d03f1de9ab477caf8ee825b69dd7192 GIT binary patch literal 28387 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 U=cx~W3pwC(CDGAIC`e-d7wX}N`Tzg` literal 0 HcmV?d00001 diff --git a/contract/pdf.py b/contract/pdf.py new file mode 100644 index 0000000..c8b55e8 --- /dev/null +++ b/contract/pdf.py @@ -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) diff --git a/contract/template.html b/contract/template.html new file mode 100644 index 0000000..1bf585e --- /dev/null +++ b/contract/template.html @@ -0,0 +1,202 @@ + + + + + + +

Univerza v Ljubljani
+ Kongresni trg 12
+ 1000 Ljubljana
+ matična številka: 5085063000
+ davčna številka: 54162513
+
+ (v nadaljevanju naročnik)
+
+ in
+
+ + {{ime_priimek}}
+ {{naslov}}
+ {{posta}}
+
+
+ (v nadaljevanju imetnik pravic)
+
+ v nadaljevanju skupaj stranki
+
+ sklepata naslednjo
+ +

+ +

POGODBO O PRENOSU AVTORSKIH PRAVIC

+ +

UVODNE DOLOČBE

+

1. člen

+ +

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.

+ +

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.

+

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).

+ +

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.

+ +

PREDMET POGODBE

+

2. člen

+ +

2.1. Predmet pogodbe so vsa avtorska dela imetnika pravic, ki so navedena v prilogi k tej + pogodbi.

+ +

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.

+ +

PRENOS AVTORSKIH PRAVIC

+

3. člen

+ +

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).

+ +

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.

+ +

JAMČEVANJE IMETNIKA PRAVIC

+ +

4. člen

+ +

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.

+ +

4.2. Določbe te pogodbe ne vplivajo na prenos moralnih avtorskih pravic, ki so v skladu z + določbami ZASP neprenosljive.

+ +

OSEBNI PODATKI

+

5. člen

+ +

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.

+ +

KONTAKTNE OSEBE

+

6. člen

+ +

7.1 Kontaktna oseba za izvedbo te pogodbe na strani naročnika je {{kontakt_narocnik}}.

+

7.2. Kontaktna oseba za izvedbo te pogodbe na strani imetnika pravic {{kontakt_imetnikpravic}}.

+ +

KONČNE DOLOČBE

+

7. člen

+ +

8.1. Če je katerakoli določba te pogodbe nična, ostanejo druga določila te pogodbe v veljavi.

+ +

8. člen

+ +

9.1. Za razmerja v zvezi s to pogodbo se uporabljajo pravni predpisi Republike Slovenije.

+

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.

+ +

9. člen

+ +

10.1. Ta pogodba nadomešča vsa predhodna pogajanja, ponudbe in druge dogovore med + strankama.

+

10.2. Ta pogodba je sestavljena v [dveh] istovetnih izvodih, od katerih prejme vsaka stranka + po enega.

+

10.3. Pogodbeni stranki s podpisom potrjujeta veljavnost te pogodbe.

+ +

V Ljubljani, dne {{date}}

+ +
+
+ +
+
+

Naročnik:

+

+

_____________________

+
+
+

Imetnik pravic:

+

+

_____________________

+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +

Priloga k pogodbi o prenosu avtorskih pravic: seznam avtorskih del, ki so predmet pogodbe

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Ime, naslov ali oznaka dela
 
 
 
 
 
 
 
+
+ + diff --git a/static/style.css b/static/style.css index 7197625..2adeb80 100644 --- a/static/style.css +++ b/static/style.css @@ -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); diff --git a/templates/index.html b/templates/index.html index 03993e3..f175766 100644 --- a/templates/index.html +++ b/templates/index.html @@ -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 @@ + + + + + + diff --git a/uploads/.gitkeep b/uploads/.gitkeep deleted file mode 100644 index e69de29..0000000