Privacy settings for profile pages

This commit is contained in:
Inga Kirschnick 2022-12-13 15:01:04 +01:00
parent 7856e97402
commit 61a6ddd4be
22 changed files with 476 additions and 786 deletions

View File

@ -90,7 +90,4 @@ def create_app(config: Config = Config) -> Flask:
from .users import bp as users_blueprint from .users import bp as users_blueprint
app.register_blueprint(users_blueprint, url_prefix='/users') app.register_blueprint(users_blueprint, url_prefix='/users')
from .test import bp as test_blueprint
app.register_blueprint(test_blueprint, url_prefix='/test')
return app return app

View File

@ -66,7 +66,7 @@ class TesseractOCRPipelineModelSchema(ma.SQLAlchemySchema):
publishing_year = ma.Int( publishing_year = ma.Int(
required=True required=True
) )
shared = ma.Boolean(required=True) is_public = ma.Boolean(required=True)
class JobSchema(ma.SQLAlchemySchema): class JobSchema(ma.SQLAlchemySchema):

View File

@ -61,6 +61,12 @@ class UserSettingJobStatusMailNotificationLevel(IntEnum):
NONE = 1 NONE = 1
END = 2 END = 2
ALL = 3 ALL = 3
class ProfilePrivacySettings(IntEnum):
SHOW_EMAIL = 1
SHOW_LAST_SEEN = 2
SHOW_MEMBER_SINCE = 4
# endregion enums # endregion enums
@ -220,7 +226,6 @@ class Role(HashidMixin, db.Model):
db.session.add(role) db.session.add(role)
db.session.commit() db.session.commit()
class Token(db.Model): class Token(db.Model):
__tablename__ = 'tokens' __tablename__ = 'tokens'
# Primary key # Primary key
@ -255,12 +260,20 @@ class Avatar(HashidMixin, FileMixin, db.Model):
def path(self): def path(self):
return os.path.join(self.user.path, 'avatar') return os.path.join(self.user.path, 'avatar')
def delete(self):
try:
os.remove(self.path)
except OSError as e:
current_app.logger.error(e)
db.session.delete(self)
class User(HashidMixin, UserMixin, db.Model): class User(HashidMixin, UserMixin, db.Model):
__tablename__ = 'users' __tablename__ = 'users'
# Primary key # Primary key
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# Foreign keys # Foreign keys
role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
# privacy_id = db.Column(db.Integer, db.ForeignKey('privacies.id'))
# Fields # Fields
email = db.Column(db.String(254), index=True, unique=True) email = db.Column(db.String(254), index=True, unique=True)
username = db.Column(db.String(64), index=True, unique=True) username = db.Column(db.String(64), index=True, unique=True)
@ -277,6 +290,8 @@ class User(HashidMixin, UserMixin, db.Model):
location = db.Column(db.String(64)) location = db.Column(db.String(64))
website = db.Column(db.String(128)) website = db.Column(db.String(128))
organization = db.Column(db.String(128)) organization = db.Column(db.String(128))
is_public = db.Column(db.Boolean, default=False)
profile_privacy_settings = db.Column(db.Integer(), default=0)
# Backrefs: role: Role # Backrefs: role: Role
# Relationships # Relationships
avatar = db.relationship( avatar = db.relationship(
@ -502,6 +517,22 @@ class User(HashidMixin, UserMixin, db.Model):
return False return False
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
#region Profile Privacy settings
def has_profile_privacy_setting(self, setting):
return self.profile_privacy_settings & setting == setting
def add_profile_privacy_setting(self, setting):
if not self.has_profile_privacy_setting(setting):
self.profile_privacy_settings += setting
def remove_profile_privacy_setting(self, setting):
if self.has_profile_privacy_setting(setting):
self.profile_privacy_settings -= setting
def reset_profile_privacy_settings(self):
self.profile_privacy_settings = 0
#endregion Profile Privacy settings
def to_json_serializeable(self, backrefs=False, relationships=False): def to_json_serializeable(self, backrefs=False, relationships=False):
json_serializeable = { json_serializeable = {
'id': self.hashid, 'id': self.hashid,
@ -519,7 +550,12 @@ class User(HashidMixin, UserMixin, db.Model):
'location': self.location, 'location': self.location,
'organization': self.organization, 'organization': self.organization,
'job_status_mail_notification_level': \ 'job_status_mail_notification_level': \
self.setting_job_status_mail_notification_level.name self.setting_job_status_mail_notification_level.name,
'is_public': self.is_public,
'show_email': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL),
'show_last_seen': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN),
'show_member_since': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE)
} }
if backrefs: if backrefs:
json_serializeable['role'] = \ json_serializeable['role'] = \

View File

@ -1,5 +1,6 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import ( from wtforms import (
BooleanField,
FileField, FileField,
StringField, StringField,
SubmitField, SubmitField,
@ -16,9 +17,6 @@ from app.models import User
from app.auth import USERNAME_REGEX from app.auth import USERNAME_REGEX
class EditProfileSettingsForm(FlaskForm): class EditProfileSettingsForm(FlaskForm):
avatar = FileField(
'Image File'
)
email = StringField( email = StringField(
'E-Mail', 'E-Mail',
validators=[InputRequired(), Length(max=254), Email()] validators=[InputRequired(), Length(max=254), Email()]
@ -37,6 +35,26 @@ class EditProfileSettingsForm(FlaskForm):
) )
] ]
) )
submit = SubmitField()
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
def validate_email(self, field):
if (field.data != self.user.email
and User.query.filter_by(email=field.data).first()):
raise ValidationError('Email already registered')
def validate_username(self, field):
if (field.data != self.user.username
and User.query.filter_by(username=field.data).first()):
raise ValidationError('Username already in use')
class EditPublicProfileInformationForm(FlaskForm):
avatar = FileField(
'Image File'
)
full_name = StringField( full_name = StringField(
'Full name', 'Full name',
validators=[Length(max=128)] validators=[Length(max=128)]
@ -68,20 +86,13 @@ class EditProfileSettingsForm(FlaskForm):
submit = SubmitField() submit = SubmitField()
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
def validate_email(self, field):
if (field.data != self.user.email
and User.query.filter_by(email=field.data).first()):
raise ValidationError('Email already registered')
def validate_username(self, field):
if (field.data != self.user.username
and User.query.filter_by(username=field.data).first()):
raise ValidationError('Username already in use')
def validate_image_file(self, field): def validate_image_file(self, field):
if not field.data.filename.lower().endswith('.jpg' or '.png' or '.jpeg'): if not field.data.filename.lower().endswith('.jpg' or '.png' or '.jpeg'):
raise ValidationError('only .jpg, .png and .jpeg!') raise ValidationError('only .jpg, .png and .jpeg!')
class EditPrivacySettingsForm(FlaskForm):
is_public = BooleanField('Public profile')
show_email = BooleanField('Show email')
show_last_seen = BooleanField('Show last seen')
show_member_since = BooleanField('Show member since')
submit = SubmitField()

View File

@ -1,5 +1,6 @@
from flask import ( from flask import (
abort, abort,
current_app,
flash, flash,
Markup, Markup,
redirect, redirect,
@ -8,12 +9,15 @@ from flask import (
url_for url_for
) )
from flask_login import current_user, login_required from flask_login import current_user, login_required
from threading import Thread
import os import os
from app import db from app import db
from app.models import Avatar, User from app.models import Avatar, ProfilePrivacySettings, User
from . import bp from . import bp
from .forms import ( from .forms import (
EditProfileSettingsForm EditPrivacySettingsForm,
EditProfileSettingsForm,
EditPublicProfileInformationForm
) )
@bp.before_request @bp.before_request
@ -25,8 +29,12 @@ def before_request():
@bp.route('/<hashid:user_id>') @bp.route('/<hashid:user_id>')
def profile(user_id): def profile(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
user_data = user.to_json_serializeable()
if not user.is_public and user != current_user:
abort(403)
return render_template('profile/profile_page.html.j2', return render_template('profile/profile_page.html.j2',
user=user) user=user,
user_data=user_data)
@bp.route('/<hashid:user_id>/avatars/<hashid:avatar_id>') @bp.route('/<hashid:user_id>/avatars/<hashid:avatar_id>')
def avatar_download(user_id, avatar_id): def avatar_download(user_id, avatar_id):
@ -41,6 +49,21 @@ def avatar_download(user_id, avatar_id):
mimetype=avatar_file.mimetype mimetype=avatar_file.mimetype
) )
@bp.route('/<hashid:user_id>/avatars/<hashid:avatar_id>', methods=['DELETE'])
def delete_avatar(avatar_id, user_id):
def _delete_avatar(app, avatar_id):
with app.app_context():
avatar_file = Avatar.query.get(avatar_id)
print(avatar_file)
avatar_file.delete()
db.session.commit()
thread = Thread(
target=_delete_avatar,
args=(current_app._get_current_object(), avatar_id)
)
thread.start()
return {}, 202
@bp.route('/<hashid:user_id>/edit-profile', methods=['GET', 'POST']) @bp.route('/<hashid:user_id>/edit-profile', methods=['GET', 'POST'])
def edit_profile(user_id): def edit_profile(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
@ -49,24 +72,58 @@ def edit_profile(user_id):
data=current_user.to_json_serializeable(), data=current_user.to_json_serializeable(),
prefix='edit-profile-settings-form' prefix='edit-profile-settings-form'
) )
edit_privacy_settings_form = EditPrivacySettingsForm(
data=current_user.to_json_serializeable(),
prefix='edit-privacy-settings-form'
)
edit_public_profile_information_form = EditPublicProfileInformationForm(
data=current_user.to_json_serializeable(),
prefix='edit-public-profile-information-form'
)
if edit_profile_settings_form.validate_on_submit(): if edit_profile_settings_form.validate_on_submit():
if edit_profile_settings_form.avatar.data:
try:
Avatar.create(edit_profile_settings_form.avatar.data, user=current_user)
except (AttributeError, OSError):
abort(500)
current_user.email = edit_profile_settings_form.email.data current_user.email = edit_profile_settings_form.email.data
current_user.username = edit_profile_settings_form.username.data current_user.username = edit_profile_settings_form.username.data
current_user.about_me = edit_profile_settings_form.about_me.data db.session.commit()
current_user.location = edit_profile_settings_form.location.data message = Markup(f'Profile settings updated')
current_user.organization = edit_profile_settings_form.organization.data flash(message, 'success')
current_user.website = edit_profile_settings_form.website.data return redirect(url_for('.profile', user_id=user.id))
current_user.full_name = edit_profile_settings_form.full_name.data if (edit_privacy_settings_form.submit.data
and edit_privacy_settings_form.validate()):
current_user.is_public = edit_privacy_settings_form.is_public.data
if edit_privacy_settings_form.show_email.data:
current_user.add_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL)
else:
current_user.remove_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL)
if edit_privacy_settings_form.show_last_seen.data:
current_user.add_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN)
else:
current_user.remove_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN)
if edit_privacy_settings_form.show_member_since.data:
current_user.add_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE)
else:
current_user.remove_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE)
db.session.commit()
flash('Your changes have been saved')
return redirect(url_for('.profile', user_id=user.id))
if edit_public_profile_information_form.validate_on_submit():
if edit_public_profile_information_form.avatar.data:
try:
Avatar.create(edit_public_profile_information_form.avatar.data, user=current_user)
except (AttributeError, OSError):
abort(500)
current_user.about_me = edit_public_profile_information_form.about_me.data
current_user.location = edit_public_profile_information_form.location.data
current_user.organization = edit_public_profile_information_form.organization.data
current_user.website = edit_public_profile_information_form.website.data
current_user.full_name = edit_public_profile_information_form.full_name.data
db.session.commit() db.session.commit()
message = Markup(f'Profile settings updated') message = Markup(f'Profile settings updated')
flash(message, 'success') flash(message, 'success')
return redirect(url_for('.profile', user_id=user.id)) return redirect(url_for('.profile', user_id=user.id))
return render_template('profile/edit_profile.html.j2', return render_template('profile/edit_profile.html.j2',
edit_profile_settings_form=edit_profile_settings_form, edit_profile_settings_form=edit_profile_settings_form,
edit_privacy_settings_form=edit_privacy_settings_form,
edit_public_profile_information_form=edit_public_profile_information_form,
user=user, user=user,
title='Edit Profile') title='Edit Profile')

View File

@ -73,11 +73,11 @@ class CreateTesseractOCRPipelineJobForm(CreateJobBaseForm):
if 'methods' in service_info: if 'methods' in service_info:
if 'binarization' in service_info['methods']: if 'binarization' in service_info['methods']:
del self.binarization.render_kw['disabled'] del self.binarization.render_kw['disabled']
if 'ocropus_nlbin_threshold' in service_info['methods']: if 'ocropus_nlbin_threshold' in service_info['methods']:
del self.ocropus_nlbin_threshold.render_kw['disabled'] del self.ocropus_nlbin_threshold.render_kw['disabled']
models = [ models = [
x for x in TesseractOCRPipelineModel.query.order_by(TesseractOCRPipelineModel.title).all() x for x in TesseractOCRPipelineModel.query.order_by(TesseractOCRPipelineModel.title).all()
if version in x.compatible_service_versions and (x.shared == True or x.user == current_user) if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
] ]
self.model.choices = [('', 'Choose your option')] self.model.choices = [('', 'Choose your option')]
self.model.choices += [(x.hashid, f'{x.title} [{x.version}]') for x in models] self.model.choices += [(x.hashid, f'{x.title} [{x.version}]') for x in models]
@ -157,7 +157,7 @@ class CreateSpacyNLPPipelineJobForm(CreateJobBaseForm):
del self.encoding_detection.render_kw['disabled'] del self.encoding_detection.render_kw['disabled']
models = [ models = [
x for x in SpaCyNLPPipelineModel.query.order_by(SpaCyNLPPipelineModel.title).all() x for x in SpaCyNLPPipelineModel.query.order_by(SpaCyNLPPipelineModel.title).all()
if version in x.compatible_service_versions and (x.shared == True or x.user == current_user) if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
] ]
self.model.choices = [('', 'Choose your option')] self.model.choices = [('', 'Choose your option')]
self.model.choices += [(x.hashid, f'{x.title} [{x.version}]') for x in models] self.model.choices += [(x.hashid, f'{x.title} [{x.version}]') for x in models]

View File

@ -98,7 +98,7 @@ def tesseract_ocr_pipeline():
return {}, 201, {'Location': job.url} return {}, 201, {'Location': job.url}
tesseract_ocr_pipeline_models = [ tesseract_ocr_pipeline_models = [
x for x in TesseractOCRPipelineModel.query.all() x for x in TesseractOCRPipelineModel.query.all()
if version in x.compatible_service_versions and (x.shared == True or x.user == current_user) if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
] ]
return render_template( return render_template(
'services/tesseract_ocr_pipeline.html.j2', 'services/tesseract_ocr_pipeline.html.j2',

View File

@ -62,15 +62,3 @@ class EditNotificationSettingsForm(FlaskForm):
for x in UserSettingJobStatusMailNotificationLevel for x in UserSettingJobStatusMailNotificationLevel
] ]
class EditPrivacySettingsForm(FlaskForm):
private_profile = BooleanField(
'Private profile'
)
private_email = BooleanField(
'Private email'
)
only_username = BooleanField(
'Show only username'
)
submit = SubmitField()

View File

@ -1,12 +1,11 @@
from flask import flash, redirect, render_template, url_for from flask import abort, flash, redirect, render_template, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from app import db from app import db
from app.models import UserSettingJobStatusMailNotificationLevel from app.models import ProfilePrivacySettings, UserSettingJobStatusMailNotificationLevel
from . import bp from . import bp
from .forms import ( from .forms import (
ChangePasswordForm, ChangePasswordForm,
EditNotificationSettingsForm, EditNotificationSettingsForm
EditPrivacySettingsForm
) )
@ -21,17 +20,13 @@ def settings():
data=current_user.to_json_serializeable(), data=current_user.to_json_serializeable(),
prefix='edit-notification-settings-form' prefix='edit-notification-settings-form'
) )
edit_privacy_settings_form = EditPrivacySettingsForm(
data=current_user.to_json_serializeable(),
prefix='edit-privacy-settings-form'
)
if change_password_form.submit.data and change_password_form.validate(): if change_password_form.submit.data and change_password_form.validate():
current_user.password = change_password_form.new_password.data current_user.password = change_password_form.new_password.data
db.session.commit() db.session.commit()
flash('Your changes have been saved') flash('Your changes have been saved')
return redirect(url_for('.index')) return redirect(url_for('.index'))
if (edit_notification_settings_form.submit.data if (edit_notification_settings_form.submit
and edit_notification_settings_form.validate()): and edit_notification_settings_form.validate()):
current_user.setting_job_status_mail_notification_level = ( current_user.setting_job_status_mail_notification_level = (
UserSettingJobStatusMailNotificationLevel[ UserSettingJobStatusMailNotificationLevel[
@ -45,6 +40,5 @@ def settings():
'settings/settings.html.j2', 'settings/settings.html.j2',
change_password_form=change_password_form, change_password_form=change_password_form,
edit_notification_settings_form=edit_notification_settings_form, edit_notification_settings_form=edit_notification_settings_form,
edit_privacy_settings_form=edit_privacy_settings_form,
title='Settings' title='Settings'
) )

View File

@ -34,9 +34,9 @@ class SpaCyNLPPipelineModelList extends RessourceList {
<div class="switch action-switch center-align" data-action="share-request"> <div class="switch action-switch center-align" data-action="share-request">
<span class="share"></span> <span class="share"></span>
<label> <label>
<input type="checkbox" class="shared"> <input type="checkbox" class="is_public">
<span class="lever"></span> <span class="lever"></span>
shared public
</label> </label>
</div> </div>
</td> </td>
@ -59,7 +59,7 @@ class SpaCyNLPPipelineModelList extends RessourceList {
'title': spaCyNLPPipelineModel.title, 'title': spaCyNLPPipelineModel.title,
'title-2': spaCyNLPPipelineModel.title, 'title-2': spaCyNLPPipelineModel.title,
'version': spaCyNLPPipelineModel.version, 'version': spaCyNLPPipelineModel.version,
'shared': spaCyNLPPipelineModel.shared ? 'True' : 'False' 'is_public': spaCyNLPPipelineModel.is_public ? 'True' : 'False'
}; };
}, },
sortArgs: ['creation-date', {order: 'desc'}], sortArgs: ['creation-date', {order: 'desc'}],
@ -75,7 +75,7 @@ class SpaCyNLPPipelineModelList extends RessourceList {
'title', 'title',
'title-2', 'title-2',
'version', 'version',
{name: 'shared', attr: 'data-checked'} {name: 'is_public', attr: 'data-checked'}
] ]
}; };
@ -87,7 +87,7 @@ class SpaCyNLPPipelineModelList extends RessourceList {
init(user) { init(user) {
this._init(user.spacy_nlp_pipeline_models); this._init(user.spacy_nlp_pipeline_models);
if (user.role.name !== ('Administrator' || 'Contributor')) { if (user.role.name !== ('Administrator' || 'Contributor')) {
for (let switchElement of this.listjs.list.querySelectorAll('.shared')) { for (let switchElement of this.listjs.list.querySelectorAll('.is_public')) {
switchElement.setAttribute('disabled', ''); switchElement.setAttribute('disabled', '');
} }
} }
@ -134,8 +134,8 @@ class SpaCyNLPPipelineModelList extends RessourceList {
let spaCyNLPPipelineModelId = spaCyNLPPipelineModelElement.dataset.id; let spaCyNLPPipelineModelId = spaCyNLPPipelineModelElement.dataset.id;
switch (action) { switch (action) {
case 'share-request': { case 'share-request': {
let shared = actionSwitchElement.querySelector('input').checked; let is_public = actionSwitchElement.querySelector('input').checked;
Utils.shareSpaCyNLPPipelineModelRequest(this.userId, spaCyNLPPipelineModelId, shared); Utils.shareSpaCyNLPPipelineModelRequest(this.userId, spaCyNLPPipelineModelId, is_public);
break; break;
} }
default: { default: {

View File

@ -34,9 +34,9 @@ class TesseractOCRPipelineModelList extends RessourceList {
<div class="switch action-switch center-align" data-action="share-request"> <div class="switch action-switch center-align" data-action="share-request">
<span class="share"></span> <span class="share"></span>
<label> <label>
<input type="checkbox" class="shared"> <input type="checkbox" class="is_public">
<span class="lever"></span> <span class="lever"></span>
shared public
</label> </label>
</div> </div>
</td> </td>
@ -59,7 +59,7 @@ class TesseractOCRPipelineModelList extends RessourceList {
'title': tesseractOCRPipelineModel.title, 'title': tesseractOCRPipelineModel.title,
'title-2': tesseractOCRPipelineModel.title, 'title-2': tesseractOCRPipelineModel.title,
'version': tesseractOCRPipelineModel.version, 'version': tesseractOCRPipelineModel.version,
'shared': tesseractOCRPipelineModel.shared ? 'True' : 'False' 'is_public': tesseractOCRPipelineModel.is_public ? 'True' : 'False'
}; };
}, },
sortArgs: ['creation-date', {order: 'desc'}], sortArgs: ['creation-date', {order: 'desc'}],
@ -75,7 +75,7 @@ class TesseractOCRPipelineModelList extends RessourceList {
'title', 'title',
'title-2', 'title-2',
'version', 'version',
{name: 'shared', attr: 'data-checked'} {name: 'is_public', attr: 'data-checked'}
] ]
}; };
@ -87,7 +87,7 @@ class TesseractOCRPipelineModelList extends RessourceList {
init (user) { init (user) {
this._init(user.tesseract_ocr_pipeline_models); this._init(user.tesseract_ocr_pipeline_models);
if (user.role.name !== ('Administrator' || 'Contributor')) { if (user.role.name !== ('Administrator' || 'Contributor')) {
for (let switchElement of this.listjs.list.querySelectorAll('.shared')) { for (let switchElement of this.listjs.list.querySelectorAll('.is_public')) {
switchElement.setAttribute('disabled', ''); switchElement.setAttribute('disabled', '');
} }
} }
@ -134,8 +134,8 @@ class TesseractOCRPipelineModelList extends RessourceList {
let tesseractOCRPipelineModelId = tesseractOCRPipelineModelElement.dataset.id; let tesseractOCRPipelineModelId = tesseractOCRPipelineModelElement.dataset.id;
switch (action) { switch (action) {
case 'share-request': { case 'share-request': {
let shared = actionSwitchElement.querySelector('input').checked; let is_public = actionSwitchElement.querySelector('input').checked;
Utils.shareTesseractOCRPipelineModelRequest(this.userId, tesseractOCRPipelineModelId, shared); Utils.shareTesseractOCRPipelineModelRequest(this.userId, tesseractOCRPipelineModelId, is_public);
break; break;
} }
default: { default: {

View File

@ -429,11 +429,11 @@ class Utils {
}); });
} }
static shareTesseractOCRPipelineModelRequest(userId, tesseractOCRPipelineModelId, shared) { static shareTesseractOCRPipelineModelRequest(userId, tesseractOCRPipelineModelId, is_public) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let tesseractOCRPipelineModel = app.data.users[userId].tesseract_ocr_pipeline_models[tesseractOCRPipelineModelId]; let tesseractOCRPipelineModel = app.data.users[userId].tesseract_ocr_pipeline_models[tesseractOCRPipelineModelId];
let msg = ''; let msg = '';
if (shared) { if (is_public) {
msg = `Model "${tesseractOCRPipelineModel.title}" is now public`; msg = `Model "${tesseractOCRPipelineModel.title}" is now public`;
} else { } else {
msg = `Model "${tesseractOCRPipelineModel.title}" is now private`; msg = `Model "${tesseractOCRPipelineModel.title}" is now private`;
@ -453,11 +453,11 @@ class Utils {
}); });
} }
static shareSpaCyNLPPipelineModelRequest(userId, spaCyNLPPipelineModelId, shared) { static shareSpaCyNLPPipelineModelRequest(userId, spaCyNLPPipelineModelId, is_public) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let spaCyNLPPipelineModel = app.data.users[userId].spacy_nlp_pipeline_models[spaCyNLPPipelineModelId]; let spaCyNLPPipelineModel = app.data.users[userId].spacy_nlp_pipeline_models[spaCyNLPPipelineModelId];
let msg = ''; let msg = '';
if (shared) { if (is_public) {
msg = `Model "${spaCyNLPPipelineModel.title}" is now public`; msg = `Model "${spaCyNLPPipelineModel.title}" is now public`;
} else { } else {
msg = `Model "${spaCyNLPPipelineModel.title}" is now private`; msg = `Model "${spaCyNLPPipelineModel.title}" is now private`;

View File

@ -33,7 +33,8 @@
<li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li> <li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li>
<li><div class="divider"></div></li> <li><div class="divider"></div></li>
<li><a class="subheader">Account</a></li> <li><a class="subheader">Account</a></li>
<li><a href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a></li> <li><a href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>General Settings</a></li>
<li><a href="{{ url_for('profile.edit_profile', user_id=current_user.id) }}"><i class="material-icons left">contact_page</i>Profile settings</a></li>
<li><a href="{{ url_for('auth.logout') }}">Log out</a></li> <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
{% if current_user.can(Permission.ADMINISTRATE) or current_user.can(Permission.USE_API) %} {% if current_user.can(Permission.ADMINISTRATE) or current_user.can(Permission.USE_API) %}
<li><div class="divider"></div></li> <li><div class="divider"></div></li>

View File

@ -9,37 +9,15 @@
</div> </div>
<div class="col s12"> <div class="col s12">
<div class="card"> <div class="card">
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<div class="card-content"> <div class="card-content">
{{ edit_profile_settings_form.hidden_tag() }}
<div class="row"> <div class="row">
<div class="col s5"> <div class="col s6">
<div class="row"> {{ edit_profile_settings_form.hidden_tag() }}
<div class="col s1"></div>
<div class="col s10">
{% if current_user.avatar %}
<img src="{{ url_for('profile.avatar_download', user_id=user.id, avatar_id=current_user.avatar.id) }}" alt="user-image" class="circle responsive-img">
{% else %}
<img src="{{ url_for('static', filename='images/user_avatar.png') }}" alt="user-image" class="circle responsive-img">
{% endif %}
</div>
<div class="col s1"></div>
</div>
<div class="row">
<div class="col s12">
{{wtf.render_field(edit_profile_settings_form.avatar, accept='image/jpeg, image/png, image/gif', placeholder="Choose an image file")}}
</div>
</div>
</div>
<div class="col s7">
{{ wtf.render_field(edit_profile_settings_form.username, material_icon='person') }} {{ wtf.render_field(edit_profile_settings_form.username, material_icon='person') }}
{{ wtf.render_field(edit_profile_settings_form.email, material_icon='email') }} {{ wtf.render_field(edit_profile_settings_form.email, material_icon='email') }}
{{ wtf.render_field(edit_profile_settings_form.full_name, material_icon='badge') }}
{{ wtf.render_field(edit_profile_settings_form.about_me, material_icon='description') }}
{{ wtf.render_field(edit_profile_settings_form.website, material_icon='laptop') }}
{{ wtf.render_field(edit_profile_settings_form.organization, material_icon='business') }}
{{ wtf.render_field(edit_profile_settings_form.location, material_icon='location_on') }}
</div> </div>
</div> </div>
</div> </div>
@ -50,7 +28,167 @@
</div> </div>
</form> </form>
</div> </div>
<form method="POST">
{{ edit_privacy_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Privacy settings</span>
<br>
{{ wtf.render_field(edit_privacy_settings_form.is_public, id='public-profile') }}
<br>
<hr>
<br>
{{ wtf.render_field(edit_privacy_settings_form.show_email, data_action='disable', disabled=true) }}
<br>
{{ wtf.render_field(edit_privacy_settings_form.show_last_seen, data_action='disable', disabled=true) }}
<br>
{{ wtf.render_field(edit_privacy_settings_form.show_member_since, data_action='disable', disabled=true) }}
<br>
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_privacy_settings_form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
<div class="card">
<form method="POST" enctype="multipart/form-data">
<div class="card-content">
{{ edit_public_profile_information_form.hidden_tag() }}
<div class="row">
<div class="col s12">
<span class="card-title">Public Profile</span>
</div>
</div>
<div class="row">
<div class="col s5">
<div class="row">
<div class="col s2"></div>
<div class="col s8">
{% if current_user.avatar %}
<img src="{{ url_for('profile.avatar_download', user_id=user.id, avatar_id=current_user.avatar.id) }}" alt="user-image" class="circle responsive-img" id="avatar">
{% else %}
<img src="{{ url_for('static', filename='images/user_avatar.png') }}" alt="user-image" class="circle responsive-img" id="placeholder-avatar">
{% endif %}
</div>
<div class="col s2"></div>
</div>
<div class="row">
<div class="col s2">
<div class="center-align">
<a class="action-button btn-floating red waves-effect waves-light modal-trigger" href="#delete-request" style="margin-top:15px;"><i class="material-icons">delete</i></a>
</div>
</div>
<div class="col s10">
{{wtf.render_field(edit_public_profile_information_form.avatar, accept='image/jpeg, image/png, image/gif', placeholder='Choose an image file', id='avatar-upload', data_action='disable')}}
</div>
</div>
</div>
<div class="col s7">
<p></p>
<br>
{{ wtf.render_field(edit_public_profile_information_form.full_name, material_icon='badge') }}
{{ wtf.render_field(edit_public_profile_information_form.about_me, material_icon='description') }}
{{ wtf.render_field(edit_public_profile_information_form.website, material_icon='laptop') }}
{{ wtf.render_field(edit_public_profile_information_form.organization, material_icon='business') }}
{{ wtf.render_field(edit_public_profile_information_form.location, material_icon='location_on') }}
</div>
</div>
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_public_profile_information_form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock page_content %} {% endblock page_content %}
{% block modals %}
<div class="modal" id="delete-request">
<div class="modal-content">
<h4>Confirm Avatar deletion</h4>
<p>Do you really want to delete your Avatar?</p>
</div>
<div class="modal-footer">
<a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a>
<a class="action-button btn modal-close red waves-effect waves-light" data-action="delete">Delete</a>
</div>
</div>
{% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
let publicProfile = document.querySelector('#public-profile');
let disableButtons = document.querySelectorAll('[data-action="disable"]');
let deleteButton = document.querySelector('[data-action="delete"]');
let cancelButton = document.querySelector('[data-action="cancel"]');
let deleteRequestModal = document.querySelector('#delete-request');
let avatar = document.querySelector('#avatar');
let placeholderAvatar = document.querySelector('#placeholder-avatar');
let avatarUpload = document.querySelector('#avatar-upload');
for (let i = 0; i < disableButtons.length; i++) {
disableButtons[i].disabled = !publicProfile.checked;
}
publicProfile.addEventListener('change', function() {
if (publicProfile.checked) {
for (let i = 0; i < disableButtons.length; i++) {
disableButtons[i].disabled = false;
}
} else {
for (let i = 0; i < disableButtons.length; i++) {
disableButtons[i].checked = false;
disableButtons[i].disabled = true;
}
}
});
avatarUpload.addEventListener('change', function() {
let file = this.files[0];
if (avatar){
avatar.src = URL.createObjectURL(file);
} else {
placeholderAvatar.src = URL.createObjectURL(file);
}
});
deleteButton.addEventListener('click', function() {
return new Promise((resolve, reject) => {
let user_id = "{{ current_user.hashid }}";
let avatar_id = "{{ current_user.avatar.hashid }}";
fetch(`/profile/${user_id}/avatars/${avatar_id}`, {method: 'DELETE', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
app.flash(`Avatar marked for deletion`, 'corpus');
resolve(response);
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
avatar.src = "{{ url_for('static', filename='images/user_avatar.png') }}";
});
});
cancelButton.addEventListener('click', function() {
let modal = M.Modal.getInstance(deleteRequestModal);
modal.close();
});
</script>
{% endblock scripts %}

View File

@ -2,96 +2,133 @@
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% block page_content %} {% block page_content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<div class="row">
<div class="col s1"></div>
<div class="col s3">
<br>
<br>
{% if user.avatar %}
<img src="{{ url_for('profile.avatar_download', user_id=user.id, avatar_id=user.avatar.id) }}" alt="user-image" class="circle responsive-img">
{% else %}
<img src="{{ url_for('static', filename='images/user_avatar.png') }}" alt="user-image" class="circle responsive-img">
{% endif %}
</div>
<div class="col s1"></div>
<div class="col s7">
<div class="row"> <div class="row">
<div class="col s1"></div> <div class="col s12">
<div class="col s4"> <h3 style="float:left">{{ user.username }}<span class="new badge green" id="public-information-badge" data-badge-caption="custom caption" style="margin-top:20px;"></span></h3>
{% if user.avatar %}
<img src="{{ url_for('profile.avatar_download', user_id=user.id, avatar_id=user.avatar.id) }}" alt="user-image" class="circle responsive-img">
{% else %}
<img src="{{ url_for('static', filename='images/user_avatar.png') }}" alt="user-image" class="circle responsive-img">
{% endif %}
</div> </div>
<div class="col s7"> <div class="col 12">
<h3>{{ user.username }}</h3> {% if user_data['show_last_seen'] %}
<div class="chip">Last seen: {{ user.last_seen.strftime('%Y-%m-%d %H:%M') }}</div> <div class="chip">Last seen: {{ user.last_seen.strftime('%Y-%m-%d %H:%M') }}</div>
{% endif %}
{% if user.location %} {% if user.location %}
<p><span class="material-icons" style="margin-right:20px; margin-top:20px;">location_on</span><i>{{ user.location }}</i></p> <p><span class="material-icons" style="margin-right:20px; margin-top:20px;">location_on</span><i>{{ user.location }}</i></p>
{% endif %} {% endif %}
<p></p> <p></p>
<br> <br>
{% if user.about_me%} {% if user.about_me%}
<div class="card"> <div style="border-left: solid 3px #E91E63; padding-left: 15px;">
<div class="card-content"> <h5>About me</h5>
<span class="card-title">About me</span> <p>{{ user.about_me }}</p>
<p>{{ user.about_me }}</p>
</div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row">
<div class="col s1"></div>
<div class="col s6"> </div>
<table> </div>
{% if user.full_name %} <p></p>
<tr> <br>
<td><span class="material-icons">person</span></td> <div class="row">
<td>{{ user.full_name }} </td> <div class="col s1"></div>
</tr> <div class="col s8">
{% endif %} <table>
{% if user.email %} {% if user.full_name %}
<tr> <tr>
<td><span class="material-icons">email</span></td> <td><span class="material-icons">person</span></td>
<td>{{ user.email }}</td> <td>{{ user.full_name }} </td>
</tr> </tr>
{% endif %} {% endif %}
{% if user.website %} {% if user_data['show_email'] %}
<tr> <tr>
<td><span class="material-icons">laptop</span></td> <td><span class="material-icons">email</span></td>
<td><a href="{{ user.website }}">{{ user.website }}</a></td> <td>{{ user.email }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if user.organization %} {% if user.website %}
<tr> <tr>
<td><span class="material-icons">business</span></td> <td><span class="material-icons">laptop</span></td>
<td>{{ user.organization }}</td> <td><a href="{{ user.website }}">{{ user.website }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
</table> {% if user.organization %}
<br> <tr>
<p><i>Member since: {{ user.member_since.strftime('%Y-%m-%d') }}</i></p> <td><span class="material-icons">business</span></td>
<p></p> <td>{{ user.organization }}</td>
<br> </tr>
{% if current_user.is_authenticated and current_user.id == user.id %} {% endif %}
<a class="waves-effect waves-light btn-small" href="{{ url_for('profile.edit_profile', user_id=user.id) }}">Edit profile</a> </table>
{% endif %} <br>
</div> {% if user_data['show_member_since'] %}
</div> <p><i>Member since: {{ user.member_since.strftime('%Y-%m-%d') }}</i></p>
</div> {% endif %}
</div> <p></p>
</div> <br>
</div> {% if current_user.is_authenticated and current_user.id == user.id %}
<div class="row"> <a class="waves-effect waves-light btn-small" href="{{ url_for('profile.edit_profile', user_id=user.id) }}">Edit profile</a>
<div class="col s6"> {% endif %}
<div class="card">
<div class="card-content">
<h4>Groups</h4>
</div>
</div>
</div>
<div class="col s6">
<div class="card">
<div class="card-content">
<h4>Public corpora</h4>
<div class="public-corpora-list" data-user-id="{{ user.hashid }}"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="row">
<div class="col s6">
<div class="card">
<div class="card-content">
<h4>Groups</h4>
</div>
</div>
</div>
<div class="col s6">
<div class="card">
<div class="card-content">
<h4>Public corpora</h4>
{# <div class="public-corpora-list" data-user-id="{{ user.hashid }}"></div> #}
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %} {% endblock page_content %}
{% block scripts %}
{{ super() }}
<script>
let publicInformationBadge = document.querySelector('#public-information-badge');
if ("{{ user }}" == "{{ current_user }}") {
if ("{{ user.is_public }}" == "True") {
publicInformationBadge.dataset.badgeCaption = 'Your profile is public';
publicInformationBadge.classList.add('green');
publicInformationBadge.classList.remove('red');
} else {
publicInformationBadge.dataset.badgeCaption = 'Your profile is private';
publicInformationBadge.classList.add('red');
publicInformationBadge.classList.remove('green');
}
} else {
publicInformationBadge.remove();
}
</script>
{% endblock scripts %}

View File

@ -85,6 +85,7 @@
</label> </label>
</div> </div>
</div> </div>
{% endif %}
{% if 'disabled' not in form.ocropus_nlbin_threshold.render_kw or not form.ocropus_nlbin_threshold.render_kw['disabled'] %} {% if 'disabled' not in form.ocropus_nlbin_threshold.render_kw or not form.ocropus_nlbin_threshold.render_kw['disabled'] %}
<div class="col s9 hide" id="create-job-form-ocropus_nlbin_threshold-container"> <div class="col s9 hide" id="create-job-form-ocropus_nlbin_threshold-container">
<br> <br>
@ -92,7 +93,6 @@
<p class="range-field">{{ form.ocropus_nlbin_threshold() }}</p> <p class="range-field">{{ form.ocropus_nlbin_threshold() }}</p>
</div> </div>
{% endif %} {% endif %}
{% endif %}
<!-- <!--
Seperate each setting with the following Seperate each setting with the following
<div class="col s12"><p>&nbsp;</p></div> <div class="col s12"><p>&nbsp;</p></div>

View File

@ -24,24 +24,6 @@
</div> </div>
</form> </form>
<div class="card">
<div class="card-content">
<span class="card-title">Privacy settings</span>
<br>
{{ wtf.render_field(edit_privacy_settings_form.private_profile) }}
<br>
{{ wtf.render_field(edit_privacy_settings_form.private_email) }}
<br>
{{ wtf.render_field(edit_privacy_settings_form.only_username) }}
<br>
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
</div>
</div>
</div>
<form method="POST"> <form method="POST">
{{ change_password_form.hidden_tag() }} {{ change_password_form.hidden_tag() }}
<div class="card"> <div class="card">

View File

@ -1,117 +0,0 @@
<div class="row" id="concordance-extension-container">
<div class="col s12">
<div class="card">
<div class="card-content">
<form id="concordance-extension-form">
<div class="row">
<div class="input-field col s12 m9">
<i class="material-icons prefix">search</i>
<input class="validate corpus-analysis-action" id="concordance-extension-form-query" name="query" type="text"
required pattern=".*\S+.*"
placeholder="&lt;ent_type=&quot;PERSON&quot;&gt; []* &lt;/ent_type&gt; []* [simple_pos=&quot;VERB&quot;] :: match.text_publishing_year=&quot;1991&quot;;">
</input>
<label for="concordance-extension-form-query">Query</label>
<span class="error-color-text helper-text hide" id="concordance-extension-error"></span>
<a class="modal-trigger" href="#cql-tutorial-modal" style="margin-left: 40px;"><i class="material-icons" style="font-size: inherit;">help</i>
Corpus Query Language tutorial</a>
<span> | </span>
<a class="modal-trigger" href="#tagsets-modal"><i class="material-icons" style="font-size: inherit;">info</i> Tagsets</a>
</div>
<div class="input-field col s12 m3">
<i class="material-icons prefix">arrow_forward</i>
<input class="validate corpus-analysis-action" id="concordance-extension-form-subcorpus-name" name="subcorpus-name" type="text"
required pattern="^[A-Z][a-z0-9\-]*" value="Last">
</input>
<label for="concordance-extension-form-subcorpus-name">Subcorpus name</label>
</div>
<div class="col s12 m9 l9">
<div class="row">
<div class="input-field col s4 l3">
<i class="material-icons prefix">short_text</i>
<select class="corpus-analysis-action" name="context">
<option value="10" selected>10</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="30">30</option>
</select>
<label>Context</label>
</div>
<div class="input-field col s4 l3">
<i class="material-icons prefix">format_list_numbered</i>
<select class="corpus-analysis-action" name="per-page">
<option value="10" selected>10</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
</select>
<label>Matches per page</label>
</div>
<div class="input-field col s4 l3">
<i class="material-icons prefix">format_shapes</i>
<select name="text-style">
<option value="0">Plain text</option>
<option value="1" selected>Highlight entities</option>
<option value="2">Token text</option>
</select>
<label>Text style</label>
</div>
<div class="input-field col s4 l3">
<i class="material-icons prefix">format_quote</i>
<select name="token-representation">
<option value="lemma">lemma</option>
<option value="pos">pos</option>
<option value="simple_pos">simple_pos</option>
<option value="word" selected>word</option>
</select>
<label>Token representation</label>
</div>
</div>
</div>
<div class="col s12 m3 l3 right-align">
<p class="hide-on-small-only">&nbsp;</p>
<a class="btn waves-effect waves-light modal-trigger" href="#concordance-query-builder" id="concordance-query-builder-button">
<i class="material-icons left">build</i>
Query builder
</a>
<button class="btn waves-effect waves-light corpus-analysis-action" id="concordance-extension-form-submit" type="submit" name="submit">
Send
<i class="material-icons right">send</i>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col s12">
<div id="concordance-extension-subcorpus-list"></div>
<div class="card">
<div class="card-content">
<div class="progress hide" id="concordance-extension-progress">
<div class="indeterminate"></div>
</div>
<div class="row">
<div class="col s9"><p class="hide" id="concordance-extension-subcorpus-info"></p></div>
<div class="col s3 right-align" id="concordance-extension-subcorpus-actions"></div>
</div>
<table class="highlight">
<thead>
<tr>
<th style="width: 2%;"></th>
<th style="width: 8%;">Source</th>
<th class="right-align" style="width: 22.5%;">Left context</th>
<th class="center-align" style="width: 40%;">KWIC</th>
<th class="left-align" style="width: 22.5%;">Right Context</th>
<th class="left-align" style="width: 5%;"></th>
</tr>
</thead>
<tbody id="concordance-extension-subcorpus-items"></tbody>
</table>
<ul class="pagination hide" id="concordance-extension-subcorpus-pagination"></ul>
</div>
</div>
</div>
</div>

View File

@ -1,454 +0,0 @@
{% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %}
<style>
a {color: #FFFFFF;}
</style>
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis" id="corpus-analysis-app-container"{% endblock main_attribs %}
{% block page_content %}
<ul class="row tabs no-autoinit" id="corpus-analysis-app-extension-tabs">
<li class="tab col s3"><a href="#corpus-analysis-app-overview"><i class="nopaque-icons service-icon left" data-service="corpus-analysis"></i>Corpus analysis</a></li>
<li class="tab col s3"><a class="active" href="#concordance-extension-container"><i class="material-icons left">list_alt</i>Concordance</a></li>
<li class="tab col s3"><a href="#reader-extension-container"><i class="material-icons left">chrome_reader_mode</i>Reader</a></li>
</ul>
{# <div class="row" id="corpus-analysis-app-overview">
<div class="col s12">
<h1>{{ title }}</h1>
</div>
<div class="col s3">
<div class="card extension-selector hoverable" data-target="concordance-extension-container">
<div class="card-content">
<span class="card-title"><i class="material-icons left">list_alt</i>Concordance</span>
<p>Query your corpus with the CQP query language utilizing a KWIC view.</p>
</div>
</div>
</div>
<div class="col s3">
<div class="card extension-selector hoverable" data-target="reader-extension-container">
<div class="card-content">
<span class="card-title"><i class="material-icons left">chrome_reader_mode</i>Reader</span>
<p>Inspect your corpus in detail with a full text view, including annotations.</p>
</div>
</div>
</div>
</div> #}
{% include "test/analyse_corpus.concordance.html.j2" %}
{% endblock page_content %}
{% block modals %}
{{ super() }}
<div class="modal no-autoinit" id="corpus-analysis-app-init-modal">
<div class="modal-content">
<h4>Initializing session...</h4>
<p>If the loading takes to long or an error occured,
<a onclick="window.location.reload()" href="#">click here</a>
to refresh your session or
<a href="">go back</a>!
</p>
<div class="progress" id="corpus-analysis-app-init-progress">
<div class="indeterminate"></div>
</div>
<p class="error-color-text hide" id="corpus-analysis-app-init-error"></p>
</div>
</div>
<div class="modal" id="cql-tutorial-modal">
<div class="modal-content">
{% with headline_num=4 %}
{% include "main/manual/_08_cqp_query_language.html.j2" %}
{% endwith %}
</div>
</div>
<div class="modal" id="tagsets-modal">
<div class="modal-content">
<h4>Tagsets</h4>
<ul class="tabs">
<li class="tab"><a class="active" href="#simple_pos-tagset">simple_pos</a></li>
<li class="tab"><a href="#english-ent_type-tagset">English ent_type</a></li>
<li class="tab"><a href="#english-pos-tagset">English pos</a></li>
<li class="tab"><a href="#german-ent_type-tagset">German ent_type</a></li>
<li class="tab"><a href="#german-pos-tagset">German pos</a></li>
</ul>
{% include "main/manual/_10_tagsets.html.j2" %}
</div>
</div>
<div class="modal" id="concordance-query-builder">
<div class="modal-content">
<div>
<nav>
<div class="nav-wrapper" id="query-builder-nav">
<a href="#!" class="brand-logo"><i class="material-icons">build</i>Query Builder</a>
<i class="material-icons close right" id="close-query-builder">close</i>
<a class="modal-trigger" href="#query-builder-tutorial-modal" >
<i class="material-icons right tooltipped" id="query-builder-tutorial-info-icon" data-position="bottom" data-tooltip="Click here if you are unsure how to use the Query Builder <br>and want to find out what other options it offers.">help</i>
</a>
</div>
</nav>
</div>
<p></p>
<div id="query-container" class="hide">
<div class="row">
<h6 class="col s2">Your Query:
<a class="modal-trigger" href="#query-builder-tutorial-modal">
<i class="material-icons left" id="general-options-query-builder-tutorial-info-icon">help_outline</i></a>
</h6>
</div>
<div class="row">
<div class="col s10" id="your-query" data-position="bottom" data-tooltip="You can edit your query by deleting individual elements or moving them via drag and drop."></div>
<a class="btn-small waves-effect waves-teal col s1" id="insert-query-button">
<i class="material-icons">send</i>
</a>
</div>
<p><i> Preview:</i></p>
<p id="query-preview"></p>
<br>
</div>
<h6>Use the following options to build your query. If you need help, click on the question mark in the upper right corner!</h6>
<p></p>
<a class="btn-large waves-effect waves-light tooltipped" id="positional-attr-button" data-position="bottom" data-tooltip="Search for any token, for example a word, a lemma or a part-of-speech tag">Add new token to your query</a>
<a class="btn-large waves-effect waves-light tooltipped" id="structural-attr-button" data-position="bottom" data-tooltip="Structure your query with structural attributes, for example sentences, entities or annotate the text">Add structural attributes to your query</a>
<div id="structural-attr" class="hide">
<p></p>
<h6>Which structural attribute do you want to add to your query?<a class="modal-trigger" href="#query-builder-tutorial-modal"><i class="material-icons left" id="add-structural-attribute-tutorial-info-icon">help_outline</i></a></h6>
<p></p>
<div class="row">
<div class="col s12">
<a class="btn-small waves-effect waves-light" id="sentence">sentence</a>
<a class="btn-small waves-effect waves-light" id="entity">entity</a>
<a class="btn-small waves-effect waves-light" id="text-annotation">Meta Data</a>
</div>
</div>
<div id="entity-builder" class="hide">
<p></p>
<br>
<div class="row">
<a class="btn waves-effect waves-light col s4" id="empty-entity">Add Entity of any type</a>
<p class="col s1 l1"></p>
<div class= "input-field col s3">
<select name="englishenttype" id="english-ent-type">
<option value="" disabled selected>English ent_type</option>
<option value="CARDINAL">CARDINAL</option>
<option value="DATE">DATE</option>
<option value="EVENT">EVENT</option>
<option value="FAC">FAC</option>
<option value="GPE">GPE</option>
<option value="LANGUAGE">LANGUAGE</option>
<option value="LAW">LAW</option>
<option value="LOC">LOC</option>
<option value="MONEY">MONEY</option>
<option value="NORP">NORP</option>
<option value="ORDINAL">ORDINAL</option>
<option value="ORG">ORG</option>
<option value="PERCENT">PERCENT</option>
<option value="PERSON">PERSON</option>
<option value="PRODUCT">PRODUCT</option>
<option value="QUANTITY">QUANTITY</option>
<option value="TIME">TIME</option>
<option value="WORK_OF_ART">WORK_OF_ART</option>
</select>
<label>Entity Type</label>
</div>
<div class= "input-field col s3">
<select name="germanenttype" id="german-ent-type">
<option value="" disabled selected>German ent_type</option>
<option value="LOC">LOC</option>
<option value="MISC">MISC</option>
<option value="ORG">ORG</option>
<option value="PER">PER</option>
</select>
</div>
</div>
</div>
<div id="text-annotation-builder" class="hide">
<p></p>
<br>
<div class="row">
<div class= "input-field col s4 l3">
<select name="text-annotation-options" id="text-annotation-options">
<option class="btn-small waves-effect waves-light" value="address">address</option>
<option class="btn-small waves-effect waves-light" value="author">author</option>
<option class="btn-small waves-effect waves-light" value="booktitle">booktitle</option>
<option class="btn-small waves-effect waves-light" value="chapter">chapter</option>
<option class="btn-small waves-effect waves-light" value="editor">editor</option>
<option class="btn-small waves-effect waves-light" value="institution">institution</option>
<option class="btn-small waves-effect waves-light" value="journal">journal</option>
<option class="btn-small waves-effect waves-light" value="pages">pages</option>
<option class="btn-small waves-effect waves-light" value="publisher">publisher</option>
<option class="btn-small waves-effect waves-light" value="publishing_year">publishing year</option>
<option class="btn-small waves-effect waves-light" value="school">school</option>
<option class="btn-small waves-effect waves-light" value="title">title</option>
</select>
<label>Meta data</label>
</div>
<div class= "input-field col s7 l5">
<i class="material-icons prefix">mode_edit</i>
<input placeholder="Type in your text annotation" type="text" id="text-annotation-input">
</div>
<div class="col s1 l1 center-align">
<p class="btn-floating waves-effect waves-light" id="text-annotation-submit">
<i class="material-icons right">send</i>
</p>
</div>
<div class="hide" id="no-value-metadata-message"><i>No value entered!</i></div>
</div>
</div>
</div>
<div id="positional-attr" class="hide">
<p></p>
<div class="row" id="token-kind-selector">
<div class="col s5">
<h6>Which kind of token are you looking for? <a class="modal-trigger" href="#query-builder-tutorial-modal"><i class="material-icons left" id="token-tutorial-info-icon">help_outline</i></a></h6>
</div>
<div class="input-field col s3">
<select id="token-attr">
<option value="word" selected>word</option>
<option value="lemma">lemma</option>
<option value="english-pos">english pos</option>
<option value="german-pos">german pos</option>
<option value="simple-pos-button">simple_pos</option>
<option value="empty-token">empty token</option>
</select>
</div>
</div>
<p></p>
<div id="token-builder-content">
<div class="row" >
<div id="token-query"></div>
<div id="word-builder">
<div class= "input-field col s3 l4">
<i class="material-icons prefix">mode_edit</i>
<input placeholder="Type in your word" type="text" id="word-input">
</div>
</div>
<div id="lemma-builder" class="hide" >
<div class= "input-field col s3 l4">
<i class="material-icons prefix">mode_edit</i>
<input placeholder="Type in your lemma" type="text" id="lemma-input">
</div>
</div>
<div id="english-pos-builder" class="hide">
<div class="col s6 m4 l4">
<div class="row">
<div class= "input-field col s12">
<select name="englishpos" id="english-pos">
<option value="default" disabled selected>English pos tagset</option>
<option value="ADD">email</option>
<option value="AFX">affix</option>
<option value="CC">conjunction, coordinating</option>
<option value="CD">cardinal number</option>
<option value="DT">determiner</option>
<option value="EX">existential there</option>
<option value="FW">foreign word</option>
<option value="HYPH">punctuation mark, hyphen</option>
<option value="IN">conjunction, subordinating or preposition</option>
<option value="JJ">adjective</option>
<option value="JJR">adjective, comparative</option>
<option value="JJS">adjective, superlative</option>
</select>
<label>Part-of-speech tags</label>
</div>
</div>
</div>
</div>
<div id="german-pos-builder" class="hide">
<div class="col s6 m4 l4">
<div class="row">
<div class= "input-field col s12">
<select name="germanpos" id="german-pos">
<option value="default" disabled selected>German pos tagset</option>
<option value="ADJA">adjective, attributive</option>
<option value="ADJD">adjective, adverbial or predicative</option>
<option value="ADV">adverb</option>
<option value="APPO">postposition</option>
<option value="APPR">preposition; circumposition left</option>
<option value="APPRART">preposition with article</option>
<option value="APZR">circumposition right</option>
<option value="ART">definite or indefinite article</option>
</select>
<label>Part-of-speech tags</label>
</div>
</div>
</div>
</div>
<div id="simplepos-builder" class="hide">
<div class="col s6 m4 l4">
<div class="row">
<div class= "input-field col s12">
<select name="simplepos" id="simple-pos">
<option value="default" disabled selected>simple_pos tagset</option>
<option value="ADJ">adjective</option>
<option value="ADP">adposition</option>
<option value="ADV">adverb</option>
<option value="AUX">auxiliary verb</option>
<option value="CONJ">coordinating conjunction</option>
<option value="DET">determiner</option>
<option value="INTJ">interjection</option>
<option value="NOUN">noun</option>
<option value="NUM">numeral</option>
<option value="PART">particle</option>
<option value="PRON">pronoun</option>
<option value="PROPN">proper noun</option>
<option value="PUNCT">punctuation</option>
<option value="SCONJ">subordinating conjunction</option>
<option value="SYM">symbol</option>
<option value="VERB">verb</option>
<option value="X">other</option>
</select>
<label>Simple part-of-speech tags</label>
</div>
</div>
</div>
</div>
<div class="col s1 l1 center-align">
<p class="btn-floating waves-effect waves-light" id="token-submit">
<i class="material-icons right">send</i>
</p>
</div>
<div class="hide" id="no-value-message"><i>No value entered!</i></div>
</div>
<div id="token-edit-options">
<div class="row">
<h6>Options to edit your token: <a class="modal-trigger" href="#query-builder-tutorial-modal"><i class="material-icons left" id="edit-options-tutorial-info-icon">help_outline</i></a></h6>
</div>
<p></p>
<div class="row">
<div id="input-options" class="col s5 m5 l5 xl4">
<a id="wildcard-char" class="btn-small waves-effect waves-light tooltipped" data-position="top" data-tooltip="Look for a variable character (also called wildcard character)">Wildcard character</a>
<a id="option-group" class="btn-small waves-effect waves-light tooltipped" data-position="top" data-tooltip="Find character sequences from a list of options">Option Group</a>
</div>
<div class="col s3 m3 l3 xl3" id="incidence-modifiers-button">
<a class="dropdown-trigger btn-small waves-effect waves-light" href="#" data-target="incidence-modifiers" data-position="top" data-tooltip="Incidence Modifiers are special characters or patterns, <br>which determine how often a character represented previously should occur.">incidence modifiers</a>
</div>
<ul id="incidence-modifiers" class="dropdown-content">
<li><a id="one-or-more" data-token="+" class="tooltipped" data-position ="top" data-tooltip="...occurrences of the character/token before">one or more (+)</a></li>
<li><a id="zero-or-more" data-token="*" class="tooltipped" data-position ="top" data-tooltip="...occurrences of the character/token before">zero or more (*)</a></li>
<li><a id="zero-or-one" data-token="?" class="tooltipped" data-position ="top" data-tooltip="...occurrences of the character/token before">zero or one (?)</a></li>
<li><a id="exactly-n" class="modal-trigger tooltipped" href="#exactlyN" data-token="{n}" class="" data-position ="top" data-tooltip="...occurrences of the character/token before">exactly n ({n})</a></li>
<li><a id="between-n-m" class="modal-trigger tooltipped" href="#betweenNM" data-token="{n,m}" class="" data-position ="top" data-tooltip="...occurrences of the character/token before">between n and m ({n,m})</a></li>
</ul>
<div id="ignore-case-checkbox" class="col s2 m2 l2 xl2">
<p id="ignore-case">
<label>
<input type="checkbox" class="filled-in" />
<span>Ignore Case</span>
</label>
</p>
</div>
<div class="col s2 m2 l2 xl2" id="condition-container">
<a class="btn-small tooltipped waves-effect waves-light" id="or" data-position="bottom" data-tooltip="You can add another condition to your token. <br>At least one must be fulfilled">or</a>
<a class="btn-small tooltipped waves-effect waves-light" id="and" data-position="bottom" data-tooltip="You can add another condition to your token. <br>Both must be fulfilled">and</a>
</div>
</div>
</div>
</div>
<div id="exactlyN" class="modal">
<div class="row modal-content">
<div class="input-field col s10">
<i class="material-icons prefix">mode_edit</i>
<input placeholder="type in a number for 'n'" type="text" id="n-input">
</div>
<div class="col s2">
<p class="btn-floating waves-effect waves-light" id="n-submit">
<i class="material-icons right">send</i>
</p>
</div>
</div>
</div>
<div id="betweenNM" class="modal">
<div class="row modal-content">
<div class= "input-field col s5">
<i class="material-icons prefix">mode_edit</i>
<input placeholder="number for 'n'" type="text" id="n-m-input">
</div>
<div class= "input-field col s5">
<i class="material-icons prefix">mode_edit</i>
<input placeholder="number for 'm'" type="text" id="m-input">
</div>
<div class="col s2">
<p class="btn-floating waves-effect waves-light" id="n-m-submit">
<i class="material-icons right">send</i>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal modal-fixed-footer" id="query-builder-tutorial-modal">
<div class="modal-content" >
<div id="query-builder-tutorial-start"></div>
<ul class="tabs">
<li class="tab"><a class="active" href="#query-builder-tutorial">Query Builder Tutorial</a></li>
{# <li class="tab"><a href="#qb-examples">Examples</a></li> #}
<li class="tab"><a href="#cql-cb-tutorial">Corpus Query Language Tutorial</a></li>
<li class="tab"><a href="#tagsets-cb-tutorial">Tagsets</a></li>
</ul>
<div id="query-builder-tutorial">
{% include "main/manual/_09_query_builder.html.j2" %}
</div>
{# <div id="qb-examples"></div> #}
<div id ="cql-cb-tutorial">
{% with headline_num=4 %}
{% include "main/manual/_08_cqp_query_language.html.j2" %}
{% endwith %}
</div>
<div id="tagsets-cb-tutorial">
<h4>Tagsets</h4>
{% include "main/manual/_10_tagsets.html.j2" %}
</div>
<div class="fixed-action-btn">
<a class="btn-floating btn-large teal" id="scroll-up-button-query-builder-tutorial" href='#query-builder-tutorial-start'>
<i class="large material-icons">arrow_upward</i>
</a>
</div>
</div>
</div>
{% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
const concordanceQueryBuilder = new ConcordanceQueryBuilder()
</script>
{% endblock scripts %}

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('test', __name__)
from . import routes

View File

@ -1,10 +0,0 @@
from flask import render_template
from flask_login import login_required
from app.models import Corpus, CorpusFile, CorpusStatus
from . import bp
import os
@bp.route('')
@login_required
def test():
return render_template('test/analyse_corpus.html.j2', title="Test")

View File

@ -0,0 +1,35 @@
"""Add is_public and profile_privacy_settings columns to users table
Revision ID: 5b2a6e590166
Revises: ef6a275f8079
Create Date: 2022-12-12 12:39:09.339847
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5b2a6e590166'
down_revision = 'ef6a275f8079'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
'users',
sa.Column('is_public', sa.Boolean(), nullable=True)
)
op.add_column(
'users',
sa.Column('profile_privacy_settings', sa.Integer(), nullable=True)
)
op.execute('UPDATE users SET is_public = false;')
op.execute('UPDATE users SET profile_privacy_settings = 0;')
def downgrade():
op.drop_column('users', 'is_public')
op.drop_column('users', 'profile_privacy_settings')