Merge branch 'public-corpus' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into public-corpus

This commit is contained in:
Patrick Jentsch 2023-03-01 16:33:03 +01:00
commit 73cb566db2
10 changed files with 150 additions and 64 deletions

View File

@ -3,27 +3,24 @@ from flask_login import current_user
from functools import wraps from functools import wraps
from app.models import Corpus, CorpusFollowerAssociation from app.models import Corpus, CorpusFollowerAssociation
def corpus_follower_permission_required(permissions): def corpus_follower_permission_required(*permissions):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
corpus_id = kwargs.get('corpus_id') corpus_id = kwargs.get('corpus_id')
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if current_user == corpus.user or current_user.is_administrator(): if current_user == corpus.user or current_user.is_administrator():
print('user or admin')
return f(*args, **kwargs) return f(*args, **kwargs)
if not current_user.is_following_corpus(corpus): if not current_user.is_following_corpus(corpus):
print('not following corpus')
abort(403) abort(403)
corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first_or_404() corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first_or_404()
for permission in permissions: if not all([corpus_follower_association.role.has_permission(p) for p in permissions]):
if not corpus_follower_association.role.has_permission(permission): abort(403)
abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
return decorator return decorator
def owner_or_admin_required(): def corpus_owner_or_admin_required():
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):

View File

@ -16,7 +16,7 @@ from flask_login import current_user, login_required
from threading import Thread from threading import Thread
import jwt import jwt
import os import os
from .decorators import corpus_follower_permission_required, owner_or_admin_required from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
from app import db, hashids from app import db, hashids
from app.models import ( from app.models import (
Corpus, Corpus,
@ -34,12 +34,6 @@ from .forms import (
UpdateCorpusFileForm UpdateCorpusFileForm
) )
@bp.route('/<hashid:corpus_id>/test')
@login_required
@corpus_follower_permission_required(['VIEW', 'ADD_CORPUS_FILE'])
def test(corpus_id):
return 'ok'
@bp.route('/fake-add') @bp.route('/fake-add')
@login_required @login_required
def fake_add(): def fake_add():
@ -52,7 +46,7 @@ def fake_add():
@bp.route('/<hashid:corpus_id>/is_public', methods=['POST']) @bp.route('/<hashid:corpus_id>/is_public', methods=['POST'])
@login_required @login_required
@owner_or_admin_required() @corpus_owner_or_admin_required()
def update_corpus_is_public(corpus_id): def update_corpus_is_public(corpus_id):
is_public = request.json is_public = request.json
if not isinstance(is_public, bool): if not isinstance(is_public, bool):
@ -67,7 +61,7 @@ def update_corpus_is_public(corpus_id):
@bp.route('/<hashid:corpus_id>/followers/add', methods=['POST']) @bp.route('/<hashid:corpus_id>/followers/add', methods=['POST'])
@login_required @login_required
@owner_or_admin_required() @corpus_owner_or_admin_required()
def add_corpus_followers(corpus_id): def add_corpus_followers(corpus_id):
usernames = request.json usernames = request.json
if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)): if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
@ -124,7 +118,7 @@ def current_user_unfollow_corpus(corpus_id):
@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['POST']) @bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['POST'])
@corpus_follower_permission_required(['REMOVE_FOLLOWER', 'UPDATE_FOLLOWER']) @corpus_follower_permission_required('REMOVE_FOLLOWER', 'UPDATE_FOLLOWER')
def add_permission(corpus_id, follower_id): def add_permission(corpus_id, follower_id):
corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404() corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
if not (corpus_follower_association.corpus.user == current_user or current_user.is_administrator()): if not (corpus_follower_association.corpus.user == current_user or current_user.is_administrator()):
@ -218,6 +212,7 @@ def generate_corpus_share_link(corpus_id):
@bp.route('/<hashid:corpus_id>', methods=['DELETE']) @bp.route('/<hashid:corpus_id>', methods=['DELETE'])
@login_required @login_required
@corpus_owner_or_admin_required()
def delete_corpus(corpus_id): def delete_corpus(corpus_id):
def _delete_corpus(app, corpus_id): def _delete_corpus(app, corpus_id):
with app.app_context(): with app.app_context():
@ -226,8 +221,6 @@ def delete_corpus(corpus_id):
db.session.commit() db.session.commit()
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
thread = Thread( thread = Thread(
target=_delete_corpus, target=_delete_corpus,
args=(current_app._get_current_object(), corpus_id) args=(current_app._get_current_object(), corpus_id)
@ -238,12 +231,9 @@ def delete_corpus(corpus_id):
@bp.route('/<hashid:corpus_id>/analyse') @bp.route('/<hashid:corpus_id>/analyse')
@login_required @login_required
@corpus_follower_permission_required('VIEW')
def analyse_corpus(corpus_id): def analyse_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user
or current_user.is_administrator()
or current_user.is_following_corpus(corpus)):
abort(403)
return render_template( return render_template(
'corpora/analyse_corpus.html.j2', 'corpora/analyse_corpus.html.j2',
corpus=corpus, corpus=corpus,
@ -253,6 +243,7 @@ def analyse_corpus(corpus_id):
@bp.route('/<hashid:corpus_id>/build', methods=['POST']) @bp.route('/<hashid:corpus_id>/build', methods=['POST'])
@login_required @login_required
@corpus_owner_or_admin_required()
def build_corpus(corpus_id): def build_corpus(corpus_id):
def _build_corpus(app, corpus_id): def _build_corpus(app, corpus_id):
with app.app_context(): with app.app_context():
@ -277,6 +268,7 @@ def build_corpus(corpus_id):
@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST']) @bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
@login_required @login_required
@corpus_follower_permission_required('ADD_CORPUS_FILE')
def create_corpus_file(corpus_id): def create_corpus_file(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()): if not (corpus.user == current_user or current_user.is_administrator()):
@ -324,10 +316,9 @@ def create_corpus_file(corpus_id):
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST']) @bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
@login_required @login_required
@corpus_follower_permission_required('ADD_CORPUS_FILE', 'UPDATE_CORPUS_FILE', 'REMOVE_CORPUS_FILE')
def corpus_file(corpus_id, corpus_file_id): def corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404() corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
if not (corpus_file.corpus.user == current_user or current_user.is_administrator()):
abort(403)
form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable()) form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable())
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(corpus_file) form.populate_obj(corpus_file)
@ -348,6 +339,7 @@ def corpus_file(corpus_id, corpus_file_id):
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE']) @bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE'])
@login_required @login_required
@corpus_follower_permission_required('REMOVE_CORPUS_FILE')
def delete_corpus_file(corpus_id, corpus_file_id): def delete_corpus_file(corpus_id, corpus_file_id):
def _delete_corpus_file(app, corpus_file_id): def _delete_corpus_file(app, corpus_file_id):
with app.app_context(): with app.app_context():
@ -368,6 +360,7 @@ def delete_corpus_file(corpus_id, corpus_file_id):
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download') @bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
@login_required @login_required
@corpus_follower_permission_required('VIEW')
def download_corpus_file(corpus_id, corpus_file_id): def download_corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404() corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
if not (corpus_file.corpus.user == current_user or current_user.is_administrator()): if not (corpus_file.corpus.user == current_user or current_user.is_administrator()):

View File

@ -27,20 +27,7 @@ def faq():
@bp.route('/dashboard') @bp.route('/dashboard')
@login_required @login_required
def dashboard(): def dashboard():
# users = [ return render_template('main/dashboard.html.j2', title='Dashboard')
# u.to_json_serializeable(filter_by_privacy_settings=True) for u
# in User.query.filter(User.is_public == True, User.id != current_user.id).all()
# ]
# corpora = [
# c.to_json_serializeable() for c
# in Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
# ]
return render_template(
'main/dashboard.html.j2',
title='Dashboard',
# users=users,
# corpora=corpora
)
@bp.route('/dashboard2') @bp.route('/dashboard2')
@ -67,3 +54,20 @@ def privacy_policy():
@bp.route('/terms_of_use') @bp.route('/terms_of_use')
def terms_of_use(): def terms_of_use():
return render_template('main/terms_of_use.html.j2', title='Terms of Use') return render_template('main/terms_of_use.html.j2', title='Terms of Use')
@bp.route('/social-area')
def social_area():
users = [
u.to_json_serializeable(relationships=True, filter_by_privacy_settings=True,) for u
in User.query.filter(User.is_public == True, User.id != current_user.id).all()
]
corpora = [
c.to_json_serializeable() for c
in Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
]
return render_template(
'main/social_area.html.j2',
users=users,
corpora=corpora,
title='Social Area'
)

View File

@ -0,0 +1,14 @@
class FollowedCorpusList extends CorpusList {
get item() {
return `
<tr class="list-item clickable hoverable">
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim();
}
}

View File

@ -13,14 +13,14 @@ class UserList extends ResourceList {
get item() { get item() {
return ` return `
<tr class="list-item clickable hoverable"> <tr class="list-item clickable hoverable">
<td><img alt="user-image" class="circle responsive-img avatar" style="width:50%"></td> <td><img alt="user-image" class="circle responsive-img avatar" style="width:25%"></td>
<td><b><span class="username"></span><b></td> <td><b><span class="username"></span><b></td>
<td><span class="full-name"></span></td> <td><span class="full-name"></span></td>
<td><span class="location"></span></td> <td><span class="location"></span></td>
<td><span class="organization"></span></td> <td><span class="organization"></span></td>
<td><span class="corpora-online"></span></td> <td><span class="corpora-online"></span></td>
<td class="right-align"> <td class="right-align">
<a class="list-action-trigger btn-floating waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a> <a class="list-action-trigger btn-floating waves-effect waves-light" data-list-action="view" style="background-color:#D9A36D"><i class="material-icons">send</i></a>
</td> </td>
</tr> </tr>
`.trim(); `.trim();
@ -77,7 +77,7 @@ class UserList extends ResourceList {
'full-name': user.full_name ? user.full_name : '', 'full-name': user.full_name ? user.full_name : '',
'location': user.location ? user.location : '', 'location': user.location ? user.location : '',
'organization': user.organization ? user.organization : '', 'organization': user.organization ? user.organization : '',
'corpora-online': '-' 'corpora-online': Object.values(user.corpora).filter((corpus) => corpus.is_public).length
}; };
}; };

View File

@ -22,6 +22,7 @@
'js/ResourceLists/CorpusFileList.js', 'js/ResourceLists/CorpusFileList.js',
'js/ResourceLists/PublicCorpusFileList.js', 'js/ResourceLists/PublicCorpusFileList.js',
'js/ResourceLists/CorpusList.js', 'js/ResourceLists/CorpusList.js',
'js/ResourceLists/FollowedCorpusList.js',
'js/ResourceLists/PublicCorpusList.js', 'js/ResourceLists/PublicCorpusList.js',
'js/ResourceLists/JobList.js', 'js/ResourceLists/JobList.js',
'js/ResourceLists/JobInputList.js', 'js/ResourceLists/JobInputList.js',

View File

@ -6,11 +6,10 @@
<div class="col s4"> <div class="col s4">
<a href="{{ url_for('users.user', user_id=current_user.id) }}"> <a href="{{ url_for('users.user', user_id=current_user.id) }}">
{% if current_user.avatar %} {% if current_user.avatar %}
<img src="{{ url_for('users.profile_avatar', user_id=current_user.id) }}" alt="user-image" class="circle responsive-img" style="height:80%; margin-top: 20px; margin-left:-15px;"> <img src="{{ url_for('users.profile_avatar', user_id=current_user.id) }}" alt="user-image" class="circle responsive-img" style="height:80%; margin-top: 13px; margin-left:-15px;">
{% else %} {% else %}
<img src="{{ url_for('static', filename='images/user_avatar.png') }}" alt="user-image" class="circle responsive-img" style="height:80%; margin-top: 20px; margin-left:-15px;"> <img src="{{ url_for('static', filename='images/user_avatar.png') }}" alt="user-image" class="circle responsive-img" style="height:80%; margin-top: 13px; margin-left:-15px;">
{% endif %} {% endif %}
{# <i class="material-icons" style="color:white; font-size:3em; margin-top: 25px; margin-left:-15px;">account_circle</i></div> #}
</a> </a>
</div> </div>
<div class="col s8"> <div class="col s8">
@ -27,6 +26,7 @@
<li><a href="{{ url_for('main.dashboard', _anchor='corpora') }}" style="padding-left: 47px;"><i class="nopaque-icons">I</i>My Corpora</a></li> <li><a href="{{ url_for('main.dashboard', _anchor='corpora') }}" style="padding-left: 47px;"><i class="nopaque-icons">I</i>My Corpora</a></li>
<li><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" style="padding-left: 47px;"><i class="nopaque-icons">J</i>My Jobs</a></li> <li><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" style="padding-left: 47px;"><i class="nopaque-icons">J</i>My Jobs</a></li>
<li><a href="{{ url_for('contributions.contributions') }}"><i class="material-icons">new_label</i>Contribute</a></li> <li><a href="{{ url_for('contributions.contributions') }}"><i class="material-icons">new_label</i>Contribute</a></li>
<li><a href="{{ url_for('main.social_area') }}"><i class="material-icons">group</i>Social Area</a></li>
<li><div class="divider"></div></li> <li><div class="divider"></div></li>
<li><a class="subheader">Processes & Services</a></li> <li><a class="subheader">Processes & Services</a></li>
<li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>File setup</a></li> <li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>File setup</a></li>

View File

@ -0,0 +1,70 @@
{% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} style="background-color:#d8c9ba86" {% endblock main_attribs %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12 m3 push-m9">
<div class="center-align">
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light" style="background-color:#D9A36D">
<i class="left material-icons">group</i>
</a>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card" style="border-top: 10px solid #D9A36D;">
<div class="card-content">
<div class="row">
<div class="col s12">
<div class="card-panel z-depth-0">
<span class="card-title"><i class="left material-icons">layers</i>Your social area</span>
<p>Here you can network with your team and other users. You can find corpora that are public and request them or just see what other users are working on.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col s12">
<h3>Other Users</h3>
<p>Find other users and see what corpora they have made public.</p>
<div class="card">
<div class="card-content">
<div class="user-list no-autoinit"></div>
</div>
</div>
</div>
<div class="col s12">
<h3>Public Corpora</h3>
<p>Find public corpora.</p>
<div class="card">
<div class="card-content">
<span class="card-title">Public Corpora</span>
<div class="public-corpus-list no-autoinit"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block scripts %}
{{ super() }}
<script>
let userList = new UserList(document.querySelector('.user-list'));
userList.add({{ users|tojson }});
let publicCorpusList = new PublicCorpusList(document.querySelector('.public-corpus-list'));
publicCorpusList.add({{ corpora|tojson }});
</script>
{% endblock scripts %}

View File

@ -26,7 +26,7 @@
</div> </div>
<div class="col 12"> <div class="col 12">
{% if user.show_last_seen %} {% if user.show_last_seen %}
<div class="chip">Last seen: {{ user.last_seen }}</div> <div class="chip">Last seen: {{ last_seen }}</div>
{% endif %} {% 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>
@ -76,7 +76,7 @@
</table> </table>
<br> <br>
{% if user.show_member_since %} {% if user.show_member_since %}
<p><i>Member since: {{ user.member_since }}</i></p> <p><i>Member since: {{ member_since }}</i></p>
{% endif %} {% endif %}
<p></p> <p></p>
<br> <br>
@ -93,7 +93,8 @@
<div class="col s6"> <div class="col s6">
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<h4>Groups</h4> <h4>Followed corpora</h4>
<div class="followed-corpus-list no-autoinit"></div>
</div> </div>
</div> </div>
</div> </div>
@ -101,7 +102,7 @@
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<h4>Public corpora</h4> <h4>Public corpora</h4>
<div class="public-corpora-list" data-user-id="{{ user.hashid }}"></div> <div class="public-corpus-list no-autoinit"></div>
</div> </div>
</div> </div>
</div> </div>
@ -127,6 +128,11 @@ if ("{{ user.id }}" == "{{ current_user.hashid }}") {
} else { } else {
publicInformationBadge.remove(); publicInformationBadge.remove();
} }
let followedCorpusList = new FollowedCorpusList(document.querySelector('.followed-corpus-list'));
followedCorpusList.add({{ followed_corpora|tojson }});
let publicCorpusList = new PublicCorpusList(document.querySelector('.public-corpus-list'));
publicCorpusList.add({{ own_public_corpora|tojson }});
</script> </script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -1,3 +1,4 @@
from datetime import datetime
from flask import ( from flask import (
abort, abort,
current_app, current_app,
@ -12,7 +13,7 @@ from flask_login import current_user, login_required
from threading import Thread from threading import Thread
import os import os
from app import db from app import db
from app.models import Avatar, ProfilePrivacySettings, User from app.models import Avatar, Corpus, ProfilePrivacySettings, User
from . import bp from . import bp
from .forms import ( from .forms import (
EditPrivacySettingsForm, EditPrivacySettingsForm,
@ -29,10 +30,23 @@ def before_request():
@login_required @login_required
def user(user_id): def user(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
last_seen = user.last_seen.strftime('%Y-%m-%d %H:%M')
member_since = user.member_since.strftime('%Y-%m-%d')
followed_corpora = [
c.to_json_serializeable() for c in user.followed_corpora
]
own_public_corpora = [
c.to_json_serializeable() for c
in Corpus.query.filter_by(is_public = True, user = user).all()
]
if not user.is_public and user != current_user: if not user.is_public and user != current_user:
abort(403) abort(403)
return render_template( return render_template(
'users/profile.html.j2', 'users/profile.html.j2',
followed_corpora=followed_corpora,
last_seen=last_seen,
member_since=member_since,
own_public_corpora=own_public_corpora,
user=user.to_json_serializeable(), user=user.to_json_serializeable(),
user_id=user_id user_id=user_id
) )
@ -56,18 +70,6 @@ def delete_user(user_id):
thread.start() thread.start()
return {}, 202 return {}, 202
@bp.route('/<hashid:user_id>')
def profile(user_id):
user = User.query.get_or_404(user_id)
if not user.is_public and user != current_user:
abort(403)
return render_template(
'users/profile.html.j2',
user=user.to_json_serializeable(),
user_id=user_id
)
@bp.route('/<hashid:user_id>/avatar') @bp.route('/<hashid:user_id>/avatar')
def profile_avatar(user_id): def profile_avatar(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
@ -91,7 +93,6 @@ def delete_profile_avatar(user_id):
avatar = Avatar.query.get(avatar_id) avatar = Avatar.query.get(avatar_id)
avatar.delete() avatar.delete()
db.session.commit() db.session.commit()
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
if user.avatar is None: if user.avatar is None:
abort(404) abort(404)