Merge branch 'following-mechanics' into development

This commit is contained in:
Inga Kirschnick 2023-02-07 08:58:30 +01:00
commit 96520fa46f
10 changed files with 256 additions and 14 deletions

View File

@ -1,6 +1,7 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired from flask_wtf.file import FileField, FileRequired
from wtforms import ( from wtforms import (
BooleanField,
StringField, StringField,
SubmitField, SubmitField,
TextAreaField, TextAreaField,
@ -77,6 +78,9 @@ class UpdateCorpusFileForm(CorpusFileBaseForm):
kwargs['prefix'] = 'update-corpus-file-form' kwargs['prefix'] = 'update-corpus-file-form'
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class ChangeCorpusSettingsForm(FlaskForm):
is_public = BooleanField('Public Corpus')
submit = SubmitField()
class ImportCorpusForm(FlaskForm): class ImportCorpusForm(FlaskForm):
pass pass

View File

@ -5,15 +5,17 @@ from flask import (
Markup, Markup,
redirect, redirect,
render_template, render_template,
send_from_directory request,
send_from_directory,
url_for
) )
from flask_login import current_user, login_required 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, hashids
from app.models import Corpus, CorpusFile, CorpusStatus from app.models import Corpus, CorpusFile, CorpusStatus, CorpusFollowerAssociation, User
from . import bp from . import bp
from .forms import CreateCorpusFileForm, CreateCorpusForm, UpdateCorpusFileForm from .forms import ChangeCorpusSettingsForm, CreateCorpusFileForm, CreateCorpusForm, UpdateCorpusFileForm
def user_can_read_corpus(user, corpus): def user_can_read_corpus(user, corpus):
@ -64,19 +66,35 @@ def create_corpus():
) )
@bp.route('/<hashid:corpus_id>') @bp.route('/<hashid:corpus_id>', methods=['GET', 'POST'])
@login_required @login_required
def corpus(corpus_id): def corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if not user_can_read_corpus(current_user, corpus): if not user_can_read_corpus(current_user, corpus):
abort(403) abort(403)
corpus_settings_form = ChangeCorpusSettingsForm(
data=corpus.to_json_serializeable(),
prefix='corpus-settings-form'
)
if corpus_settings_form.validate_on_submit():
corpus.is_public = corpus_settings_form.is_public.data
db.session.commit()
flash('Your changes have been saved')
return redirect(url_for('.corpus', corpus_id=corpus.id))
# following_users = [
# u.to_json_serializeable() for u
# in corpus.following_users
# ]
return render_template( return render_template(
'corpora/corpus.html.j2', 'corpora/corpus.html.j2',
corpus_settings_form=corpus_settings_form,
corpus=corpus, corpus=corpus,
# following_users=following_users,
title='Corpus' title='Corpus'
) )
# @bp.route('/<hashid:corpus_id>/update') # @bp.route('/<hashid:corpus_id>/update')
# @login_required # @login_required
# def update_corpus(corpus_id): # def update_corpus(corpus_id):
@ -263,3 +281,54 @@ def import_corpus():
@login_required @login_required
def export_corpus(corpus_id): def export_corpus(corpus_id):
abort(503) abort(503)
@bp.route('/<hashid:corpus_id>/follow', methods=['GET', 'POST'])
@login_required
def follow_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
user_hashid = request.args.get('user_id')
if user_hashid is None:
user = current_user
else:
if not current_user.is_administrator():
abort(403)
else:
user_id = hashids.decode(user_hashid)
user = User.query.get_or_404(user_id)
if not user.is_following_corpus(corpus):
user.follow_corpus(corpus)
db.session.commit()
return {}, 202
@bp.route('/<hashid:corpus_id>/unfollow', methods=['GET', 'POST'])
@login_required
def unfollow_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
user_hashid = request.args.get('user_id')
if user_hashid is None:
user = current_user
else:
if not current_user.is_administrator():
abort(403)
else:
user_id = hashids.decode(user_hashid)
user = User.query.get_or_404(user_id)
if user.is_following_corpus(corpus):
user.unfollow_corpus(corpus)
db.session.commit()
return {}, 202
@bp.route('/add_permission/<hashid:corpus_id>/<hashid:user_id>/<int:permission>')
def add_permission(corpus_id, user_id, permission):
a = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
a.add_permission(permission)
db.session.commit()
return 'ok'
@bp.route('/remove_permission/<hashid:corpus_id>/<hashid:user_id>/<int:permission>')
def remove_permission(corpus_id, user_id, permission):
a = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
a.remove_permission(permission)
db.session.commit()
return 'ok'

View File

@ -1,7 +1,7 @@
from flask import flash, redirect, render_template, url_for from flask import flash, redirect, render_template, url_for
from flask_login import current_user, login_required, login_user from flask_login import current_user, login_required, login_user
from app.auth.forms import LoginForm from app.auth.forms import LoginForm
from app.models import User from app.models import Corpus, User
from . import bp from . import bp
@ -31,7 +31,11 @@ def dashboard():
u.to_json_serializeable(filter_by_privacy_settings=True) for u 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() in User.query.filter(User.is_public == True, User.id != current_user.id).all()
] ]
return render_template('main/dashboard.html.j2', title='Dashboard', users=users) corpora = [
c.to_json_serializeable() for c
in Corpus.query.filter(Corpus.is_public == True).all()
]
return render_template('main/dashboard.html.j2', title='Dashboard', users=users, corpora=corpora)
@bp.route('/dashboard2') @bp.route('/dashboard2')

View File

@ -68,6 +68,11 @@ class ProfilePrivacySettings(IntEnum):
SHOW_EMAIL = 1 SHOW_EMAIL = 1
SHOW_LAST_SEEN = 2 SHOW_LAST_SEEN = 2
SHOW_MEMBER_SINCE = 4 SHOW_MEMBER_SINCE = 4
class CorpusFollowPermission(IntEnum):
VIEW = 1
CONTRIBUTE = 2
ADMINISTRATE = 4
# endregion enums # endregion enums
@ -298,6 +303,16 @@ class CorpusFollowerAssociation(db.Model):
def __repr__(self): def __repr__(self):
return f'<CorpusFollowerAssociation {self.following_user.__repr__()} ~ {self.followed_corpus.__repr__()}>' return f'<CorpusFollowerAssociation {self.following_user.__repr__()} ~ {self.followed_corpus.__repr__()}>'
def has_permission(self, permission):
return self.permissions & permission == permission
def add_permission(self, permission):
if not self.has_permission(permission):
self.permissions += permission
def remove_permission(self, permission):
if self.has_permission(permission):
self.permissions -= permission
class User(HashidMixin, UserMixin, db.Model): class User(HashidMixin, UserMixin, db.Model):
__tablename__ = 'users' __tablename__ = 'users'
@ -576,6 +591,18 @@ class User(HashidMixin, UserMixin, db.Model):
self.profile_privacy_settings = 0 self.profile_privacy_settings = 0
#endregion Profile Privacy settings #endregion Profile Privacy settings
def follow_corpus(self, corpus):
if not self.is_following_corpus(corpus):
self.followed_corpora.append(corpus)
def unfollow_corpus(self, corpus):
if self.is_following_corpus(corpus):
self.followed_corpora.remove(corpus)
def is_following_corpus(self, corpus):
return corpus in self.followed_corpora
def to_json_serializeable(self, backrefs=False, relationships=False, filter_by_privacy_settings=False): def to_json_serializeable(self, backrefs=False, relationships=False, filter_by_privacy_settings=False):
json_serializeable = { json_serializeable = {
'id': self.hashid, 'id': self.hashid,
@ -623,6 +650,10 @@ class User(HashidMixin, UserMixin, db.Model):
x.hashid: x.to_json_serializeable(relationships=True) x.hashid: x.to_json_serializeable(relationships=True)
for x in self.spacy_nlp_pipeline_models for x in self.spacy_nlp_pipeline_models
} }
json_serializeable['followed_corpora'] = {
x.hashid: x.to_json_serializeable(relationships=True)
for x in self.followed_corpora
}
if filter_by_privacy_settings: if filter_by_privacy_settings:
if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL): if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL):

View File

@ -17,7 +17,7 @@ class CorpusFileList extends ResourceList {
}); });
}); });
app.getUser(this.userId).then((user) => { app.getUser(this.userId).then((user) => {
this.add(Object.values(user.corpora[this.corpusId].files)); this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
this.isInitialized = true; this.isInitialized = true;
}); });
} }

View File

@ -1,7 +1,7 @@
class UserList extends ResourceList { class UserList extends ResourceList {
static autoInit() { static autoInit() {
for (let publicUserListElement of document.querySelectorAll('.user-list:not(.no-autoinit)')) { for (let userListElement of document.querySelectorAll('.user-list:not(.no-autoinit)')) {
new UserList(publicUserListElement); new UserList(userListElement);
} }
} }
@ -41,14 +41,14 @@ class UserList extends ResourceList {
initListContainerElement() { initListContainerElement() {
if (!this.listContainerElement.hasAttribute('id')) { if (!this.listContainerElement.hasAttribute('id')) {
this.listContainerElement.id = Utils.generateElementId('public-user-list-'); this.listContainerElement.id = Utils.generateElementId('user-list-');
} }
let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`); let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`);
this.listContainerElement.innerHTML = ` this.listContainerElement.innerHTML = `
<div class="input-field"> <div class="input-field">
<i class="material-icons prefix">search</i> <i class="material-icons prefix">search</i>
<input id="${listSearchElementId}" class="search" type="text"></input> <input id="${listSearchElementId}" class="search" type="text"></input>
<label for="${listSearchElementId}">Search public user</label> <label for="${listSearchElementId}">Search user</label>
</div> </div>
<table> <table>
<thead> <thead>
@ -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': '0' 'corpora-online': '-'
}; };
}; };

View File

@ -20,6 +20,7 @@
<li><a href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a></li> <li><a href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</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='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('main.dashboard', _anchor='social') }}" style="padding-left: 47px;"><i class="material-icons">groups</i>Social</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><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>

View File

@ -1,4 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %}
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %} {% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %} {% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
@ -9,7 +10,15 @@
<div class="col s12" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}" id="corpus-display"> <div class="col s12" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}" id="corpus-display">
<div class="row"> <div class="row">
<div class="col s8 m9 l10"> <div class="col s8 m9 l10">
<h1 id="title"><span class="corpus-title"></span></h1> {# <h1 id="title"><span class="corpus-title"></span></h1> #}
<h1 id="title">{{ corpus.title }}</h1>
{% if not corpus.user == current_user %}
{% if current_user.is_following_corpus(corpus) %}
<a class="btn waves-effect waves-light" id="follow-corpus-request"><i class="material-icons left">add</i>Unfollow Corpus</a>
{% elif not current_user.is_following_corpus(corpus) %}
<a class="btn waves-effect waves-light" id="follow-corpus-request"><i class="material-icons left">add</i>Follow Corpus</a>
{% endif %}
{% endif %}
</div> </div>
<div class="col s4 m3 l2 right-align"> <div class="col s4 m3 l2 right-align">
<p>&nbsp;</p> <p>&nbsp;</p>
@ -76,6 +85,33 @@
</div> </div>
</div> </div>
</div> </div>
{% if current_user.can(Permission.ADMINISTRATE) or current_user.hashid == corpus.user.hashid %}
<div class="col s12">
<form method="POST">
{{ corpus_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title" id="files">Corpus settings</span>
<br>
<p></p>
{{ wtf.render_field(corpus_settings_form.is_public) }}
<br>
</div>
<div class="card-action right-align">
{{ wtf.render_field(corpus_settings_form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
{% endif %}
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title" id="files">Corpus followers</span>
<div class="user-list no-autoinit"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock page_content %} {% endblock page_content %}
@ -84,5 +120,56 @@
{{ super() }} {{ super() }}
<script> <script>
let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display')); let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
let corpusFollowingRequest = document.querySelector('#follow-corpus-request');
{# let followingUserList = new UserList(document.querySelector('.user-list'));
followingUserList.add({{ following_users|tojson }}); #}
corpusFollowingRequest.addEventListener('click', function() {
corpusFollowingRequest.innerHTML = '<i class="material-icons left">add</i>Unfollow Corpus';
if ("{{ current_user.is_following_corpus(corpus) }}" === "False") {
corpusFollowingRequest.lastChild.textContent = 'Unfollow Corpus';
return new Promise((resolve, reject) => {
fetch(`/corpora/{{ corpus.hashid }}/follow`, {method: 'POST', 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);}
if (response.status === 409) {app.flash('Conflict', 'error'); reject(response);}
app.flash(`You follow "{{ corpus.title }}" now`, 'corpus');
window.location.href = '{{ url_for("corpora.corpus", corpus_id=corpus.id) }}'
resolve(response);
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
});
} else {
corpusFollowingRequest.innerHTML = '<i class="material-icons left">add</i>Unfollow Corpus';
return new Promise((resolve, reject) => {
fetch(`/corpora/{{ corpus.hashid }}/unfollow`, {method: 'POST', 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);}
if (response.status === 409) {app.flash('Conflict', 'error'); reject(response);}
app.flash(`You are not following "{{ corpus.title }}" anymore`, 'corpus');
resolve(response);
window.location.href = '{{ url_for("corpora.corpus", corpus_id=corpus.id) }}'
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
});
}
});
</script> </script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -42,6 +42,23 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col s12" id="social">
<h3>Social</h3>
<div class="card">
<div class="card-content">
<span class="card-title">Other users</span>
<p>Find other users and follow them to see their corpora.</p>
<div class="user-list no-autoinit"></div>
</div>
</div>
<div class="card">
<div class="card-content">
<span class="card-title">Public corpora</span>
<p>Find public corpora</p>
<div class="public-corpus-list no-autoinit"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock page_content %} {% endblock page_content %}
@ -96,3 +113,14 @@
</div> </div>
</div> </div>
{% endblock modals %} {% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
let userList = new UserList(document.querySelector('.user-list'));
userList.add({{ users|tojson }});
let publicCorpusList = new CorpusList(document.querySelector('.public-corpus-list'));
publicCorpusList.add({{ corpora|tojson }});
</script>
{% endblock scripts %}

View File

@ -89,6 +89,24 @@
</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> </div>
{% endblock page_content %} {% endblock page_content %}