model changes, user institution history, style changes, forgot passowrd functionality, etc.

This commit is contained in:
msinkec 2021-09-02 14:40:54 +02:00
parent 7da73f7d6a
commit 788f3bc3eb
25 changed files with 551 additions and 176 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, PyJWT
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"]

136
app.py
View File

@ -123,7 +123,8 @@ upload_handler_regular = portal.regular.UploadHandlerRegular(
MAIL_SUBJECT=MAIL_SUBJECT,
MAIL_BODY=MAIL_BODY,
CONTRACT_CLIENT_CONTACT=CONTRACT_CLIENT_CONTACT,
MAX_FILES_PER_UPLOAD=MAX_FILES_PER_UPLOAD
MAX_FILES_PER_UPLOAD=MAX_FILES_PER_UPLOAD,
APP_SECRET_KEY=APP_SECRET_KEY
)
upload_handler_solar = portal.solar.UploadHandlerSolar(
@ -136,7 +137,8 @@ upload_handler_solar = portal.solar.UploadHandlerSolar(
MAIL_SUBJECT=MAIL_SUBJECT,
MAIL_BODY=MAIL_BODY,
CONTRACT_CLIENT_CONTACT=CONTRACT_CLIENT_CONTACT,
MAX_FILES_PER_UPLOAD=MAX_FILES_PER_UPLOAD
MAX_FILES_PER_UPLOAD=MAX_FILES_PER_UPLOAD,
APP_SECRET_KEY=APP_SECRET_KEY
)
@ -217,6 +219,9 @@ def solar_register_post():
name = request.form.get('name')
email = request.form.get('email')
password = request.form.get('password')
institution_name = request.form.get('institution')
institution_role = request.form.get('role')
institution = portal.base.get_institution_obj_by_name(institution_name)
user = RegisteredUser.query.filter_by(email=email).first()
@ -248,7 +253,17 @@ def solar_register_post():
flash('Predolgo geslo.')
return redirect('/solar/register')
portal.base.register_new_user(name, email, password, active=False)
if not institution:
flash('Institucija ne obstaja.')
return redirect('/solar/register')
if institution_role not in ['coordinator', 'mentor', 'other']:
flash('Neveljavna vloga v instituciji.')
return redirect('/solar/register')
user_id = portal.base.register_new_user(name, email, password, active=False)
portal.base.add_user_to_institution(user_id, institution.id, institution_role)
flash('Uspešna registracija.')
return redirect('/solar/login')
@ -270,13 +285,20 @@ def logout():
def solar(text):
is_admin = current_user.role == 'admin'
current_user_institution = portal.base.get_user_institution(current_user.id)
current_user_obj = portal.base.get_user_obj(current_user.get_id())
institution_contract = None
if current_user_institution:
current_user_institution_moderator = portal.base.is_institution_moderator(current_user.id, current_user_institution.id)
current_user_institution_coordinator = portal.base.is_institution_coordinator(current_user.id, current_user_institution.id)
institution_contract = portal.base.get_institution_contract(current_user_institution.id)
else:
current_user_institution_moderator = False
current_user_institution_coordinator = False
if text.startswith('oddaja/') or text == 'oddaja':
return render_template('solar-oddaja.html', is_admin=is_admin, is_institution_moderator=current_user_institution_moderator)
return render_template('solar-oddaja.html',
is_admin=is_admin,
institution=current_user_institution,
institution_contract=institution_contract,
is_institution_coordinator=current_user_institution_coordinator)
elif text.startswith('zgodovina/') or text == 'zgodovina':
upload_items = portal.solar.get_upload_history(current_user.id)
uploader_names = []
@ -289,58 +311,66 @@ 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, is_admin=is_admin, is_institution_moderator=current_user_institution_moderator)
elif text.startswith('pogodbe/') or text == 'pogodbe':
institution_names=institution_names, is_admin=is_admin, is_institution_coordinator=current_user_institution_coordinator)
elif text.startswith('pogodbe-institucije/') or text.startswith('pogodbe-ucencistarsi/'):
# Check for ownload contract request.
match = re.match('^pogodbe/([a-z0-9_]+\.pdf)$', text)
match = re.match('^pogodbe-(institucije|ucencistarsi)/([a-z0-9_]+\.pdf)$', text)
if match:
filename = match.group(1)
contract_type = match.group(1)
filename = match.group(2)
if len(filename) < 10:
return '', 404
prefix = filename[:2]
suffix = filename[2:]
f_hash = filename.split('.')[0]
if contract_type == 'institucije':
actual_filename = portal.base.get_actual_institution_contract_filename(f_hash)
else:
actual_filename = portal.base.get_actual_studentparent_contract_filename(f_hash)
safe_path = safe_join(str(upload_handler_solar.get_uploads_subdir('contracts')), prefix, suffix)
try:
return send_file(safe_path, as_attachment=True)
return send_file(safe_path, attachment_filename=actual_filename, as_attachment=True)
except FileNotFoundError:
return '', 404
user_obj = portal.base.get_user_obj(current_user.get_id())
institution = portal.base.get_user_institution(user_obj.id)
elif text.startswith('pogodbe/') or text == 'pogodbe':
contracts_students = []
contract_school = []
enable_upload_school_contract = False
show_upload_form = False
collaborators = []
if institution:
collaborators = portal.base.get_all_active_institution_users(institution.id)
cooperation_history = dict()
if current_user_institution:
collaborators = portal.base.get_all_active_institution_users(current_user_institution.id)
show_upload_form = True
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)
contract_school = portal.solar.get_institution_contract(current_user_institution.id)
cooperation_history = portal.base.get_institution_cooperation_history(current_user_institution.id)
if portal.base.is_institution_coordinator(current_user_obj.id, current_user_institution.id):
contracts_students = portal.solar.get_institution_student_contracts(current_user_institution.id)
enable_upload_school_contract = True
else:
contracts_students = portal.solar.get_institution_student_contracts(institution.id, user_obj.id)
contracts_students = portal.solar.get_institution_student_contracts(current_user_institution.id, current_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,
collaborators=collaborators,
is_admin=is_admin, is_institution_moderator=current_user_institution_moderator)
cooperation_history=cooperation_history,
is_admin=is_admin, is_institution_coordinator=current_user_institution_coordinator)
elif text.startswith('admin/') or text == 'admin':
users = portal.base.get_all_active_users_join_institutions()
inactive_users = portal.base.get_all_inactive_users()
users = portal.base.get_all_users_join_institutions()
inactive_users = portal.base.get_all_users_join_institutions(active=False)
solar_institutions = portal.solar.get_all_institutions()
cooperation_history = portal.base.get_cooperation_history()
if is_admin:
return render_template('solar-admin.html', users=users,
return render_template('solar-admin.html', users=users, user_cooperation_history=cooperation_history,
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):
if portal.base.is_institution_coordinator(current_user.id, current_user_institution.id):
solar_users = portal.base.get_all_active_users()
institution_users = portal.base.get_all_active_institution_users(institution.id)
institution_users = portal.base.get_all_active_institution_users(current_user_institution.id)
return render_template('solar-manage-institution.html', users=solar_users,
institution_users=institution_users)
return '', 404
@ -420,7 +450,7 @@ def solar_forgotpass():
def solar_sendresetpass():
email = request.form.get('email')
portal.base.send_resetpass_mail(email, upload_handler_regular.config)
portal.base.send_resetpass_mail(email, upload_handler_solar.config)
flash('Povezava za ponastavitev gesla je bila poslana na vpisan email naslov.')
return redirect(redirect_url())
@ -428,7 +458,7 @@ def solar_sendresetpass():
@app.route('/solar/resetpass/<token>')
def solar_resetpass(token):
user = portal.base.verify_reset_token(token)
user = portal.base.verify_reset_token(token, upload_handler_solar.config['APP_SECRET_KEY'])
if not user:
return '', 404
@ -439,7 +469,7 @@ def solar_resetpass(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)
user = portal.base.verify_reset_token(token, upload_handler_solar.config['APP_SECRET_KEY'])
if not user:
return '', 404
@ -448,7 +478,8 @@ def solar_resetpass_post(token):
if rowcount == 0:
return '', 404
return 'Ponastavitev gesla uspešna.'
flash('Ponastavitev gesla je bila uspešna.')
return redirect('/solar/login')
@app.route('/solar/topuploads')
@ -460,9 +491,12 @@ def solar_topuploads_srednje():
@app.route('/solar/deluser', methods=['POST'])
@login_required
def solar_del_user():
# TODO: check if user is institution moderator for the added users institution or is an admin
# TODO: delete from "user", "user_institution_mapping", update "institution_contract" set user to NULL
return '', 404
if not portal.base.is_admin(current_user.id):
return '', 404
user_id = request.form.get('user_id')
portal.base.del_user(user_id)
flash('Uporabnik je bil odstranjen.')
return redirect(redirect_url())
@app.route('/<corpus_name>/addinstitution', methods=['POST'])
@login_required
@ -482,11 +516,8 @@ def add_institution(corpus_name):
flash('Predolgo ime.')
return redirect(redirect_url())
if not region:
flash('Prazno polje za regijo.')
return redirect(redirect_url())
if len(region) > 100:
flash('Predolgi niz za regijo.')
if not region in portal.solar.VALID_REGIONS:
flash('Neveljavna vrednost za regijo.')
return redirect(redirect_url())
institution_id = portal.base.add_institution(name, region)
@ -506,37 +537,34 @@ def add_user_institution_mapping(corpus_name):
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)):
if not (portal.base.is_admin(current_user.id) or portal.base.is_institution_coordinator(current_user.id, institution_id)):
return '', 404
user_id = request.form['user_id']
role = request.form['role']
if role not in ['moderator', 'user']:
if role not in ['coordinator', 'mentor', 'other']:
return '', 404
if portal.base.get_user_institution(user_id):
flash('Uporabnik je že dodeljen instituciji. Dodeljevanje večim institucijam '\
'zaenkrat ni implementirano.')
flash('Uporabnik je že dodeljen instituciji.')
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'])
@app.route('/solar/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
def del_user_institution_mapping():
user_id = request.form['user_id']
institution = portal.base.get_user_institution(user_id)
if not institution:
flash('Uporabnik ni član nobene institucije.')
return redirect(redirect_url())
if not portal.base.is_institution_member(user_id, institution.id):
flash('Uporabnik ni član vaše institucije.')
if not portal.base.is_admin(current_user.id) \
and not portal.base.is_institution_coordinator(current_user.id, institution.id):
flash('Nimate ustreznih pravic za odstranitev uporabnika iz institucije.')
return redirect(redirect_url())
portal.base.del_user_from_institution(user_id, institution.id)

View File

@ -0,0 +1,37 @@
"""Added UserCooperationHistory.
Revision ID: 4c132220432f
Revises: ff356b1ef661
Create Date: 2021-09-01 09:25:43.144096
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4c132220432f'
down_revision = 'ff356b1ef661'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_cooperation',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user', sa.Integer(), nullable=False),
sa.Column('institution', sa.Integer(), nullable=False),
sa.Column('role', sa.String(), nullable=False),
sa.Column('badge_text', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['institution'], ['institution.id'], ),
sa.ForeignKeyConstraint(['user'], ['registered_user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_cooperation')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""Added school year column to user history.
Revision ID: c60b7bfaaf85
Revises: 4c132220432f
Create Date: 2021-09-02 08:36:41.711040
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c60b7bfaaf85'
down_revision = '4c132220432f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_cooperation', sa.Column('school_year', sa.String(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_cooperation', 'school_year')
# ### end Alembic commands ###

View File

@ -0,0 +1,58 @@
"""Dropped out predavanje stuff, added contract initial file name.
Revision ID: ff356b1ef661
Revises: 5ba116fc7f06
Create Date: 2021-08-26 12:45:56.036966
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'ff356b1ef661'
down_revision = '5ba116fc7f06'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('stamps_solar')
op.drop_table('upload_predavanja')
op.add_column('contracts_solar', sa.Column('original_filename', sa.String(), nullable=True))
op.add_column('institution_contract', sa.Column('original_filename', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('institution_contract', 'original_filename')
op.drop_column('contracts_solar', 'original_filename')
op.create_table('upload_predavanja',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('upload_hash', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('timestamp', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('address', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('subject', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('faculty', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('phone', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('keywords', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('file_contract', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('upload_file_hashes', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True),
sa.Column('agree_publish_future', sa.TEXT(), autoincrement=False, nullable=False),
sa.Column('agree_machine_translation', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column('agree_news_cjvt', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name='upload_predavanja_pkey')
)
op.create_table('stamps_solar',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('institution', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False),
sa.Column('file_logo', sa.TEXT(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['institution'], ['institution.id'], name='stamps_institution_fkey'),
sa.PrimaryKeyConstraint('id', name='stamps_pkey')
)
# ### end Alembic commands ###

View File

@ -25,7 +25,7 @@ import jwt
from werkzeug.security import generate_password_hash
from . model import db, UploadRegular, UploadSolar, RegisteredUser, UserInstitutionMapping, Institution, InstitutionContract, CorpusAccess
from . model import *
#REGEX_EMAIL = re.compile('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$')
@ -248,6 +248,33 @@ def get_user_institution(user_id):
return None
def get_institution_contract(institution_id):
return InstitutionContract.query.filter_by(institution=institution_id).first()
def get_institution_cooperation_history(institution_id):
#return CooperationToken.query.join(UserCooperationTokenMapping,
# UserCooperationTokenMapping.cooperation_token == CooperationToken.id).filter(UserCooperationTokenMapping.user == user_id).all()
#
res = dict()
uch_rows = UserCooperationHistory.query.filter_by(institution=institution_id).order_by(UserCooperationHistory.school_year.desc()).all()
for row in uch_rows:
if row.user not in res:
res[row.user] = {
'coordinator': [],
'moderator': [],
'other': []
}
res[row.user][row.role].append(row.school_year)
return res
def get_cooperation_history():
return UserCooperationHistory.query.all()
def has_user_corpus_access(user_id, corpus_name):
user = RegisteredUser.query.filter_by(id=user_id).first()
@ -281,6 +308,10 @@ def get_institution_obj(institution_id):
return Institution.query.filter_by(id=institution_id).first()
def get_institution_obj_by_name(institution_name):
return Institution.query.filter_by(name=institution_name).first()
def register_new_user(name, email, password, active=True, admin=False):
model_obj = RegisteredUser(
name=name,
@ -334,11 +365,18 @@ def activate_user(user_id):
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})
rowcount = db.session.query(RegisteredUser).filter_by(id=user_id).update({'pass_hash': phash})
db.session.commit()
return rowcount
def del_user(user_id):
db.session.query(UserCooperationHistory).filter(UserCooperationHistory.user == user_id).delete()
db.session.query(UserInstitutionMapping).filter(UserInstitutionMapping.user == user_id).delete()
db.session.query(RegisteredUser).filter(RegisteredUser.id == user_id).delete()
db.session.commit()
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()
@ -347,24 +385,28 @@ def del_user_from_institution(user_id, institution_id):
def get_all_active_users():
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():
def get_all_users_join_institutions(active=True):
#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()
RegisteredUser.id == UserInstitutionMapping.user).filter(RegisteredUser.active == active).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):
def is_institution_coordinator(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
if user_inst_mapping.role != 'moderator':
if user_inst_mapping.role != 'coordinator':
return False
return True
@ -376,15 +418,34 @@ def is_institution_member(user_id, institution_id):
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 get_actual_institution_contract_filename(f_hash):
contract = InstitutionContract.query.filter_by(file_contract=f_hash).first()
if contract:
return contract.original_filename
return None
def verify_reset_token(token):
def get_actual_studentparent_contract_filename(f_hash):
contract = ContractsSolar.query.filter_by(file_contract=f_hash).first()
if contract:
return contract.original_filename
return None
def get_password_reset_token(email, key, expires=600):
return jwt.encode({'reset_password': email,
'exp': int(time.time()) + expires},
key=key, algorithm='HS256')
def verify_reset_token(token, key):
try:
email = jwt.decode(token,
key=os.getenv('APP_SECRET_KEY'), algorithms=["HS256"])['reset_password']
message = jwt.decode(token,
key=key, algorithms=["HS256"])
email = message['reset_password']
exp = message['exp']
if int(time.time()) >= exp:
# Token timed out
return None
except Exception as e:
logging.error(e)
return
@ -392,13 +453,12 @@ def verify_reset_token(token):
def send_resetpass_mail(email, config):
jwt_token = get_password_reset_token(email)
jwt_token = get_password_reset_token(email, config['APP_SECRET_KEY'])
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))
Geslo lahko ponastavite na naslednji povezavi: https://zbiranje.slovenscina.eu/solar/resetpass/{}'''.format(jwt_token)
# Create a secure SSL context
context = ssl.create_default_context()

View File

@ -28,25 +28,6 @@ class UploadRegular(db.Model):
corpus_name = db.Column(db.String, nullable=False)
class UploadPredavanja(db.Model):
__tablename__ = 'upload_predavanja'
id = db.Column(db.Integer, primary_key=True)
upload_hash = db.Column(db.String, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
name = db.Column(db.String, nullable=False)
address = db.Column(db.String, nullable=False)
subject = db.Column(db.String, nullable=False)
faculty = db.Column(db.String, nullable=False)
email = db.Column(db.String, nullable=False)
phone = db.Column(db.String, nullable=True)
keywords = db.Column(db.String, nullable=False)
agree_publish_future = db.Column(db.String, nullable=False)
agree_machine_translation = db.Column(db.Boolean, default=False, nullable=False)
agree_news_cjvt = db.Column(db.Boolean, default=False, nullable=False)
file_contract = db.Column(db.String, nullable=True)
upload_file_hashes = db.Column(sqlalchemy.types.ARRAY(db.String), nullable=True)
class UploadSolar(db.Model):
__tablename__ = 'upload_solar'
id = db.Column(db.Integer, primary_key=True)
@ -65,14 +46,6 @@ class UploadSolar(db.Model):
upload_file_hashes = db.Column(sqlalchemy.types.ARRAY(db.String), nullable=True)
class StampsSolar(db.Model):
__tablename__ = 'stamps_solar'
id = db.Column(db.Integer, primary_key=True)
institution = db.Column(db.Integer, sqlalchemy.ForeignKey('institution.id'), nullable=True)
name = db.Column(db.String, nullable=False)
file_logo = db.Column(db.String, nullable=True)
class ContractsSolar(db.Model):
__tablename__ = 'contracts_solar'
id = db.Column(db.Integer, primary_key=True)
@ -80,6 +53,7 @@ class ContractsSolar(db.Model):
upload_user = db.Column(db.Integer, sqlalchemy.ForeignKey('registered_user.id'), nullable=True)
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
file_contract = db.Column(db.String, nullable=False)
original_filename = db.Column(db.String, nullable=True)
contract_type = db.Column(db.String, nullable=False)
@ -100,7 +74,29 @@ class UserInstitutionMapping(db.Model):
id = db.Column(db.Integer, primary_key=True)
user = db.Column(db.Integer, sqlalchemy.ForeignKey('registered_user.id'), nullable=False)
institution = db.Column(db.Integer, sqlalchemy.ForeignKey('institution.id'), nullable=False)
role =db.Column(db.String, nullable=False)
role = db.Column(db.String, nullable=False)
#class CooperationToken(db.Model):
# __tablename__ = 'cooperation_token'
# id = db.Column(db.Integer, primary_key=True)
# name = db.Column(db.String, nullable=False)
#
#
#class UserCooperationTokenMapping(db.Model):
# __tablename__ = 'user_cooperation_token_mapping'
# id = db.Column(db.Integer, primary_key=True)
# user = db.Column(db.Integer, sqlalchemy.ForeignKey('registered_user.id'), nullable=False)
# cooperation_token = db.Column(db.Integer, sqlalchemy.ForeignKey('cooperation_token.id'), nullable=False)
class UserCooperationHistory(db.Model):
__tablename__ = 'user_cooperation'
id = db.Column(db.Integer, primary_key=True)
user = db.Column(db.Integer, sqlalchemy.ForeignKey('registered_user.id'), nullable=False)
institution = db.Column(db.Integer, sqlalchemy.ForeignKey('institution.id'), nullable=False)
role = db.Column(db.String, nullable=False)
school_year = db.Column(db.String, nullable=False)
badge_text = db.Column(db.String, nullable=True)
class Institution(db.Model):
@ -124,4 +120,5 @@ class InstitutionContract(db.Model):
corpus = db.Column(db.String, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
file_contract = db.Column(db.String, nullable=True)
original_filename = db.Column(db.String, nullable=True)

View File

@ -13,6 +13,7 @@ VALID_PROGRAMS = {'OS', 'SSG', 'MGP', 'ZG', 'NPI', 'SPI', 'SSI', 'PTI'}
VALID_SUBJECTS = {'slo', 'drug-jez', 'drug-druz', 'drug-narav', 'drug-strok', 'drug-izb'}
VALID_TEXT_TYPES = {'esej-spis', 'prakticno', 'solski-test', 'delo-v-razredu'}
VALID_GRAMMAR_CORRECTIONS = {'popr-ne', 'brez-popr', 'popr-da'}
VALID_REGIONS = {'CE', 'GO', 'KK', 'KP', 'KR', 'LJ', 'MB', 'MS', 'NM', 'PO', 'SG'}
MAXLEN_FORM = 150
@ -109,17 +110,18 @@ class UploadHandlerSolar(UploadHandler):
if user_institution_mapping is None:
return 'Vaš uporabnik ni dodeljen nobeni inštituciji.'
institution_id = user_institution_mapping.institution
is_institution_moderator = True if user_institution_mapping.role == 'moderator' else False
is_institution_coordinator = True if user_institution_mapping.role == 'coordinator' else False
if contract_type == 'sola':
if not is_institution_moderator:
if not is_institution_coordinator:
return 'Vaš uporabnik nima pravic za nalaganje pogodbe s šolo.'
# TODO: insert institution contract
model_obj = InstitutionContract(
institution=institution_id,
corpus='solar',
timestamp=timestamp,
file_contract=f_hash
file_contract=f_hash,
original_filename=f_obj.filename
)
self.store_model(model_obj)
else:
@ -129,6 +131,7 @@ class UploadHandlerSolar(UploadHandler):
timestamp=timestamp,
file_contract=f_hash,
contract_type=contract_type,
original_filename=f_obj.filename
)
self.store_model(model_obj)

BIN
static/image/contract.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/image/password.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/image/register.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
static/image/showpass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/image/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/image/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -142,33 +142,50 @@ label {
}
.contract-item {
height: 50px;
height: 65px;
margin: 5px;
border: 0px;
border-bottom: 2px solid #c4c4c4;
}
.contract-item-icon {
height: 50%;
margin-top: 15px;
margin-left: 15px;
margin-right: 15px;
float: left;
}
.contract-item-title {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
color: #46535b;
padding-top: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.contract-item-button {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
font-size: 14px;
float: right;
margin-top: -5px;
}
.contract-item-date {
.contract-item-data {
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 10px;
color: #46535b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#collaborators-container {
@ -200,6 +217,33 @@ label {
margin: 25px;
padding:5px;
border-radius: 10px;
text-align: center;
}
.warning {
background: #ffcc00;
border-radius: 15px;
border: 0px;
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
margin: 20px;
padding: 10px;
text-align: center;
}
.message-notification {
background: #99cc33;
border-radius: 15px;
border: 0px;
font-family: Roboto;
font-style: normal;
font-weight: normal;
font-size: 16px;
margin: 20px;
padding: 10px;
text-align: center;
}
#button-submit {
@ -554,6 +598,10 @@ select {
left: 25% !important;
}
.history-item {
border-bottom: 1px solid grey;
}
.history-item-date {
font-family: Roboto;
font-style: normal;
@ -598,6 +646,7 @@ select {
font-weight: normal;
font-size: 12px;
line-height: 14px;
margin-bottom: 5px;
color: #848C91;
}

View File

@ -47,7 +47,7 @@
<label for="telefon">Telefon:</label>
<input type="text" id="telefon" name="telefon"/>
<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>
<div class="form-text">*Po kliku na gumb “Oddaj” se bo prikazala vsebina pogodbe 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>
<button id="button-submit" type="submit">Oddaj</button>
</div>

View File

@ -5,8 +5,8 @@
<title>Admin panel - Šolar</title>
<style>
.tableFixHead {
overflow-y: auto;
height: 306px;
overflow-y: scroll;
max-height: 306px;
}
.tableFixHead thead th {
position: sticky;
@ -24,6 +24,9 @@
th {
background: #eee;
}
h2 {
color: blue;
}
</style>
</head>
<body>
@ -45,13 +48,13 @@
<input type="password" id="password" name="password"><br>
<input type="submit" value="Dodaj">
</form>
<!--<h3>Odstrani uporabnika</h3>
<h3>Odstrani uporabnika</h3>
<form action="/solar/deluser" method="post">
<label for="user_id">ID uporabnika:</label><br>
<input type="text" id="user_id" name="user_id"><br>
<input type="submit" value="Odstrani">
</form>-->
<h3>Seznam vseh aktivnih uporabnikov</h3>
</form>
<h3>Aktivni uporabniki</h3>
<div class="tableFixHead">
<table>
<thead>
@ -60,6 +63,7 @@
<th>Ime in priimek</th>
<th>Email</th>
<th>ID institucije</th>
<th>Vloga v instituciji</th>
</tr>
</thead>
<tbody>
@ -69,6 +73,7 @@
<td>{{item[0].name}}</td>
<td>{{item[0].email}}</td>
<td>{{item[1].institution}}</td>
<td>{{item[1].role}}</td>
{% endfor %}
</table>
</div>
@ -80,11 +85,18 @@
<input type="text" id="institution_id" name="institution_id"><br>
<label for="role">Vloga v instituciji:</label>
<select name="role" id="role">
<option value="moderator">Moderator</option>
<option value="user">Uporabnik</option>
<option value="coordinator">Koordinator/-ka</option>
<option value="mentor">Mentor/-ica</option>
<option value="other">Druga vloga</option>
</select>
<input type="submit" value="Dodeli">
</form>
<h3>Odstrani uporabnika iz institucije</h3>
<form action="/solar/deluserfrominstitution" method="post">
<label for="user_id">ID uporabnika:</label>
<input type="text" id="user_id" name="user_id"><br>
<input type="submit" value="Odstrani">
</form>
<div> </div>
<h2>Institucije</h2>
<h3>Dodaj institucijo</h3>
@ -92,7 +104,19 @@
<label for="name">Naziv:</label>
<input type="text" id="name" name="name"><br>
<label for="region">Regija:</label>
<input type="text" id="region" name="region"><br>
<select name="region" id="region">
<option value="CE">Celje</option>
<option value="GO">Nova Gorica</option>
<option value="KK">Krško</option>
<option value="KP">Koper</option>
<option value="KR">Kranj</option>
<option value="LJ">Ljubljana</option>
<option value="MB">Maribor</option>
<option value="MS">Murska Sobota</option>
<option value="NM">Novo mesto</option>
<option value="PO">Postojna</option>
<option value="SG">Slovenj Gradec</option>
</select>
<input type="submit" value="Dodaj">
</form>
<h3>Seznam vseh instituticij</h3>
@ -115,7 +139,7 @@
{% endfor %}
</table>
</div>
<h3>Seznam uporabnikov, ki niso aktivirani</h3>
<h3>Čakajoči uporabniki</h3>
<div class="tableFixHead">
<table>
<thead>
@ -123,14 +147,18 @@
<th>ID</th>
<th>Ime in priimek</th>
<th>Email</th>
<th>Institucija</th>
<th>Vloga v instituciji</th>
</tr>
</thead>
<tbody>
{% for item in inactive_users %}
<tr>
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.email}}</td>
<td>{{item[0].id}}</td>
<td>{{item[0].name}}</td>
<td>{{item[0].email}}</td>
<td>{{item[1].institution}}</td>
<td>{{item[1].role}}</td>
</tr>
{% endfor %}
</table>
@ -141,4 +169,30 @@
<input type="text" id="id" name="id"><br>
<input type="submit" value="Aktiviraj">
</form>
<h2>Zgodovina sodelovanja</h2>
<div class="tableFixHead">
<table>
<thead>
<tr>
<th>ID</th>
<th>ID uporabnika</th>
<th>ID institucije</th>
<th>Vloga</th>
<th>Šolsko leto</th>
<th>Besedilo značke</th>
</tr>
</thead>
<tbody>
{% for item in user_cooperation_history %}
<tr>
<td>{{item.id}}</td>
<td>{{item.user}}</td>
<td>{{item.institution}}</td>
<td>{{item.role}}</td>
<td>{{item.school_year}}</td>
<td>{{item.badge_text}}</td>
</tr>
{% endfor %}
</table>
</div>
</body>

View File

@ -28,9 +28,8 @@
</div>
</div>
<button class="button-general" style="margin-top: 20px;">Pošlji povezavo za ponastavitev geslo</button>
<button class="button-general" style="margin-top: 20px;">Pošlji</button>
</form>
<a href="/solar/register" class="contract-item-button">Registracija</a>
</div>
</div>
</div>

View File

@ -8,11 +8,17 @@
<body>
<div id="main-window">
<div id="rect1">
<div style="padding: 50px;">
<div style="padding: 25px;">
<div id="logo-container">
<img src="/static/image/logo.svg" alt="logo"/>
</div>
<h3 id="title" style="font-size: 27px; text-align: left;">Prijava - ŠOLAR</h3>
<h1 id="title">Portal za oddajanje besedil</h1>
<h2 id="subtitle">Korpus Šolar</h2>
<div class="form-text"><em>Zbiranje besedil za korpus Šolar poteka po naslednjem postopku, ki prinaša tudi točke za napredovanje.</em></div>
<h3 id="title" style="font-size: 27px; text-align: left;">Prijava</h3>
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
@ -24,22 +30,32 @@
<form method="POST" action="/solar/login">
<div>
<div>
<input type="email" name="email" placeholder="Email" autofocus="">
<img src="/static/image/user.png" alt="user" style="float: left; width: 10%;"/>
<input type="email" name="email" placeholder="Email" autofocus=""
style="float: right; width: 85%; margin-bottom: 20px; margin-top: 10px;">
</div>
</div>
<div>
<div>
<input type="password" name="password" placeholder="Geslo">
<img src="/static/image/password.png" alt="user" style="float: left; width: 10%; margin-top: 20px;"/>
<input type="password" name="password" placeholder="Geslo"
style="float: right; width: 85%; margin-bottom: 20px; margin-top: 10px;">
</div>
</div>
<div style="display:flex; flex-direction: row; justify-content: left; align-items: center">
<label>Zapomni prijavo</label>
<input style="width: 10%;" type="checkbox">
</div>
<button class="button-general" style="margin-top: 20px;">PRIJAVA</button>
<button class="button-general" style="margin-top: 30px; margin-bottom: 20px;">PRIJAVA</button>
</form>
<a href="/solar/register" class="contract-item-button">Registracija</a>
<a href="/solar/forgotpass" class="contract-item-button">Pozabljeno geslo</a>
<a href="/solar/register" class="contract-item-button"
style="-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
margin-top: 80px;
text-decoration: none;
color: #46535b;
width: 100%;"
><img src="/static/image/register.png" alt="user" style="float: left; width: 15%; margin-right: 25px;"/>
Registracija<br><div style="font-size: 11px; margin-top: 10px;">Še nimate uporabniškega računa? Registrirajte se!</div></a>
</div>
</div>
</div>

View File

@ -5,8 +5,8 @@
<title>Upravljanje institucije - Šolar</title>
<style>
.tableFixHead {
overflow-y: auto;
height: 206px;
overflow-y: scroll;
max-height: 306px;
}
.tableFixHead thead th {
position: sticky;
@ -24,12 +24,15 @@
th {
background: #eee;
}
h2 {
color: blue;
}
</style>
</head>
<body>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
<div style="background: blue;">
{{ messages[0] }}
</div>
{% endif %}
@ -76,14 +79,15 @@
</table>
</div>
<br>
<h3>Dodeli uporabnika instituciji</h3>
<h3>Dodaj uporabnika instituciji</h3>
<form action="/solar/addusertoinstitution" method="post">
<label for="user_id">ID uporabnika:</label>
<input type="text" id="user_id" name="user_id"><br>
<label for="role">Vloga v instituciji:</label>
<select name="role" id="role">
<option value="moderator">Moderator</option>
<option value="user">Uporabnik</option>
<option value="coordinator">Koordinator/-ka</option>
<option value="mentor">Mentor/-ica</option>
<option value="other">Druga vloga</option>
</select>
<input type="submit" value="Dodeli">
</form>

View File

@ -19,12 +19,13 @@
</head>
<body>
<a href="/solar/logout">Odjavi se</a>
{% if is_institution_moderator %}
{% if is_institution_coordinator %}
<br><a href="/solar/manage-institution">Upravljaj z institucijo</a>
{% endif %}
{% if is_admin %}
<br><a href="/solar/admin">Administracijski meni</a>
{% endif %}
<br><a href="mailto:email@example.com">Pomoč</a>
<div class="bg"></div>
<div id="main-window">
<div id="rect1">
@ -41,6 +42,13 @@
<button id="button-pogodbe" class="selection-tab-button">POGODBE</button>
</div>
{% if not institution %}
<div class="warning">Niste član nobene institucije!</div>
{% elif not institution_contract %}
<div class="warning">Pogodba s šolo še ni naložena!</div>
{% endif %}
<div id="data-confirm-notification" class="message-notification" style="display: none;">Prosimo, preverite in potrdite vnešene podatke.</div>
<label for="program">PROGRAM</label>
<select id="program" name="program">
<option value="OS" selected="selected">Osnovnošolski (OŠ)</option>
@ -109,7 +117,8 @@
<button id="button-submit" type="submit">Oddaj</button>
</div>
<div class="dropzone-previews"></div>
<div class="dropzone-previews">
</div>
</form>
</div>
@ -236,6 +245,7 @@ zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatk
var btnPogodbe = document.getElementById("button-pogodbe");
var elemTermsPopup = document.getElementById("popup-terms");
var termsScrollbox = document.getElementById("popup-terms-text");
var dataConfirmNotification = document.getElementById("data-confirm-notification");
var scrollboxTriggered = false;
var form = document.forms["my-dropzone"];
@ -307,6 +317,9 @@ zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatk
alert("Polje za predmet ne more biti prazno!");
} else if (vrsta === "delo-v-razredu" && isEmptyOrSpaces(vrstaCustom)) {
alert("Polje za vrsto besedila ne more biti prazno!");
} else if (dataConfirmNotification.style.display == "none") {
dataConfirmNotification.style.display = "inherit";
btnSubmit.textContent = "Potrdi";
} else {
// Then make terms popup visible
btnSubmit.disabled = true;
@ -325,6 +338,8 @@ zagotovili vse potrebne informacije v skladu s predpisi o varstvu osebnih podatk
// Clear fields and hide popup agian
btnSubmit.disabled = false;
elemTermsPopup.style.display = "none";
dataConfirmNotification.style.display = "none";
btnSubmit.textContent = "Oddaj";
form.reset();
scrollboxTriggered = false;
});

View File

@ -7,12 +7,13 @@
</head>
<body>
<a href="/solar/logout">Odjavi se</a>
{% if is_institution_moderator %}
{% if is_institution_coordinator %}
<br><a href="/solar/manage-institution">Upravljaj z institucijo</a>
{% endif %}
{% if is_admin %}