Progress on solar implementation, switched to storing metadata in SQL database...

This commit is contained in:
msinkec 2021-03-24 11:05:36 +01:00
parent 13aad3a61a
commit 17f727f620
17 changed files with 862 additions and 248 deletions

View File

@ -1,16 +1,18 @@
FROM python:3.9 FROM python:3.9
COPY entrypoint.sh /usr/src/portal-webapp/
COPY app.py /usr/src/portal-webapp/ COPY app.py /usr/src/portal-webapp/
COPY config.ini /usr/src/portal-webapp/ COPY config.ini /usr/src/portal-webapp/
COPY templates /usr/src/portal-webapp/templates COPY templates /usr/src/portal-webapp/templates
COPY static /usr/src/portal-webapp/static COPY static /usr/src/portal-webapp/static
COPY contract/ /usr/src/portal-webapp/contract COPY contract/ /usr/src/portal-webapp/contract
COPY portal/ /usr/src/portal-webapp/portal COPY portal/ /usr/src/portal-webapp/portal
COPY migrations// /usr/src/portal-webapp/migrations
WORKDIR /usr/src/portal-webapp WORKDIR /usr/src/portal-webapp
RUN apt-get update && apt-get -y install wkhtmltopdf && \ RUN apt-get update && apt-get -y install wkhtmltopdf && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir pdfkit flask flask-dropzone gunicorn pdfkit RUN pip3 install --no-cache-dir pdfkit flask flask-dropzone flask-log-request-id Flask-SQLAlchemy alembic flask-migrate Flask-script psycopg2 gunicorn pdfkit
CMD ["gunicorn", "--bind", "0.0.0.0:80", "-w", "1", "--access-logfile", "-", "app:app"] ENTRYPOINT ["./entrypoint.sh"]

1
README.md Normal file
View File

@ -0,0 +1 @@
## A web application for contribution of textual data files

161
app.py
View File

@ -1,15 +1,25 @@
import logging
import os import os
import configparser import configparser
import re
from pathlib import Path from pathlib import Path
from flask import Flask, render_template, request from flask import Flask, render_template, request
from flask_dropzone import Dropzone from flask_dropzone import Dropzone
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from portal.model import db
import portal.base import portal.base
# TODO: Put all the stuff in base.py into a class, so it can have a state of it's own, to avoid passing a bunch of arguments at each function call.
ENABLED_FILETYPES = ['txt', 'csv', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'xml', 'mxliff', 'tmx']
REGEX_EMAIL = re.compile('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$')
# TODO: make logging level configurable
logging.basicConfig(level=logging.DEBUG, format='[APP LOGGER] %(asctime)s %(levelname)s: %(message)s')
###################### ######################
# Load configuration # # Load configuration #
###################### ######################
@ -23,9 +33,13 @@ MAIL_PASS = config['MAIL_PASS']
SMTP_PORT = int(config['SMTP_PORT']) SMTP_PORT = int(config['SMTP_PORT'])
IMAP_PORT = int(config['IMAP_PORT']) IMAP_PORT = int(config['IMAP_PORT'])
MAX_UPLOAD_SIZE = int(config['MAX_UPLOAD_SIZE']) # Bytes MAX_UPLOAD_SIZE = int(config['MAX_UPLOAD_SIZE']) # Bytes
MAX_FILES_PER_UPLOAD = int(config['MAX_FILES_PER_UPLOAD'])
CONTRACT_CLIENT_CONTACT = config['CONTRACT_CLIENT_CONTACT'] CONTRACT_CLIENT_CONTACT = config['CONTRACT_CLIENT_CONTACT']
MAIL_SUBJECT = config['MAIL_SUBJECT'] MAIL_SUBJECT = config['MAIL_SUBJECT']
MAIL_BODY = config['MAIL_BODY'] MAIL_BODY = config['MAIL_BODY']
SQL_CONN_STR = config['SQL_CONN_STR']
DESC_PREVODI = config['DESC_PREVODI']
DESC_GIGAFIDA = config['DESC_GIGAFIDA']
if 'UPLOADS_DIR' in config: if 'UPLOADS_DIR' in config:
UPLOADS_DIR = Path(config['UPLOADS_DIR']) UPLOADS_DIR = Path(config['UPLOADS_DIR'])
@ -34,13 +48,6 @@ else:
if not UPLOADS_DIR.exists: if not UPLOADS_DIR.exists:
UPLOADS_DIR.mkdir(parents=True) UPLOADS_DIR.mkdir(parents=True)
if 'DATA_DIR' in config:
DATA_DIR = Path(config['DATA_DIR'])
else:
DATA_DIR = Path(__file__).resolve().parent / 'data'
if not DATA_DIR.exists:
DATA_DIR.mkdir(parents=True)
# Override configs with environment variables, if set # Override configs with environment variables, if set
if 'PORTALDS4DS1_MAIL_HOST' in os.environ: if 'PORTALDS4DS1_MAIL_HOST' in os.environ:
MAIL_HOST = os.environ['PORTALDS4DS1_MAIL_HOST'] MAIL_HOST = os.environ['PORTALDS4DS1_MAIL_HOST']
@ -54,16 +61,22 @@ if 'PORTALDS4DS1_IMAP_PORT' in os.environ:
IMAP_PORT = int(os.environ['PORTALDS4DS1_IMAP_PORT']) IMAP_PORT = int(os.environ['PORTALDS4DS1_IMAP_PORT'])
if 'PORTALDS4DS1_MAX_UPLOAD_SIZE' in os.environ: if 'PORTALDS4DS1_MAX_UPLOAD_SIZE' in os.environ:
MAX_UPLOAD_SIZE = int(os.environ['PORTALDS4DS1_MAX_UPLOAD_SIZE']) MAX_UPLOAD_SIZE = int(os.environ['PORTALDS4DS1_MAX_UPLOAD_SIZE'])
if 'PORTALDS4DS1_MAX_FILES_PER_UPLOAD' in os.environ:
MAX_FILES_PER_UPLOAD = int(os.environ['PORTALDS4DS1_MAX_FILES_PER_UPLOAD'])
if 'PORTALDS4DS1_CONTRACT_CLIENT_CONTACT' in os.environ: if 'PORTALDS4DS1_CONTRACT_CLIENT_CONTACT' in os.environ:
CONTRACT_CLIENT_CONTACT = os.environ['PORTALDS4DS1_CONTRACT_CLIENT_CONTACT'] CONTRACT_CLIENT_CONTACT = os.environ['PORTALDS4DS1_CONTRACT_CLIENT_CONTACT']
if 'PORTALDS4DS1_UPLOADS_DIR' in os.environ: if 'PORTALDS4DS1_UPLOADS_DIR' in os.environ:
UPLOADS_DIR = os.environ['PORTALDS4DS1_UPLOADS_DIR'] UPLOADS_DIR = os.environ['PORTALDS4DS1_UPLOADS_DIR']
if 'PORTALDS4DS1_DATA_DIR' in os.environ:
DATA_DIR = os.environ['PORTALDS4DS1_DATA_DIR']
if 'PORTALDS4DS1_MAIL_SUBJECT' in os.environ: if 'PORTALDS4DS1_MAIL_SUBJECT' in os.environ:
MAIL_SUBJECT = os.environ['PORTALDS4DS1_MAIL_SUBJECT'] MAIL_SUBJECT = os.environ['PORTALDS4DS1_MAIL_SUBJECT']
if 'PORTALDS4DS1_MAIL_BODY' in os.environ: if 'PORTALDS4DS1_MAIL_BODY' in os.environ:
MAIL_BODY = os.environ['PORTALDS4DS1_MAIL_BODY'] MAIL_BODY = os.environ['PORTALDS4DS1_MAIL_BODY']
if 'PORTALDS4DS1_SQL_CONN_STR' in os.environ:
SQL_CONN_STR = os.environ['PORTALDS4DS1_SQL_CONN_STR']
if 'PORTALDS4DS1_DESC_PREVODI' in os.environ:
DESC_PREVODI = os.environ['PORTALDS4DS1_DESC_PREVODI']
if 'PORTALDS4DS1_DESC_GIGAFIDA' in os.environ:
DESC_GIGAFIDA = os.environ['PORTALDS4DS1_DESC_GIGAFIDA']
VALID_CORPUS_NAMES = ['prevodi', 'gigafida', 'solar'] VALID_CORPUS_NAMES = ['prevodi', 'gigafida', 'solar']
@ -75,11 +88,32 @@ app = Flask(__name__)
app.config.update( app.config.update(
UPLOADED_PATH = UPLOADS_DIR, UPLOADED_PATH = UPLOADS_DIR,
MAX_CONTENT_LENGTH = MAX_UPLOAD_SIZE, MAX_CONTENT_LENGTH = MAX_UPLOAD_SIZE,
TEMPLATES_AUTO_RELOAD = True TEMPLATES_AUTO_RELOAD = True,
SQLALCHEMY_DATABASE_URI = SQL_CONN_STR,
SQLALCHEMY_ECHO = True
) )
# Run "python app.py db -?" to see more info about DB migrations.
manager = Manager(app)
db.init_app(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
# Set up dropzone.js to serve all the stuff for "file dropping" on the web interface.
dropzone = Dropzone(app) dropzone = Dropzone(app)
upload_handler = portal.base.UploadHandler(
UPLOADS_DIR=UPLOADS_DIR,
MAIL_HOST=MAIL_HOST,
MAIL_LOGIN=MAIL_LOGIN,
MAIL_PASS=MAIL_PASS,
SMTP_PORT=SMTP_PORT,
IMAP_PORT=IMAP_PORT,
MAIL_SUBJECT=MAIL_SUBJECT,
MAIL_BODY=MAIL_BODY,
CONTRACT_CLIENT_CONTACT=CONTRACT_CLIENT_CONTACT
)
@app.route('/') @app.route('/')
def index(): def index():
@ -90,11 +124,20 @@ def index():
def index_corpus(corpus_name): def index_corpus(corpus_name):
if corpus_name not in VALID_CORPUS_NAMES: if corpus_name not in VALID_CORPUS_NAMES:
return 'Korpus "{}" ne obstaja.'.format(corpus_name), 404 return 'Korpus "{}" ne obstaja.'.format(corpus_name), 404
if corpus_name == 'prevodi': if corpus_name == 'prevodi':
subtitle = 'KORPUS PARALELNIH BESEDIL ANG-SLO' description = DESC_PREVODI
elif corpus_name == 'gigafida': elif corpus_name == 'gigafida':
subtitle = 'KORPUS GIGAFIDA' description = DESC_GIGAFIDA
return render_template('basic.html', subtitle=subtitle, corpus_name=corpus_name) elif corpus_name == 'solar':
return handle_solar(request)
return render_template('basic.html',
corpus_name=corpus_name, description=description, max_files=MAX_FILES_PER_UPLOAD)
def handle_solar(request):
return 404
@app.route('/<corpus_name>/upload', methods=['POST']) @app.route('/<corpus_name>/upload', methods=['POST'])
@ -102,40 +145,90 @@ def handle_upload(corpus_name):
if corpus_name not in VALID_CORPUS_NAMES: if corpus_name not in VALID_CORPUS_NAMES:
return 404 return 404
if corpus_name == 'solar':
return handle_upload_solar(request)
else:
return handle_upload_unauthenticated(request, corpus_name)
def handle_upload_solar(request):
return 404
def handle_upload_unauthenticated(request, corpus_name):
files = request.files files = request.files
if len(files) > 20: if len(files) > MAX_FILES_PER_UPLOAD:
return 'Naložite lahko do 20 datotek hkrati.', 400 return 'Naložite lahko do {} datotek hkrati.'.format(MAX_FILES_PER_UPLOAD), 400
elif len(files) < 1: elif len(files) < 1:
return 'Priložena ni bila nobena datoteka.', 400 return 'Priložena ni bila nobena datoteka.', 400
print('one') err = check_suffixes(files)
err = portal.base.check_suffixes(files)
if err: if err:
return err, 400 return err, 400
print('two') err = check_form(request.form)
err = portal.base.check_form(request.form)
if err: if err:
return err, 400 return err, 400
print('three') # Parse request.
upload_metadata = portal.base.get_upload_metadata(corpus_name, request) upload_metadata = upload_handler.extract_upload_metadata(corpus_name, request)
contract_file_name = portal.base.generate_contract_pdf(UPLOADS_DIR, upload_metadata, CONTRACT_CLIENT_CONTACT)
# Add contract_file_name to metadata TODO: move somewhere else logging.info('Upload with id "{}" supplied form data: {}'.format(upload_metadata['upload_id'],
upload_metadata['contract'] = contract_file_name str(upload_metadata['form_data'])))
portal.base.store_datafiles(UPLOADS_DIR, files, upload_metadata)
portal.base.store_metadata(UPLOADS_DIR, upload_metadata) # Generate contract PDF file based on the uploads metadata.
portal.base.send_confirm_mail( upload_handler.generate_upload_contract_pdf(upload_metadata)
subject=MAIL_SUBJECT,
body=MAIL_BODY, # Store uploaded files to disk.
uploads_path=UPLOADS_DIR, upload_handler.store_datafiles(files, upload_metadata)
upload_metadata=upload_metadata,
mail_host=MAIL_HOST, mail_login=MAIL_LOGIN, mail_pass=MAIL_PASS, # Store metadata to database.
imap_port=IMAP_PORT, smtp_port=SMTP_PORT) upload_handler.store_metadata_unauthenticated(upload_metadata)
# Send confirmation mail along with the contract to the submitted email address.
upload_handler.send_confirm_mail(upload_metadata)
return 'Uspešno ste oddali datotek(e). Št. datotek: {}'.format(len(files)) return 'Uspešno ste oddali datotek(e). Št. datotek: {}'.format(len(files))
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:
return 'Datoteka "{}" ni pravilnega formata.'.format(f.filename)
return None
def check_form(form):
ime = form.get('ime')
podjetje = form.get('podjetje')
naslov = form.get('naslov')
posta = form.get('posta')
email = form.get('email')
telefon = form.get('telefon')
if len(ime) > 100:
return 'Predolgo ime.'
if len(podjetje) > 100:
return 'Predolgo ime institucije.'
if len(email) > 100:
return 'Predolgi email naslov'
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
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@ -1,13 +1,16 @@
[DEFAULT] [DEFAULT]
SQL_CONN_STR=postgresql://portal:randompass123@localhost/portal
MAIL_HOST=posta.cjvt.si MAIL_HOST=posta.cjvt.si
MAIL_LOGIN=oddaja-besedil@cjvt.si MAIL_LOGIN=oddaja-besedil@cjvt.si
MAIL_PASS=secretmailpass123 MAIL_PASS=secretmailpass123
SMTP_PORT=465 SMTP_PORT=465
IMAP_PORT=993 IMAP_PORT=993
MAX_UPLOAD_SIZE=1000000000 MAX_UPLOAD_SIZE=1000000000
MAX_FILES_PER_UPLOAD=30
UPLOADS_DIR=./uploads UPLOADS_DIR=./uploads
DATA_DIR=./data
CONTRACT_CLIENT_CONTACT=Testko Tester CONTRACT_CLIENT_CONTACT=Testko Tester
DESC_PREVODI=<h2 id="subtitle">Prevodi</h2><p>Strojno prevajanje je ena od uporabnih jezikovnih tehnologij, saj omogoča hitro sporazumevanje med ljudmi iz različnih kultur in jezikovnih okolij. Več o razvoju slovenskega strojnega prevajalnika lahko preberete na tej <a href="https://slovenscina.eu/strojno-prevajanje">povezavi</a>. Za kakovosten strojni prevajalnik so ključnega pomena prevodi, iz kateri se algoritmi umetne inteligence naučijo prevajati. S prispevanjem besedil v korpus prevodov boste pomembno prispevali k razvoju slovenskega strojnega prevajalnika med angleščino in slovenščino. Več informacij o prispevanju besedil najdete <a href="https://slovenscina.eu/strojno-prevajanje/pogosta-vprasanja">tukaj</a>.</p>
DESC_GIGAFIDA=<h2 id="subtitle">Gigafida</h2><p><a href="https://viri.cjvt.si/gigafida/">Gigafida</a> je referenčni korpus pisne slovenščine. Besedila so izbrana in strojno obdelana z namenom, da bi korpus kot vzorec sodobne standardne slovenščine lahko služil za jezikoslovne in druge humanistične raziskave, izdelavo sodobnih slovarjev, slovnic, učnih gradiv in razvoj jezikovnih tehnologij za slovenščino. S prispevanjem besedil v korpus Gigafida pomembno prispevate k razvoju sodobnih jezikovnih tehnologij za slovenski jezik.</p>
MAIL_SUBJECT=RSDO: pogodba za oddana besedila ({upload_id}) MAIL_SUBJECT=RSDO: pogodba za oddana besedila ({upload_id})
MAIL_BODY=Hvala, ker ste prispevali besedila in na ta način pomagali pri razvoju slovenskega jezika v digitalnem okolju. V prilogi vam pošiljamo pogodbo s seznamom naloženih datotek. MAIL_BODY=Hvala, ker ste prispevali besedila in na ta način pomagali pri razvoju slovenskega jezika v digitalnem okolju. V prilogi vam pošiljamo pogodbo s seznamom naloženih datotek.

View File

@ -4,6 +4,7 @@ services:
build: . build: .
restart: always restart: always
environment: environment:
- PORTALDS4DS1_SQL_CONN_STR=postgresql://portal:randompass123@db/portal
- PORTALDS4DS1_MAIL_HOST=posta.cjvt.si - PORTALDS4DS1_MAIL_HOST=posta.cjvt.si
- PORTALDS4DS1_MAIL_LOGIN=oddaja-besedil@cjvt.si - PORTALDS4DS1_MAIL_LOGIN=oddaja-besedil@cjvt.si
- PORTALDS4DS1_MAIL_PASS=randompass123 - PORTALDS4DS1_MAIL_PASS=randompass123
@ -15,4 +16,14 @@ services:
ports: ports:
- 127.0.0.1:5000:80 - 127.0.0.1:5000:80
volumes: volumes:
- /tmp/uploads/:/usr/src/portal-webapp/uploads - /tmp/portal-ds4-ds1/uploads:/usr/src/portal-webapp/uploads
command: gunicorn --bind 0.0.0.0:80 -w 1 --access-logfile - app:app
db:
image: postgres:9.6.21-alpine
restart: always
environment:
- POSTGRES_PASSWORD=randompass123
- POSTGRES_USER=portal
- POSTGRES_DB=portal
volumes:
- /tmp/portal-ds4-ds1/db:/var/lib/postgresql/data

7
entrypoint.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
# Upgrade DB schema to version used by application. This also initializes table, if they aren't already created.
flask db upgrade
exec "$@"

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

50
migrations/alembic.ini Normal file
View File

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

90
migrations/env.py Normal file
View File

@ -0,0 +1,90 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.engine
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,40 @@
"""Initial migration.
Revision ID: a846faa2b908
Revises:
Create Date: 2021-03-24 08:53:24.792682
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a846faa2b908'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('upload_unauthenticated',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('upload_hash', sa.String(), nullable=True),
sa.Column('timestamp', sa.DateTime(), nullable=True),
sa.Column('form_name', sa.String(), nullable=True),
sa.Column('form_org', sa.String(), nullable=True),
sa.Column('form_address', sa.String(), nullable=True),
sa.Column('form_zipcode', sa.String(), nullable=True),
sa.Column('form_email', sa.String(), nullable=True),
sa.Column('file_contract', sa.String(), nullable=True),
sa.Column('upload_file_hashes', sa.ARRAY(sa.String()), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('upload_unauthenticated')
# ### end Alembic commands ###

View File

@ -1,8 +1,9 @@
import re
import hashlib import hashlib
import time import time
import ssl import ssl
import traceback
from pathlib import Path from pathlib import Path
from datetime import datetime
import imaplib import imaplib
from smtplib import SMTP_SSL from smtplib import SMTP_SSL
@ -17,9 +18,7 @@ from email.mime.application import MIMEApplication
import pdfkit import pdfkit
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from . model import db, UploadUnauthenticated
ENABLED_FILETYPES = ['txt', 'csv', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'xml', 'mxliff', 'tmx']
REGEX_EMAIL = re.compile('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$')
class ContractCreator: class ContractCreator:
@ -49,54 +48,48 @@ class ContractCreator:
pdfkit.from_string(html_str, out_f, options=self.pdfkit_options) pdfkit.from_string(html_str, out_f, options=self.pdfkit_options)
contract_creator = ContractCreator() class UploadHandler:
def __init__(self, **kwargs):
self.config = kwargs
def get_upload_metadata(corpus_name, request): self.contract_creator = ContractCreator()
def extract_upload_metadata(self, corpus_name, request):
upload_metadata = dict() upload_metadata = dict()
file_hashes = create_file_hashes(request.files) file_hashes = self.create_file_hashes(request.files)
file_names = file_hashes.keys() file_names = file_hashes.keys()
form_data = request.form.copy() form_data = request.form.copy()
upload_timestamp = int(time.time()) upload_timestamp = int(time.time())
upload_id = create_upload_id(corpus_name, form_data, upload_timestamp, file_hashes) upload_id = self.create_upload_id(corpus_name, form_data, upload_timestamp, file_hashes)
upload_metadata['corpus_name'] = corpus_name upload_metadata['corpus_name'] = corpus_name
upload_metadata['form_data'] = form_data upload_metadata['form_data'] = form_data
upload_metadata['upload_id'] = upload_id upload_metadata['upload_id'] = upload_id
upload_metadata['timestamp'] = upload_timestamp upload_metadata['timestamp'] = upload_timestamp
upload_metadata['file_hashes'] = file_hashes upload_metadata['file_hashes_dict'] = file_hashes
upload_metadata['file_names'] = file_names upload_metadata['file_names'] = file_names
upload_metadata['contract_file'] = upload_id + '.pdf'
return upload_metadata return upload_metadata
def check_suffixes(files): def get_uploads_subdir(self, dir_name):
for key, f in files.items(): subdir = self.config['UPLOADS_DIR'] / dir_name
if key.startswith('file'):
suffix = f.filename.split('.')[-1]
if suffix not in ENABLED_FILETYPES:
return 'Datoteka "{}" ni pravilnega formata.'.format(f.filename)
return None
def get_subdir(uploads_path, dir_name):
subdir = uploads_path / dir_name
if not subdir.exists(): if not subdir.exists():
subdir.mkdir(parents=True) subdir.mkdir(parents=True)
return subdir return subdir
def create_upload_id(corpus_name, form_data, upload_timestamp, file_hashes): def create_upload_id(self, corpus_name, form_data, upload_timestamp, file_hashes):
ime = form_data.get('ime') # Order is important while hashing, hence the sorting.
podjetje = form_data.get('podjetje') val_buff = [str(upload_timestamp)]
naslov = form_data.get('naslov') for key in sorted(form_data):
posta = form_data.get('posta') val_buff.append(form_data[key])
email = form_data.get('email')
telefon = form_data.get('telefon')
# This hash serves as an unique identifier for the whole upload. # This hash serves as an unique identifier for the whole upload.
metahash = hashlib.md5((corpus_name+ime+podjetje+naslov+posta+email+telefon).encode()) metahash = hashlib.md5((''.join(val_buff)).encode())
# Include file hashes to avoid metafile name collisions if they have the same form values, # 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. # but different data files. Sort hashes first so upload order doesn't matter.
sorted_f_hashes = list(file_hashes.values()) sorted_f_hashes = list(file_hashes.values())
@ -106,39 +99,7 @@ def create_upload_id(corpus_name, form_data, upload_timestamp, file_hashes):
return metahash return metahash
def create_file_hashes(self, files):
def check_form(form):
ime = form.get('ime')
podjetje = form.get('podjetje')
naslov = form.get('naslov')
posta = form.get('posta')
email = form.get('email')
telefon = form.get('telefon')
if len(ime) > 100:
return 'Predolgo ime.'
if len(podjetje) > 100:
return 'Predolgo ime institucije.'
if len(email) > 100:
return 'Predolgi email naslov'
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
def create_file_hashes(files):
res = dict() res = dict()
for key, f in files.items(): for key, f in files.items():
if key.startswith('file'): if key.startswith('file'):
@ -148,36 +109,37 @@ def create_file_hashes(files):
f.seek(0) f.seek(0)
return res return res
def store_metadata_unauthenticated(self, upload_metadata):
def store_metadata(uploads_path, upload_metadata): timestamp = datetime.fromtimestamp(upload_metadata['timestamp'])
base = get_subdir(uploads_path, 'meta')
timestamp = upload_metadata['timestamp']
upload_id = upload_metadata['upload_id']
form_data = upload_metadata['form_data'] form_data = upload_metadata['form_data']
email = form_data['email'] file_hashes = upload_metadata['file_hashes_dict']
file_hashes = upload_metadata['file_hashes']
contract = upload_metadata['contract']
filename = str(timestamp) + '-' + email + '-' + upload_id + '.meta'
sorted_f_hashes = list(file_hashes.values()) sorted_f_hashes = list(file_hashes.values())
sorted_f_hashes.sort() sorted_f_hashes.sort()
path = base / filename try:
with path.open('w') as f: upload_unauthenticated = UploadUnauthenticated(
f.write('korpus=' + upload_metadata['corpus_name']) upload_hash=upload_metadata['upload_id'],
f.write('\nime=' + form_data['ime']) timestamp=timestamp,
f.write('\npodjetje=' + form_data['podjetje']) form_name=form_data['ime'],
f.write('\nnaslov=' + form_data['naslov']) form_org=form_data['podjetje'],
f.write('\nposta=' + form_data['posta']) form_address=form_data['naslov'],
f.write('\nemail=' + form_data['email']) form_zipcode=form_data['posta'],
f.write('\ndatoteke=' + str(sorted_f_hashes)) form_email=form_data['email'],
f.write('\npogodba=' + contract) file_contract=upload_metadata['contract_file'],
upload_file_hashes=sorted_f_hashes
)
db.session.add(upload_unauthenticated)
db.session.commit()
except Exception:
traceback.print_exc()
def store_datafiles(uploads_path, files, upload_metadata): def store_metadata_authenticated(self, upload_metadata):
base = get_subdir(uploads_path, 'files') pass
file_hashes = upload_metadata['file_hashes']
def store_datafiles(self, files, upload_metadata):
base = self.get_uploads_subdir('files')
file_hashes = upload_metadata['file_hashes_dict']
for key, f in files.items(): for key, f in files.items():
if key.startswith('file'): if key.startswith('file'):
@ -186,10 +148,8 @@ def store_datafiles(uploads_path, files, upload_metadata):
path.mkdir() path.mkdir()
f.save(path / f.filename) f.save(path / f.filename)
def generate_upload_contract_pdf(self, upload_metadata):
def generate_contract_pdf(uploads_path, upload_metadata, contract_client_contact): base = self.get_uploads_subdir('contracts')
base = get_subdir(uploads_path, 'contracts')
contract_file_name = upload_metadata['upload_id'] + '.pdf'
form_data = upload_metadata['form_data'] form_data = upload_metadata['form_data']
files_table_str = [] files_table_str = []
@ -203,27 +163,25 @@ def generate_contract_pdf(uploads_path, upload_metadata, contract_client_contact
'ime_priimek': form_data['ime'], 'ime_priimek': form_data['ime'],
'naslov': form_data['naslov'], 'naslov': form_data['naslov'],
'posta': form_data['posta'], 'posta': form_data['posta'],
'kontakt_narocnik': contract_client_contact, 'kontakt_narocnik': self.config['CONTRACT_CLIENT_CONTACT'],
'kontakt_imetnikpravic': form_data['ime'], 'kontakt_imetnikpravic': form_data['ime'],
'files_table_str': files_table_str 'files_table_str': files_table_str
} }
contract_creator.create_pdf(base / contract_file_name, data) self.contract_creator.create_pdf(base / upload_metadata['contract_file'], data)
return contract_file_name
def send_confirm_mail(self, upload_metadata):
def send_confirm_mail(subject, body, uploads_path, upload_metadata, mail_host, mail_login, mail_pass, imap_port=993, smtp_port=465):
upload_id = upload_metadata['upload_id'] upload_id = upload_metadata['upload_id']
message = MIMEMultipart() message = MIMEMultipart()
message['From'] = mail_login message['From'] = self.config['MAIL_LOGIN']
message['To'] = upload_metadata['form_data']['email'] message['To'] = upload_metadata['form_data']['email']
message['Subject'] = subject.format(upload_id=upload_id) message['Subject'] = self.config['MAIL_SUBJECT'].format(upload_id=upload_id)
body = body.format(upload_id=upload_id) body = self.config['MAIL_BODY'].format(upload_id=upload_id)
message.attach(MIMEText(body, "plain")) message.attach(MIMEText(body, "plain"))
contracts_dir = get_subdir(uploads_path, 'contracts') contracts_dir = self.get_uploads_subdir('contracts')
base_name = upload_metadata['contract'] base_name = upload_metadata['contract_file']
contract_file = contracts_dir / base_name contract_file = contracts_dir / base_name
with open(contract_file, "rb") as f: with open(contract_file, "rb") as f:
part = MIMEApplication( part = MIMEApplication(
@ -238,13 +196,13 @@ def send_confirm_mail(subject, body, uploads_path, upload_metadata, mail_host, m
# Create a secure SSL context # Create a secure SSL context
context = ssl.create_default_context() context = ssl.create_default_context()
with SMTP_SSL(mail_host, smtp_port, context=context) as server: with SMTP_SSL(self.config['MAIL_HOST'], self.config['SMTP_PORT'], context=context) as server:
server.login(mail_login, mail_pass) server.login(self.config['MAIL_LOGIN'], self.config['MAIL_PASS'])
server.sendmail(message['From'], message['To'], text) server.sendmail(message['From'], message['To'], text)
# Save copy of sent mail in Sent mailbox # Save copy of sent mail in Sent mailbox
imap = imaplib.IMAP4_SSL(mail_host, imap_port) imap = imaplib.IMAP4_SSL(self.config['MAIL_HOST'], self.config['IMAP_PORT'])
imap.login(mail_login, mail_pass) imap.login(self.config['MAIL_LOGIN'], self.config['MAIL_PASS'])
imap.append('Sent', '\\Seen', imaplib.Time2Internaldate(time.time()), text.encode('utf8')) imap.append('Sent', '\\Seen', imaplib.Time2Internaldate(time.time()), text.encode('utf8'))
imap.logout() imap.logout()

26
portal/model.py Normal file
View File

@ -0,0 +1,26 @@
from datetime import datetime
import sqlalchemy
from flask_sqlalchemy import SQLAlchemy
###########################################
# Model classes for describing SQL tables #
###########################################
# The "db" object gets bound to the Flask app in app.py.
db = SQLAlchemy()
# Entries for uploads to corpora, that have no authentication. E.g. "prevodi" or "gigafida".
class UploadUnauthenticated(db.Model):
__tablename__ = 'upload_unauthenticated'
id = db.Column(db.Integer, primary_key=True)
upload_hash = db.Column(db.String)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
form_name = db.Column(db.String)
form_org = db.Column(db.String)
form_address = db.Column(db.String)
form_zipcode = db.Column(db.String)
form_email = db.Column(db.String)
file_contract = db.Column(db.String)
upload_file_hashes = db.Column(sqlalchemy.types.ARRAY(db.String))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 951 KiB

After

Width:  |  Height:  |  Size: 298 KiB

View File

@ -21,14 +21,16 @@ html {
overflow-y: hidden; overflow-y: hidden;
} }
.bg { html {
background-image: url("image/bg.jpeg"); background: url(image/bg.jpeg) no-repeat center center fixed;
height: 100%; -webkit-background-size: cover;
background-position: center; -moz-background-size: cover;
background-repeat: no-repeat; -o-background-size: cover;
background-size: cover; background-size: cover;
overflow-y: scroll;
} }
#main-window { #main-window {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -60,7 +62,7 @@ html {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 200; font-weight: 200;
font-size: 36px; font-size: 32px;
line-height: 42px; line-height: 42px;
margin-block-start: 0.4em; margin-block-start: 0.4em;
z-index: 5; z-index: 5;
@ -89,6 +91,15 @@ label {
color: #46535b; color: #46535b;
} }
.form-text {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 11px;
line-height: 12px;
color: #46535b;
}
#button-submit { #button-submit {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -102,7 +113,7 @@ label {
background: #006cb7; background: #006cb7;
border-radius: 29px; border-radius: 29px;
border: 0px; border: 0px;
top: 530px; top: 630px;
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
@ -187,8 +198,8 @@ input {
margin-top: 26px; margin-top: 26px;
margin-bottom: 26px; margin-bottom: 26px;
min-height: 100px; min-height: 100px;
max-height: 500px; max-height: 670px;
top: -530px; top: -600px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
} }
@ -196,7 +207,7 @@ input {
#rect1 { #rect1 {
position: relative; position: relative;
width: 388px; width: 388px;
height: 631px; height: 731px;
background: #f5f5f5; background: #f5f5f5;
box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.25); box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.25);
@ -238,7 +249,7 @@ input {
width: 60%; width: 60%;
height: 40%; height: 40%;
margin: 0 auto; margin: 0 auto;
padding-top: 90px; padding-top: 120px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
right: 75px; right: 75px;

View File

@ -8,7 +8,7 @@
{{ dropzone.style('position: absolute; {{ dropzone.style('position: absolute;
top: -0.5px; top: -0.5px;
width: 388px; width: 388px;
height: 632px; height: 732px;
left: 385px; 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%); 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); box-shadow: 20px 4px 40px rgba(0, 0, 0, 0.25);
@ -18,7 +18,6 @@
<link rel="stylesheet" href="static/style.css" type="text/css"> <link rel="stylesheet" href="static/style.css" type="text/css">
</head> </head>
<body> <body>
<div class="bg"></div>
<div id="main-window"> <div id="main-window">
<div id="rect1"> <div id="rect1">
<div id="logo-container"> <div id="logo-container">
@ -27,8 +26,8 @@
<form id="my-dropzone" class="dropzone"> <form id="my-dropzone" class="dropzone">
<div style="position: relative; right: 390px;"> <div style="position: relative; right: 390px;">
<h1 id="title">Portal za oddajanje besedil {{corpus_name}}</h1> <h1 id="title">Portal za oddajanje besedil</h1>
<h2 id="subtitle">{{subtitle}}</h2> <div class="form-text">{{description|safe}}</div>
<label for="ime">* Ime:</label> <label for="ime">* Ime:</label>
<input type="text" id="ime" name="ime" required="required"/> <input type="text" id="ime" name="ime" required="required"/>
@ -48,8 +47,7 @@
<label for="telefon">Telefon:</label> <label for="telefon">Telefon:</label>
<input type="text" id="telefon" name="telefon"/> <input type="text" id="telefon" name="telefon"/>
<input type="checkbox" id="izjava" name="izjava" value="izjava" required="required"> <div class="form-text">*Po kliku na gumb “Oddaj” se bo prikazala vsebina pogodobe o odstopu avtorskih pravic. Če se z vsebino strinjate, kliknite gumb “Pošlji”, da podatke posredujete v korpus, po e-pošti pa boste prejeli svoj izvod pogodbe.</div>
<label for="izjava">* Izjavljam, da sem lastnik avtorskih pravic in dovoljujem, da se besedila vključijo v korpuse v skladu z ustrezno licenco korpusa.</label>
<button id="button-submit" type="submit">Oddaj</button> <button id="button-submit" type="submit">Oddaj</button>
</div> </div>
@ -163,7 +161,7 @@ zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatk
<p>10.3. Pogodbeni stranki s podpisom potrjujeta veljavnost te pogodbe.</p> <p>10.3. Pogodbeni stranki s podpisom potrjujeta veljavnost te pogodbe.</p>
</div> </div>
<button id="button-submit-cancel" class="button-terms" style="background: #ff2d2d;">Prekliči</button> <button id="button-submit-cancel" class="button-terms" style="background: #ff2d2d;">Prekliči</button>
<button id="button-submit-final" class="button-terms">Oddaj</button> <button id="button-submit-final" class="button-terms">Pošlji</button>
</div> </div>
<!--{{ dropzone.load_js() }}--> <!--{{ dropzone.load_js() }}-->
@ -190,17 +188,17 @@ zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatk
url: "/{{corpus_name}}/upload", url: "/{{corpus_name}}/upload",
autoProcessQueue: false, autoProcessQueue: false,
uploadMultiple: true, uploadMultiple: true,
parallelUploads: 20, parallelUploads: {{max_files}},
paramName: "file", // The name that will be used to transfer the file paramName: "file", // The name that will be used to transfer the file
maxFilesize: 1000, // MB maxFilesize: 1000, // MB
acceptedFiles: ".txt, .csv, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx", acceptedFiles: ".txt, .csv, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx",
maxFiles: 20, maxFiles: {{max_files}},
dictDefaultMessage: `Kliknite ali odložite datoteke sem.`, dictDefaultMessage: `Kliknite ali odložite datoteke sem.`,
dictFallbackMessage: "Vaš brskalnik ne podpira izbiranje datotek z odlaganjem (\"drag & drop\").", dictFallbackMessage: "Vaš brskalnik ne podpira izbiranje datotek z odlaganjem (\"drag & drop\").",
dictInvalidFileType: "Datoteka je napačnega formata.", dictInvalidFileType: "Datoteka je napačnega formata.",
dictFileTooBig: "Datoteke je prevelika {{filesize}}. Največja dovoljena velikost: {{maxFilesize}}MiB.", dictFileTooBig: "Datoteke je prevelika {{filesize}}. Največja dovoljena velikost: {{maxFilesize}}MiB.",
dictResponseError: "Napaka strežnika: {{statusCode}}", dictResponseError: "Napaka strežnika: {{statusCode}}",
dictMaxFilesExceeded: "Ne morete naložiti več datotek.", dictMaxFilesExceeded: "Največje število datotek že doseženo.",
dictCancelUpload: "Prekini prenos", dictCancelUpload: "Prekini prenos",
dictRemoveFile: "Odstrani datoteko", dictRemoveFile: "Odstrani datoteko",
dictCancelUploadConfirmation: "Ali res želite odstraniti to datoteko?", dictCancelUploadConfirmation: "Ali res želite odstraniti to datoteko?",
@ -221,8 +219,7 @@ zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatk
var email = form["email"].value; var email = form["email"].value;
var podjetje = form["podjetje"].value; var podjetje = form["podjetje"].value;
var telefon = form["telefon"].value; var telefon = form["telefon"].value;
var izjava = form["izjava"].checked; if (isEmptyOrSpaces(ime) || isEmptyOrSpaces(email)) {
if (isEmptyOrSpaces(ime) || isEmptyOrSpaces(email) || !izjava) {
alert("Izpolnite vsa obvezna polja!"); alert("Izpolnite vsa obvezna polja!");
} else if (!reEmail.test(email.toLowerCase())) { } else if (!reEmail.test(email.toLowerCase())) {
alert("Email napačnega formata!"); alert("Email napačnega formata!");

300
templates/index-solar.html Normal file
View File

@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Portal za oddajanje besedil</title>
<!--{{ dropzone.load_css() }}-->
<link rel="stylesheet" href="static/dropzone.css" type="text/css">
{{ dropzone.style('position: absolute;
top: -0.5px;
width: 388px;
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);
backdrop-filter: blur(20px);
border: 0px;
border-radius: 0px 20px 20px 0px;') }}
<link rel="stylesheet" href="static/style.css" type="text/css">
</head>
<body>
<div class="bg"></div>
<div id="main-window">
<div id="rect1">
<div id="logo-container">
<img src="static/image/logo.svg" alt="logo"/>
</div>
<form id="my-dropzone" class="dropzone">
<div style="position: relative; right: 390px;">
<h1 id="title">Portal za oddajanje besedil {{corpus_name}}</h1>
<h2 id="subtitle">{{subtitle}}</h2>
<label for="ime">* Ime:</label>
<input type="text" id="ime" name="ime" required="required"/>
<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">* E-Pošta:</label>
<input type="text" id="email" name="email" required="required"/>
<label for="telefon">Telefon:</label>
<input type="text" id="telefon" name="telefon"/>
<input type="checkbox" id="izjava" name="izjava" value="izjava" required="required">
<label for="izjava">* Izjavljam, da sem lastnik avtorskih pravic in dovoljujem, da se besedila vključijo v korpuse v skladu z ustrezno licenco korpusa.</label>
<button id="button-submit" type="submit">Oddaj</button>
</div>
<div class="dropzone-previews"></div>
</form>
</div>
</div>
<div id="popup-terms" style="display: none">
<div id="popup-terms-text">
<h2>POGODBA O PRENOSU AVTORSKIH PRAVIC</h2>
<h3>UVODNE DOLOČBE</h3>
<h4><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>PREDMET POGODBE</h3>
<h4><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>PRENOS AVTORSKIH PRAVIC</h3>
<h4><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>JAMČEVANJE IMETNIKA PRAVIC</h3>
<h4><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>OSEBNI PODATKI</h3>
<h4><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>KONTAKTNE OSEBE</h3>
<h4><b>6. člen</b></h4>
<p>7.1 Kontaktna oseba za izvedbo te pogodbe na strani naročnika je [xxx].</p>
<p>7.2. Kontaktna oseba za izvedbo te pogodbe na strani imetnika pravic je [xxx].</p>
<h3>KONČNE DOLOČBE</h3>
<h4><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><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><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>
</div>
<button id="button-submit-cancel" class="button-terms" style="background: #ff2d2d;">Prekliči</button>
<button id="button-submit-final" class="button-terms">Oddaj</button>
</div>
<!--{{ dropzone.load_js() }}-->
<script src="static/dropzone.js"></script>
<script>
/////////////////////////
// Dropzone //
/////////////////////////
var btnSubmit = document.getElementById("button-submit");
var btnSubmitFinal = document.getElementById("button-submit-final");
var btnSubmitCancel = document.getElementById("button-submit-cancel");
var elemTermsPopup = document.getElementById("popup-terms");
var termsScrollbox = document.getElementById("popup-terms-text");
var scrollboxTriggered = false;
var form = document.forms["my-dropzone"];
function isEmptyOrSpaces(str){
return str == null || str.match(/^ *$/) !== null;
}
const reEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
Dropzone.options.myDropzone = { // The camelized version of the ID of the form element
url: "/{{corpus_name}}/upload",
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: 20,
paramName: "file", // The name that will be used to transfer the file
maxFilesize: 1000, // MB
acceptedFiles: ".txt, .csv, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx",
maxFiles: 20,
dictDefaultMessage: `Kliknite ali odložite datoteke sem.`,
dictFallbackMessage: "Vaš brskalnik ne podpira izbiranje datotek z odlaganjem (\"drag & drop\").",
dictInvalidFileType: "Datoteka je napačnega formata.",
dictFileTooBig: "Datoteke je prevelika {{filesize}}. Največja dovoljena velikost: {{maxFilesize}}MiB.",
dictResponseError: "Napaka strežnika: {{statusCode}}",
dictMaxFilesExceeded: "Ne morete naložiti več datotek.",
dictCancelUpload: "Prekini prenos",
dictRemoveFile: "Odstrani datoteko",
dictCancelUploadConfirmation: "Ali res želite odstraniti to datoteko?",
dictUploadCanceled: "Prenos prekinjen",
// The setting up of the dropzone
init: function() {
var dz = this;
btnSubmit.addEventListener("click", function(e) {
// Make sure that the form isn't actually being sent.
e.preventDefault();
e.stopPropagation();
// Check form validity.
var ime = form["ime"].value;
var email = form["email"].value;
var podjetje = form["podjetje"].value;
var telefon = form["telefon"].value;
var izjava = form["izjava"].checked;
if (isEmptyOrSpaces(ime) || isEmptyOrSpaces(email) || !izjava) {
alert("Izpolnite vsa obvezna polja!");
} else if (!reEmail.test(email.toLowerCase())) {
alert("Email napačnega formata!");
} else if (ime.length > 100 || email.length > 100 || podjetje.length > 100 || telefon.length > 100) {
alert("Velikost polj je omejena na 100 znakov.");
} else {
// Then make terms popup visible
btnSubmit.disabled = true;
btnSubmitFinal.disabled = true;
elemTermsPopup.style.display = "inline";
scrollboxTriggered = false;
}
});
// First change the button to actually tell Dropzone to process the queue.
btnSubmitFinal.addEventListener("click", function(e) {
// Hand off data to dropzone
dz.processQueue();
// Clear fields and hide popup agian
btnSubmit.disabled = false;
elemTermsPopup.style.display = "none";
form.reset();
scrollboxTriggered = false;
});
btnSubmitCancel.addEventListener("click", function(e) {
btnSubmit.disabled = false;
scrollboxTriggered = false;
elemTermsPopup.style.display = "none";
});
// Enable final submit button only if user scrolls to the end of the terms.
function checkScrollboxTrigger(event) {
var element = event.target;
if (!scrollboxTriggered
&& element.scrollHeight - element.scrollTop <= element.clientHeight + 50
) {
scrollboxTriggered = true;
btnSubmitFinal.disabled = false;
}
}
termsScrollbox.addEventListener('scroll', function(event) {
checkScrollboxTrigger(event);
});
termsScrollbox.addEventListener("mouseenter", function(event) {
checkScrollboxTrigger(event);
});
// Listen to the sendingmultiple event. In this case, it's the sendingmultiple event instead
// of the sending event because uploadMultiple is set to true.
this.on("sendingmultiple", function() {
// Gets triggered when the form is actually being sent.
// Hide the success button or the complete form.
});
this.on("successmultiple", function(files, response) {
// Gets triggered when the files have successfully been sent.
// Redirect user or notify of success.
alert("Odgovor strežnika: " + response);
location.reload();
});
this.on("errormultiple", function(files, response) {
// Gets triggered when there was an error sending the files.
// Maybe show form again, and notify user of error
});
}
}
</script>
</body>
</html>