registration, charts, contracts history

This commit is contained in:
msinkec 2021-08-22 19:07:19 +02:00
parent 540f54dd06
commit 7da73f7d6a
20 changed files with 13999 additions and 489 deletions

View File

@ -13,6 +13,6 @@ WORKDIR /usr/src/portal-webapp
RUN apt-get update && apt-get -y install wkhtmltopdf && \
rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir pdfkit flask==1.1.4 flask-dropzone flask-log-request-id flask-login Flask-SQLAlchemy alembic flask-migrate==2.7.0 Flask-script psycopg2 gunicorn pdfkit Werkzeug==1.0.1
RUN pip3 install --no-cache-dir pdfkit flask==1.1.4 flask-dropzone flask-log-request-id flask-login Flask-SQLAlchemy alembic flask-migrate==2.7.0 Flask-script psycopg2 gunicorn pdfkit Werkzeug==1.0.1, PyJWT
ENTRYPOINT ["./entrypoint.sh"]

326
app.py
View File

@ -5,7 +5,7 @@ import configparser
from pathlib import Path
from werkzeug.security import check_password_hash
from flask import Flask, render_template, request, redirect, flash, safe_join, send_file
from flask import Flask, render_template, request, redirect, flash, safe_join, send_file, jsonify, url_for
from flask_dropzone import Dropzone
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
@ -15,7 +15,6 @@ from portal.model import db, RegisteredUser
import portal.base
import portal.solar
import portal.regular
import portal.predavanja
# TODO: Implement user registration.
@ -44,12 +43,9 @@ MAX_FILES_PER_UPLOAD = int(config['MAX_FILES_PER_UPLOAD'])
CONTRACT_CLIENT_CONTACT = config['CONTRACT_CLIENT_CONTACT']
MAIL_SUBJECT = config['MAIL_SUBJECT']
MAIL_BODY = config['MAIL_BODY']
MAIL_SUBJECT_PREDAVANJA = config['MAIL_SUBJECT_PREDAVANJA']
MAIL_BODY_PREDAVANJA = config['MAIL_BODY_PREDAVANJA']
SQL_CONN_STR = config['SQL_CONN_STR']
DESC_PREVODI = config['DESC_PREVODI']
DESC_GIGAFIDA = config['DESC_GIGAFIDA']
DESC_PREDAVANJA = config['DESC_PREDAVANJA']
if 'UPLOADS_DIR' in config:
UPLOADS_DIR = Path(config['UPLOADS_DIR'])
@ -83,10 +79,6 @@ if 'PORTALDS4DS1_MAIL_SUBJECT' in os.environ:
MAIL_SUBJECT = os.environ['PORTALDS4DS1_MAIL_SUBJECT']
if 'PORTALDS4DS1_MAIL_BODY' in os.environ:
MAIL_BODY = os.environ['PORTALDS4DS1_MAIL_BODY']
if 'PORTALDS4DS1_MAIL_SUBJECT_PREDAVANJA' in os.environ:
MAIL_SUBJECT_PREDAVANJA = os.environ['PORTALDS4DS1_MAIL_SUBJECT_PREDAVANJA']
if 'PORTALDS4DS1_MAIL_BODY_PREDAVANJA' in os.environ:
MAIL_BODY_PREDAVANJA = os.environ['PORTALDS4DS1_MAIL_BODY_PREDAVANJA']
if 'PORTALDS4DS1_SQL_CONN_STR' in os.environ:
SQL_CONN_STR = os.environ['PORTALDS4DS1_SQL_CONN_STR']
if 'PORTALDS4DS1_DESC_PREVODI' in os.environ:
@ -94,7 +86,7 @@ if 'PORTALDS4DS1_DESC_PREVODI' in os.environ:
if 'PORTALDS4DS1_DESC_GIGAFIDA' in os.environ:
DESC_GIGAFIDA = os.environ['PORTALDS4DS1_DESC_GIGAFIDA']
ENABLED_CORPUSES = ['prevodi', 'gigafida', 'solar', 'predavanja']
ENABLED_CORPUSES = ['prevodi', 'gigafida', 'solar']
CORPUSES_LOGIN_REQUIRED = ['solar']
@ -147,25 +139,18 @@ upload_handler_solar = portal.solar.UploadHandlerSolar(
MAX_FILES_PER_UPLOAD=MAX_FILES_PER_UPLOAD
)
upload_handler_predavanja = portal.predavanja.UploadHandlerPredavanja(
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_PREDAVANJA,
MAIL_BODY=MAIL_BODY_PREDAVANJA,
CONTRACT_CLIENT_CONTACT=CONTRACT_CLIENT_CONTACT,
MAX_FILES_PER_UPLOAD=MAX_FILES_PER_UPLOAD
)
# Use flask-login to manage user sessions where they are required.
login_manager = LoginManager(app)
login_manager.init_app(app)
def redirect_url(default='/'):
return request.args.get('next') or \
request.referrer or \
url_for(default)
@app.route('/')
def index():
return render_template('index.html')
@ -180,8 +165,6 @@ def index_corpus(corpus_name):
description = DESC_PREVODI
elif corpus_name == 'gigafida':
description = DESC_GIGAFIDA
elif corpus_name == 'predavanja':
return render_template('basic-predavanja.html', description=DESC_PREDAVANJA, max_files=MAX_FILES_PER_UPLOAD)
elif corpus_name == 'solar':
if current_user.is_authenticated:
return redirect('/solar/oddaja')
@ -198,15 +181,17 @@ def load_user(user_id):
@app.route('/solar/login')
def login_get():
return render_template('login.html', corpus_name='solar', title='ŠOLAR')
def solar_login_get():
return render_template('solar-login.html')
@app.route('/<corpus_name>/login', methods=['POST'])
def login_post(corpus_name):
if corpus_name not in ENABLED_CORPUSES or corpus_name not in CORPUSES_LOGIN_REQUIRED:
return '', 404
@app.route('/solar/register')
def solar_register_get():
return render_template('solar-register.html')
@app.route('/solar/login', methods=['POST'])
def solar_login_post():
email = request.form.get('email')
password = request.form.get('password')
remember = True if request.form.get('remember') else False
@ -215,23 +200,59 @@ def login_post(corpus_name):
if not user or not check_password_hash(user.pass_hash, password):
flash('Napačni podatki za prijavo. Poskusite ponovno.')
return redirect('/{}/login'.format(corpus_name))
return redirect('/solar/login')
if not user.active:
flash('Vaš uporabniški račun še ni bil aktiviran.')
return redirect('/{}/login'.format(corpus_name))
# Check if user is authorized to log into this corpus. Admins are an exception.
if not portal.base.has_user_corpus_access(user.id, corpus_name):
flash('Nimate dostopa do tega korpusa.')
return redirect('/{}/login'.format(corpus_name))
return redirect('/solar/login')
#portal.base.add_user_session(user.id)
login_user(user, remember=remember)
if corpus_name == 'solar':
return redirect('/solar/oddaja')
return '', 404
return redirect('/solar/oddaja')
@app.route('/solar/register', methods=['POST'])
def solar_register_post():
name = request.form.get('name')
email = request.form.get('email')
password = request.form.get('password')
user = RegisteredUser.query.filter_by(email=email).first()
if user:
flash('Uporabniški račun s tem emailom je že registriran.')
return redirect('/solar/register')
if not name:
flash('Prazno polje za ime.')
return redirect('/solar/register')
if len(name) > 100:
flash('Predolgo ime.')
return redirect('/solar/register')
if not email:
flash('Prazno polje za elektronsko pošto.')
return redirect('/solar/register')
if len(email) > 100:
flash('Predolgi email naslov')
return redirect('/solar/register')
elif not re.search(portal.base.REGEX_EMAIL, email):
flash('Email napačnega formata.')
return redirect('/solar/register')
if not password:
flash('Prazno polje za geslo.')
return redirect('/solar/register')
if len(password) > 100:
flash('Predolgo geslo.')
return redirect('/solar/register')
portal.base.register_new_user(name, email, password, active=False)
flash('Uspešna registracija.')
return redirect('/solar/login')
# TODO: Move solar stuff to seperate file using Flask blueprints.
@ -247,10 +268,15 @@ def logout():
@app.route('/solar/<path:text>')
@login_required
def solar(text):
if not portal.base.has_user_corpus_access(current_user.id, 'solar'):
return '', 404
is_admin = current_user.role == 'admin'
current_user_institution = portal.base.get_user_institution(current_user.id)
if current_user_institution:
current_user_institution_moderator = portal.base.is_institution_moderator(current_user.id, current_user_institution.id)
else:
current_user_institution_moderator = False
if text.startswith('oddaja/') or text == 'oddaja':
return render_template('solar-oddaja.html')
return render_template('solar-oddaja.html', is_admin=is_admin, is_institution_moderator=current_user_institution_moderator)
elif text.startswith('zgodovina/') or text == 'zgodovina':
upload_items = portal.solar.get_upload_history(current_user.id)
uploader_names = []
@ -263,7 +289,7 @@ def solar(text):
else:
institution_names.append(institution.name)
return render_template('solar-zgodovina.html', upload_history=upload_items, uploader_names=uploader_names,
institution_names=institution_names)
institution_names=institution_names, is_admin=is_admin, is_institution_moderator=current_user_institution_moderator)
elif text.startswith('pogodbe/') or text == 'pogodbe':
# Check for ownload contract request.
match = re.match('^pogodbe/([a-z0-9_]+\.pdf)$', text)
@ -281,72 +307,154 @@ def solar(text):
return '', 404
user_obj = portal.base.get_user_obj(current_user.get_id())
institutions = portal.base.get_user_institutions(user_obj.id)
institution = portal.base.get_user_institution(user_obj.id)
contracts_students = []
contract_school = []
enable_upload_school_contract = False
show_upload_form = False
if len(institutions) > 0:
collaborators = []
if institution:
collaborators = portal.base.get_all_active_institution_users(institution.id)
show_upload_form = True
institution = portal.base.get_user_institutions(user_obj.id)[0]
contracts_students = portal.solar.get_institution_student_contracts(institution.id)
contract_school = portal.solar.get_institution_contract(institution.id)
if portal.base.is_institution_moderator(user_obj.id, institution.id):
contracts_students = portal.solar.get_institution_student_contracts(institution.id)
enable_upload_school_contract = True
else:
contracts_students = portal.solar.get_institution_student_contracts(institution.id, user_obj.id)
return render_template('solar-pogodbe.html', contracts_students=contracts_students,
contract_school=contract_school,
enable_upload_school_contract=enable_upload_school_contract,
show_upload_form=show_upload_form)
show_upload_form=show_upload_form,
collaborators=collaborators,
is_admin=is_admin, is_institution_moderator=current_user_institution_moderator)
elif text.startswith('admin/') or text == 'admin':
solar_users = portal.base.get_all_active_users()
users = portal.base.get_all_active_users_join_institutions()
inactive_users = portal.base.get_all_inactive_users()
solar_institutions = portal.solar.get_all_institutions()
if current_user.role == 'admin':
return render_template('solar-admin.html', users=solar_users, institutions=solar_institutions)
if is_admin:
return render_template('solar-admin.html', users=users,
institutions=solar_institutions, inactive_users=inactive_users)
elif text.startswith('manage-institution/') or text == 'manage-institution':
institution = portal.base.get_user_institution(current_user.id)
if portal.base.is_institution_moderator(current_user.id, institution.id):
solar_users = portal.base.get_all_active_users()
institution_users = portal.base.get_all_active_institution_users(institution.id)
return render_template('solar-manage-institution.html', users=solar_users,
institution_users=institution_users)
return '', 404
@app.route('/solar/pogodbe', methods=['POST'])
@login_required
def solar_upload_contract():
if not portal.base.has_user_corpus_access(current_user.id, 'solar'):
return '', 404
return upload_handler_solar.handle_contract_upload(request, current_user.get_id())
@app.route('/<corpus_name>/adduser', methods=['POST'])
@app.route('/solar/adduser', methods=['POST'])
@login_required
def solar_add_user(corpus_name):
def solar_add_user():
if not portal.base.is_admin(current_user.id):
return '', 404
if not corpus_name in ENABLED_CORPUSES:
return '', 404
name = request.form['name']
email = request.form['email']
password = request.form['password']
name = request.form.get('name')
email = request.form.get('email')
password = request.form.get('password')
if not name:
return 'Prazno polje za ime.'
flash('Prazno polje za ime.')
return redirect(redirect_url())
if len(name) > 100:
return 'Predolgo ime.'
flash('Predolgo ime.')
return redirect(redirect_url())
if not email:
return 'Prazno polje za elektronsko pošto.'
flash('Prazno polje za elektronsko pošto.')
return redirect(redirect_url())
if len(email) > 100:
return 'Predolgi email naslov'
flash('Predolg email naslov.')
return redirect(redirect_url())
elif not re.search(portal.base.REGEX_EMAIL, email):
return 'Email napačnega formata.'
flash('Email napačnega formata.')
return redirect(redirect_url())
if not password:
return 'Prazno polje za geslo.'
flash('Prazno polje za geslo.')
return redirect(redirect_url())
if len(password) > 100:
return 'Predolgo geslo.'
flash('Predolgo geslo.')
return redirect(redirect_url())
portal.base.register_new_user(name, email, password)
return 'Uporabnik je bil dodan.'
flash('Uporabnik je bil uspešno dodan.')
return redirect(redirect_url())
@app.route('/solar/activateuser', methods=['POST'])
@login_required
def solar_activate_user():
if not portal.base.is_admin(current_user.id):
return '', 404
user_id = request.form.get('id')
if not user_id:
flash('Prazno polje za ID uporabnika.')
return redirect(redirect_url())
rowcount = portal.base.activate_user(user_id)
if rowcount == 0:
return '', 404
flash('Uporabnik je bil aktiviran.')
return redirect(redirect_url())
@app.route('/solar/forgotpass')
def solar_forgotpass():
return render_template('solar-forgotpass.html')
@app.route('/solar/sendresetpass', methods=['POST'])
def solar_sendresetpass():
email = request.form.get('email')
portal.base.send_resetpass_mail(email, upload_handler_regular.config)
flash('Povezava za ponastavitev gesla je bila poslana na vpisan email naslov.')
return redirect(redirect_url())
@app.route('/solar/resetpass/<token>')
def solar_resetpass(token):
user = portal.base.verify_reset_token(token)
if not user:
return '', 404
return render_template('solar-resetpass.html', user=user, token=token)
@app.route('/solar/resetpass/<token>', methods=['POST'])
def solar_resetpass_post(token):
new_password = request.form.get('new_password')
user = portal.base.verify_reset_token(token)
if not user:
return '', 404
rowcount = portal.base.update_user_password(user.id, new_password)
if rowcount == 0:
return '', 404
return 'Ponastavitev gesla uspešna.'
@app.route('/solar/topuploads')
@login_required
def solar_topuploads_srednje():
return jsonify(portal.solar.get_top_uploading_institutions())
@app.route('/solar/deluser', methods=['POST'])
@ -364,44 +472,76 @@ def add_institution(corpus_name):
if not corpus_name in ENABLED_CORPUSES:
return '', 404
name = request.form['name']
region = request.form['region']
name = request.form.get('name')
region = request.form.get('region')
if not name:
return 'Prazno polje za ime.'
flash('Prazno polje za ime.')
return redirect(redirect_url())
if len(name) > 100:
return 'Predolgo ime.'
flash('Predolgo ime.')
return redirect(redirect_url())
if not region:
return 'Prazno polje za regijo.'
flash('Prazno polje za regijo.')
return redirect(redirect_url())
if len(region) > 100:
return 'Predolgi niz za regijo.'
flash('Predolgi niz za regijo.')
return redirect(redirect_url())
institution_id = portal.base.add_institution(name, region)
portal.base.grant_institution_corpus_access(institution_id, corpus_name)
return 'Institucija je bila dodana.'
flash('Institucija je bila dodana.')
return redirect(redirect_url())
@app.route('/<corpus_name>/addusertoinstitution', methods=['POST'])
@login_required
def add_user_institution_mapping(corpus_name):
if not portal.base.is_admin(current_user.id):
if not corpus_name in ENABLED_CORPUSES:
return '', 404
institution_id = request.form.get('institution_id')
if not institution_id:
institution = portal.base.get_user_institution(current_user.id)
if institution:
institution_id = institution.id
if not (portal.base.is_admin(current_user.id) or portal.base.is_institution_moderator(current_user.id, institution_id)):
return '', 404
user_id = request.form['user_id']
role = request.form['role']
if role not in ['moderator', 'user']:
return '', 404
if portal.base.get_user_institution(user_id):
flash('Uporabnik je že dodeljen instituciji. Dodeljevanje večim institucijam '\
'zaenkrat ni implementirano.')
return redirect(redirect_url())
portal.base.add_user_to_institution(user_id, institution_id, role)
flash('Uporabnik je bil dodeljen instituciji.')
return redirect(redirect_url())
@app.route('/<corpus_name>/deluserfrominstitution', methods=['POST'])
@login_required
def del_user_institution_mapping(corpus_name):
institution = portal.base.get_user_institution(current_user.id)
if not portal.base.is_admin(current_user.id) \
and not portal.base.is_institution_moderator(current_user.id, institution.id):
return '', 404
if not corpus_name in ENABLED_CORPUSES:
return '', 404
user_id = request.form['user_id']
institution_id = request.form['institution_id']
role = request.form['role']
if role not in ['moderator', 'user']:
return '', 404
# TODO: remove this restriction
if len(portal.base.get_user_institutions(user_id)) > 0:
return 'Uporabnik je že dodeljen instituciji. Dodeljevanje večim institucijam '\
'zaenkrat ni implementirano.'
if not portal.base.is_institution_member(user_id, institution.id):
flash('Uporabnik ni član vaše institucije.')
return redirect(redirect_url())
portal.base.add_user_to_institution(user_id, institution_id, role)
return 'Uporabnik je bil dodeljen instituciji.'
portal.base.del_user_from_institution(user_id, institution.id)
flash('Uporabnik je bil odstranjen iz institucije.')
return redirect(redirect_url())
@app.route('/<corpus_name>/delinstitution', methods=['POST'])
@login_required
@ -420,11 +560,9 @@ def handle_upload(corpus_name):
if corpus_name == 'solar':
if not current_user.is_authenticated:
return '', 404
if not portal.base.has_user_corpus_access(current_user.id, corpus_name):
return '', 404
#if not portal.base.has_user_corpus_access(current_user.id, corpus_name):
# return '', 404
return upload_handler_solar.handle_upload(request, current_user.get_id())
elif corpus_name == 'predavanja':
return upload_handler_predavanja.handle_upload(request)
else:
return upload_handler_regular.handle_upload(request, corpus_name)

View File

@ -12,17 +12,9 @@ UPLOADS_DIR=./uploads
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/zbiranje-besedil">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>
DESC_PREDAVANJA=<h2 id="subtitle">Predavanja</h2>
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.
Lep pozdrav,
ekipa RSDO
MAIL_SUBJECT_PREDAVANJA=Projekt ON ({upload_id})
MAIL_BODY_PREDAVANJA=Spoštovani,
sodelavci projekta ON se vam zahvaljujemo za prispevek in sodelovanje. Morebitna vprašanja pošljite na naslov predavajalnik@cjvt.si.
Hvala in lep pozdrav,
ekipa CJVT UL

View File

@ -1,3 +1,4 @@
import os
import hashlib
import time
import ssl
@ -20,6 +21,8 @@ from email.mime.application import MIMEApplication
import pdfkit
from jinja2 import Environment, FileSystemLoader
import jwt
from werkzeug.security import generate_password_hash
from . model import db, UploadRegular, UploadSolar, RegisteredUser, UserInstitutionMapping, Institution, InstitutionContract, CorpusAccess
@ -238,8 +241,11 @@ class UploadHandler:
return None
def get_user_institutions(user_id):
return UserInstitutionMapping.query.filter_by(user=user_id).all()
def get_user_institution(user_id):
mapping = UserInstitutionMapping.query.filter_by(user=user_id).first()
if mapping:
return Institution.query.filter_by(id=mapping.institution).first()
return None
def has_user_corpus_access(user_id, corpus_name):
@ -252,13 +258,11 @@ def has_user_corpus_access(user_id, corpus_name):
return True
# Check if user belongs to an institution, that has access to this corpus.
institutions = get_user_institutions(user_id)
institution = get_user_institution(user_id)
has_access = False
for institution in institutions:
row = CorpusAccess.query.filter_by(institution=institution.id, corpus=corpus_name).first()
if row:
has_access = True
break
row = CorpusAccess.query.filter_by(institution=institution.id, corpus=corpus_name).first()
if row:
has_access = True
return has_access
@ -322,15 +326,86 @@ def add_user_to_institution(user_id, institution_id, role):
return model_obj.id
def activate_user(user_id):
rowcount = db.session.query(RegisteredUser).filter_by(id=user_id).update({'active': True})
db.session.commit()
return rowcount
def update_user_password(user_id, new_password):
phash = generate_password_hash(new_password)
rowcount = db.session.query(RegisteredUser).filter_by(id=user_id).update({'pass_hash': pass_hash})
db.session.commit()
return rowcount
def del_user_from_institution(user_id, institution_id):
db.session.query(UserInstitutionMapping).filter(UserInstitutionMapping.institution == institution_id).filter(UserInstitutionMapping.user == user_id).delete()
db.session.commit()
def get_all_active_users():
return RegisteredUser.query.filter_by(active=True).all()
return RegisteredUser.query.filter_by(active=True).order_by(RegisteredUser.id).all()
def get_all_inactive_users():
return RegisteredUser.query.filter_by(active=False).order_by(RegisteredUser.id).all()
def get_all_active_users_join_institutions():
#return RegisteredUser.query.filter_by(active=True).order_by(RegisteredUser.id).all()
return db.session.query(RegisteredUser, UserInstitutionMapping).outerjoin(UserInstitutionMapping,
RegisteredUser.id == UserInstitutionMapping.user).order_by(RegisteredUser.id).all()
def get_all_active_institution_users(institution_id):
return RegisteredUser.query.filter_by(active=True).join(UserInstitutionMapping,
RegisteredUser.id == UserInstitutionMapping.user).filter(UserInstitutionMapping.institution == institution_id).all()
def is_institution_moderator(user_id, institution_id):
user_inst_mapping = UserInstitutionMapping.query.filter_by(user=user_id).first()
user_inst_mapping = UserInstitutionMapping.query.filter_by(user=user_id).filter_by(institution=institution_id).first()
if not user_inst_mapping:
return False
if user_inst_mapping.role != 'moderator':
return False
return True
def is_institution_member(user_id, institution_id):
user_inst_mapping = UserInstitutionMapping.query.filter_by(user=user_id).filter_by(institution=institution_id).first()
if not user_inst_mapping:
return False
return True
def get_password_reset_token(email, expires=500):
return jwt.encode({'reset_password': email,
'exp': time() + expires},
key=os.getenv('APP_SECRET_KEY'), algorithm='HS256')
def verify_reset_token(token):
try:
email = jwt.decode(token,
key=os.getenv('APP_SECRET_KEY'), algorithms=["HS256"])['reset_password']
except Exception as e:
logging.error(e)
return
return RegisteredUser.query.filter_by(email=email).first()
def send_resetpass_mail(email, config):
jwt_token = get_password_reset_token(email)
text = '''
Zahtevali ste ponastavitev gesla vašega uporabniškega računa.
Geslo lahko ponastavite na naslednji povezavi: https://zbiranje.slovenscina.eu/solar/resetpass/{}'''.format(
'https://zbiranje.slovenscina.eu/solar/resetpass/{}'.format(jwt_token))
# Create a secure SSL context
context = ssl.create_default_context()
try:
with SMTP_SSL(config['MAIL_HOST'], config['SMTP_PORT'], context=context) as server:
server.login(config['MAIL_LOGIN'], config['MAIL_PASS'])
server.sendmail(config['MAIL_LOGIN'], email, text)
except Exception:
traceback.print_exc()

View File

@ -1,140 +0,0 @@
import logging
import traceback
import re
from datetime import datetime
import portal.base
from portal.base import UploadHandler, ContractCreator, REGEX_EMAIL
from portal.model import db, UploadPredavanja
MAXLEN_FORM = 150
class UploadHandlerPredavanja(UploadHandler):
ENABLED_FILETYPES = None # None means all filetypes
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.contract_creator = ContractCreator(base_path=self.get_uploads_subdir('contracts'),
template_path='contract/predavanja.html')
def generate_upload_contract_pdf(self, upload_metadata):
form_data = upload_metadata['form_data']
files_table_str = []
for file_name in upload_metadata['file_names']:
files_table_str.append('<tr><td style="text-align: center;">')
files_table_str.append(file_name)
files_table_str.append('</td></tr>')
files_table_str = ''.join(files_table_str)
data = {
'ime_priimek': form_data['ime'],
'files_table_str': files_table_str
}
self.contract_creator.create_pdf(upload_metadata['contract_file'], data)
@staticmethod
def store_metadata(upload_metadata):
timestamp = datetime.fromtimestamp(upload_metadata['timestamp'])
form_data = upload_metadata['form_data']
file_hashes = upload_metadata['file_hashes_dict']
sorted_f_hashes = list(file_hashes.values())
sorted_f_hashes.sort()
# Normalize keywords list
keywords_list = []
for keyword in form_data['kljucne-besede'].split(','):
keyword = keyword.strip()
keywords_list.append(keyword)
keywords = ','.join(keywords_list)
try:
model_obj = UploadPredavanja(
upload_hash=upload_metadata['upload_id'],
timestamp=timestamp,
name=form_data['ime'],
address=form_data['naslov-predavanja'],
subject=form_data['predmet'],
faculty=form_data['fakulteta'],
email=form_data['email'],
phone=form_data.get('phone'),
keywords=keywords,
agree_publish_future=form_data['javna-objava-prihodnost'],
agree_machine_translation=True if 'strojno-prevajanje' in form_data else False,
agree_news_cjvt=True if 'obvestila' in form_data else False,
file_contract=upload_metadata['contract_file'],
upload_file_hashes=sorted_f_hashes,
)
db.session.add(model_obj)
db.session.commit()
except Exception:
traceback.print_exc()
def handle_upload(self, request):
err = self.check_upload_request(request)
if err:
return err, 400
err = self.check_form(request.form)
if err:
return err, 400
# Parse request.
upload_metadata = self.extract_upload_metadata('predavanja', request)
logging.info('Upload for "predavanja" with id "{}" supplied form data: {}'.format(
upload_metadata['upload_id'], str(upload_metadata['form_data'])))
# Store uploaded files to disk.
self.store_datafiles(request.files, upload_metadata)
# Store metadata to database.
self.store_metadata(upload_metadata)
# Send confirmation mail
self.send_confirm_mail(upload_metadata)
return 'Uspešno ste oddali datotek(e). Št. datotek: {}'.format(len(request.files))
@staticmethod
def check_form(form):
name = form.get('ime')
address = form.get('naslov-predavanja')
subject = form.get('predmet')
faculty = form.get('fakulteta')
email = form.get('email')
phone = form.get('telefon')
keywords = form.get('kljucne-besede')
agree_publish_future = form.get('javna-objava-prihodnost')
if not agree_publish_future:
return 'Manjkajoča izbrana vrednost pri polju za javno objavo.'
if not name \
or not address \
or not subject \
or not faculty \
or not email \
or not keywords:
return 'Izpolnite vsa obvezna polja.'
#for keyword in keywords.split(','):
# keyword = keyword.strip()
# if keyword.isspace() or not keyword.replace(' ', '').isalpha():
# return 'Ključna beseda "{}" ni pravilnega formata.'.format(keyword)
if not re.search(REGEX_EMAIL, email):
return 'Email napačnega formata.'
for key, val in form.items():
if key == 'kljucne-besde':
if len(val) > 500:
return 'Polje "{}" presega dolžino {} znakov.'.format(key, 500)
else:
if len(val) > MAXLEN_FORM:
return 'Polje "{}" presega dolžino {} znakov.'.format(key, MAXLEN_FORM)

View File

@ -1,12 +1,12 @@
import logging
import re
import traceback
import hashlib
from datetime import datetime
from sqlalchemy import desc, exists
from sqlalchemy import desc
from collections import Counter
from portal.base import UploadHandler, get_user_institutions, has_user_corpus_access
from portal.model import db, UploadSolar, ContractsSolar, RegisteredUser, Institution, InstitutionContract, UserInstitutionMapping, CorpusAccess
from portal.base import UploadHandler, get_user_institution, has_user_corpus_access
from portal.model import UploadSolar, ContractsSolar, RegisteredUser, Institution, InstitutionContract, UserInstitutionMapping, CorpusAccess
VALID_PROGRAMS = {'OS', 'SSG', 'MGP', 'ZG', 'NPI', 'SPI', 'SSI', 'PTI'}
@ -27,8 +27,7 @@ class UploadHandlerSolar(UploadHandler):
sorted_f_hashes = list(file_hashes.values())
sorted_f_hashes.sort()
# If user is mapped to multiple institutions, let him chose in name of which one he makes the upload.
institution_id = get_user_institutions(user_id)[0].id
institution_id = get_user_institution(user_id).id
model_obj = UploadSolar(
upload_user = user_id,
@ -147,7 +146,7 @@ class UploadHandlerSolar(UploadHandler):
if program not in VALID_PROGRAMS:
return 'Invalid program "{}"'.format(program)
if predmet not in VALID_SUBJECTS:
return 'Invalid subject "{}"'.format(premdet)
return 'Invalid subject "{}"'.format(predmet)
if letnik < 1 or letnik > 9:
return 'Invalid grade: {}'.format(letnik)
if vrsta not in VALID_TEXT_TYPES:
@ -176,14 +175,30 @@ def get_all_institutions():
return res
def get_institution_student_contracts(institution_id):
return ContractsSolar.query.filter_by(institution=institution_id, contract_type='ucenci-starsi').all()
def get_institution_student_contracts(institution_id, user_id=None):
if not user_id:
return ContractsSolar.query.filter_by(institution=institution_id, contract_type='ucenci-starsi').all()
return ContractsSolar.query.filter_by(institution=institution_id, contract_type='ucenci-starsi', upload_user=user_id).all()
def get_institution_contract(institution_id):
return InstitutionContract.query.filter_by(institution=institution_id, corpus='solar').order_by(desc(InstitutionContract.timestamp)).first()
def get_top_uploading_institutions():
res = dict()
institutions = get_all_institutions()
for institution in institutions:
uploads = UploadSolar.query.filter_by(institution=institution.id).all()
for upload in uploads:
if institution.name not in res:
res[institution.name] = 0
res[institution.name] += len(upload.upload_file_hashes)
if len(res) >= 5:
return dict(sorted(res.items(), key=lambda x:x[1], reverse=True)[:5])
return dict(sorted(res.items(), key=lambda x:x[1], reverse=True))
def get_all_active_users():
# TODO: do filtering purely within an SQL query
res = []

13222
static/chart.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -119,6 +119,89 @@ label {
margin-bottom: 30px;
}
.container-title {
font-family: Roboto;
font-style: normal;
font-weight: bold;
font-size: 16px;
line-height: 14px;
color: #46535b;
text-align: center;
margin-bottom: 30px;
}
#history-container {
height: 500px;
overflow-y: auto;
}
#contract-container {
height: 300px;
overflow-y: auto;
padding: 20px;
}
.contract-item {
height: 50px;
margin: 5px;
border: 0px;
border-bottom: 2px solid #c4c4c4;
}
.contract-item-title {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
color: #46535b;
}
.contract-item-button {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
}
.contract-item-date {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 10px;
color: #46535b;
}
#collaborators-container {
height: 370px;
background: #f5f5f5;
margin: 25px;
padding:5px;
border-radius: 10px;
}
.collaborators-item {
height: 30px;
margin: 5px;
border: 0px;
border-bottom: 2px solid #c4c4c4;
}
.collaborators-item-name {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
color: #46535b;
}
#awards-container {
height: 250px;
background: #f5f5f5;
margin: 25px;
padding:5px;
border-radius: 10px;
}
#button-submit {
display: flex;
flex-direction: row;

View File

@ -1,199 +0,0 @@
<!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: 831px;
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 id="main-window" style="top: 45%;">
<div id="rect1" style="height: 831px;">
<div id="logo-container" style="top: -7.8%;">
<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</h1>
<div class="form-text">{{description|safe}}</div>
<label for="ime">* Ime in priimek:</label>
<input type="text" id="ime" name="ime" required="required"/>
<label for="naslov-predavanja">* Naslov predavanja:</label>
<input type="text" id="naslov-predavanja" name="naslov-predavanja" required="required"/>
<label for="predmet">* Predmet:</label>
<input type="text" id="predmet" name="predmet" required="required"/>
<label for="fakulteta">* Članica:</label>
<input type="text" id="fakulteta" name="fakulteta" required="required"/>
<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"/>
<label for="kljucne-besede">* Ključne besede (ločene z vejico):</label>
<input type="text" id="kljucne-besede" name="kljucne-besede" required="required"/>
<br>
<div style="display:flex; flex-direction: row; justify-content: left; align-items: center">
<label style="width: 95%; text-transform: none; font-size: 12px;"><b>*Privolitev:</b><br>Strinjam se, da Univerza v Ljubljani uporabi posnetek naloženega predavanja v okviru projekta za strojno prevajanje predavanj ON. Dostop do posnetka bodo imeli izključno sodelavci projekta za namen transkripcije govora.</label>
<input style="width: 5%;" type="checkbox" name="strojno-prevajanje" value="strojno-prevajanje">
</div>
<div style="display: flex; flex-direction: row; justify-content: left; align-items: center; width: 310px;">
<label style="text-transform: none; font-size: 12px;"><b>Objava posnetka na portalu ON:</b><br>Ali bi se v prihodnosti strinjali z objavo posnetka na portalu sistema ON? (V primeru strinjanja bi podpisali poseben dogovor o pogojih objave.)</label>
<div style="display: inline-block;">
<input type="radio" name="javna-objava-prihodnost" value="da" style="display: inline; float: left; width: 20px;" >
<label for="da" style="display: inline; float: right; position: absolute; margin-top: 5px;">Da</label><br>
<input type="radio" name="javna-objava-prihodnost" value="morda" style="display: inline; float: left; width: 20px;">
<label for="morda" style="display: inline; float: right; position: absolute; margin-top: 10px;">Morda</label><br>
<input type="radio" name="javna-objava-prihodnost" value="ne" style="display: inline; float: left; width: 20px;">
<label for="ne" style="display: inline; float: right; position: absolute; margin-top: 18px;">Ne</label><br>
</div>
</div>
<br>
<div style="display:flex; flex-direction: row; justify-content: left; align-items: center">
<label style="width: 95%; text-transform: none; font-size: 12px;"><b>Obvestila:</b><br>Želim, da me Center za jezikovne vire in tehnologije UL obvešča o novicah v zvezi s sistemom za strojno prevajanje predavanj ON.</label>
<input style="width: 5%;" type="checkbox" name="obvestila" value="obvestila">
</div>
<br>
<a class="form-text" href="https://www.cjvt.si/obvestilo-o-obdelavi-osebnih-podatkov/" style="cursor: pointer;">Obvestilo o obdelavi osebnih podatkov</a>
<button id="button-submit" type="submit" style="top: 745px;">Oddaj</button>
</div>
<div class="dropzone-previews"></div>
</form>
</div>
</div>
<!--{{ dropzone.load_js() }}-->
<script src="/static/dropzone.js"></script>
<script>
/////////////////////////
// Dropzone //
/////////////////////////
var btnSubmit = document.getElementById("button-submit");
var form = document.forms["my-dropzone"];
function isEmptyOrSpaces(str){
return str == null || str.match(/^ *$/) !== null;
}
const reEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
//const reKeyword = /^[a-zA-Zščđ枊ČĐĆŽ, \-]+$/;
Dropzone.options.myDropzone = { // The camelized version of the ID of the form element
url: "/predavanja/upload",
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: {{max_files}},
paramName: "file", // The name that will be used to transfer the file
maxFilesize: 10000, // MB
timeout: 10000000, // milliseconds
//acceptedFiles: ".txt, .csv, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx",
maxFiles: {{max_files}},
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: "Največje število datotek že doseženo.",
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 naslov = form["naslov-predavanja"].value;
var predmet = form["predmet"].value;
var fakulteta = form["fakulteta"].value;
var kljucneBesede = form["kljucne-besede"].value;
var email = form["email"].value;
var telefon = form["telefon"].value;
var privolitev = form["strojno-prevajanje"].checked;
var javnaObjava = form["javna-objava-prihodnost"];
if (isEmptyOrSpaces(ime) ||
isEmptyOrSpaces(naslov) ||
isEmptyOrSpaces(predmet) ||
isEmptyOrSpaces(fakulteta) ||
isEmptyOrSpaces(kljucneBesede) ||
isEmptyOrSpaces(email)) {
alert("Izpolnite vsa obvezna polja!");
} else if (!reEmail.test(email.toLowerCase())) {
alert("Email napačnega formata!");
// } else if (!reKeyword.test(kljucneBesede)) {
// alert("Ključne besede so napačnega formata! Besede ločujte z vejico. Besede naj ne vsebujejo posebnih znakov.");
} else if (ime.length > 100 ||
naslov.length > 100 ||
predmet.length > 100 ||
fakulteta.length > 100 ||
kljucneBesede.length > 100 ||
email.length > 100 ||
telefon.length > 100) {
alert("Velikost polj je omejena na 100 znakov.");
} else if (!privolitev) {
alert("Odkljukana privolitev je pogoj za oddajo.");
} else if (javnaObjava.value == "") {
alert("Izberite eno izmed možnosti pri polju za objavo na portalu ON!");
} else {
// Hand off data to dropzone
dz.processQueue();
// Clear fields and hide popup agian
btnSubmit.disabled = false;
form.reset();
}
});
// 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>

View File

@ -6,6 +6,6 @@
<body>
<a href="/prevodi">Korpus paralelnih besdil ANG-SLO</a><br>
<a href="/gigafida">Korpus Gigafida</a><br>
<a href="/predavanja">Korpus Predavanja</a><br>
<a href="/solar">Korpus Šolar</a><br>
</body>
</html>

View File

@ -6,7 +6,7 @@
<style>
.tableFixHead {
overflow-y: auto;
height: 106px;
height: 306px;
}
.tableFixHead thead th {
position: sticky;
@ -27,6 +27,13 @@
</style>
</head>
<body>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div style="background: blue;">
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<h2>Uporabniki</h2>
<h3>Dodaj uporabnika</h3>
<form action="/solar/adduser" method="post">
@ -35,7 +42,7 @@
<label for="email">Email:</label><br>
<input type="text" id="email" name="email"><br>
<label for="password">Geslo:</label><br>
<input type="text" id="password" name="password"><br>
<input type="password" id="password" name="password"><br>
<input type="submit" value="Dodaj">
</form>
<!--<h3>Odstrani uporabnika</h3>
@ -52,15 +59,16 @@
<th>ID</th>
<th>Ime in priimek</th>
<th>Email</th>
<th>ID institucije</th>
</tr>
</thead>
<tbody>
{% for item in users %}
<tr>
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.email}}</td>
</tr>
<td>{{item[0].id}}</td>
<td>{{item[0].name}}</td>
<td>{{item[0].email}}</td>
<td>{{item[1].institution}}</td>
{% endfor %}
</table>
</div>
@ -107,4 +115,30 @@
{% endfor %}
</table>
</div>
<h3>Seznam uporabnikov, ki niso aktivirani</h3>
<div class="tableFixHead">
<table>
<thead>
<tr>
<th>ID</th>
<th>Ime in priimek</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{% for item in inactive_users %}
<tr>
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.email}}</td>
</tr>
{% endfor %}
</table>
</div>
<h3>Aktiviraj uporabnika</h3>
<form action="/solar/activateuser" method="post">
<label for="id">ID uporabnika:</label>
<input type="text" id="id" name="id"><br>
<input type="submit" value="Aktiviraj">
</form>
</body>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Portal ŠOLAR</title>
<link rel="stylesheet" href="/static/style.css" type="text/css">
</head>
<body>
<div id="main-window">
<div id="rect1">
<div style="padding: 50px;">
<div id="logo-container">
<img src="/static/image/logo.svg" alt="logo"/>
</div>
<h3 id="title" style="font-size: 27px; text-align: left;">Pozabljeno geslo - ŠOLAR</h3>
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<form method="POST" action="/solar/sendresetpass">
<div>
<div>
<input type="email" name="email" placeholder="Email" autofocus="">
</div>
</div>
<button class="button-general" style="margin-top: 20px;">Pošlji povezavo za ponastavitev geslo</button>
</form>
<a href="/solar/register" class="contract-item-button">Registracija</a>
</div>
</div>
</div>
<div id="rect2" class="mock-side">
</div>
</div>
</body>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin panel - Šolar</title>
</head>
<body>
<h3>Uporabniki</h3>
TODO: odobri registracije
<form> </form>
<div> </div>
</body>

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Portal {{title}}</title>
<title>Portal ŠOLAR</title>
<link rel="stylesheet" href="/static/style.css" type="text/css">
</head>
<body>
@ -12,7 +12,7 @@
<div id="logo-container">
<img src="/static/image/logo.svg" alt="logo"/>
</div>
<h3 id="title" style="font-size: 27px; text-align: left;">Prijava - {{title}}</h3>
<h3 id="title" style="font-size: 27px; text-align: left;">Prijava - ŠOLAR</h3>
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
@ -21,7 +21,7 @@
</div>
{% endif %}
{% endwith %}