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

View File

@ -5,15 +5,17 @@ from flask import (
Markup,
redirect,
render_template,
send_from_directory
request,
send_from_directory,
url_for
)
from flask_login import current_user, login_required
from threading import Thread
import os
from app import db
from app.models import Corpus, CorpusFile, CorpusStatus
from app import db, hashids
from app.models import Corpus, CorpusFile, CorpusStatus, CorpusFollowerAssociation, User
from . import bp
from .forms import CreateCorpusFileForm, CreateCorpusForm, UpdateCorpusFileForm
from .forms import ChangeCorpusSettingsForm, CreateCorpusFileForm, CreateCorpusForm, UpdateCorpusFileForm
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
def corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
if not user_can_read_corpus(current_user, corpus):
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(
'corpora/corpus.html.j2',
corpus_settings_form=corpus_settings_form,
corpus=corpus,
# following_users=following_users,
title='Corpus'
)
# @bp.route('/<hashid:corpus_id>/update')
# @login_required
# def update_corpus(corpus_id):
@ -263,3 +281,54 @@ def import_corpus():
@login_required
def export_corpus(corpus_id):
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_login import current_user, login_required, login_user
from app.auth.forms import LoginForm
from app.models import User
from app.models import Corpus, User
from . import bp
@ -31,7 +31,11 @@ def 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()
]
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')

View File

@ -68,6 +68,11 @@ class ProfilePrivacySettings(IntEnum):
SHOW_EMAIL = 1
SHOW_LAST_SEEN = 2
SHOW_MEMBER_SINCE = 4
class CorpusFollowPermission(IntEnum):
VIEW = 1
CONTRIBUTE = 2
ADMINISTRATE = 4
# endregion enums
@ -298,6 +303,16 @@ class CorpusFollowerAssociation(db.Model):
def __repr__(self):
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):
__tablename__ = 'users'
@ -576,6 +591,18 @@ class User(HashidMixin, UserMixin, db.Model):
self.profile_privacy_settings = 0
#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):
json_serializeable = {
'id': self.hashid,
@ -623,6 +650,10 @@ class User(HashidMixin, UserMixin, db.Model):
x.hashid: x.to_json_serializeable(relationships=True)
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 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) => {
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;
});
}

View File

@ -1,7 +1,7 @@
class UserList extends ResourceList {
static autoInit() {
for (let publicUserListElement of document.querySelectorAll('.user-list:not(.no-autoinit)')) {
new UserList(publicUserListElement);
for (let userListElement of document.querySelectorAll('.user-list:not(.no-autoinit)')) {
new UserList(userListElement);
}
}
@ -41,14 +41,14 @@ class UserList extends ResourceList {
initListContainerElement() {
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-`);
this.listContainerElement.innerHTML = `
<div class="input-field">
<i class="material-icons prefix">search</i>
<input id="${listSearchElementId}" class="search" type="text"></input>
<label for="${listSearchElementId}">Search public user</label>
<label for="${listSearchElementId}">Search user</label>
</div>
<table>
<thead>
@ -77,7 +77,7 @@ class UserList extends ResourceList {
'full-name': user.full_name ? user.full_name : '',
'location': user.location ? user.location : '',
'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', _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='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><div class="divider"></div></li>
<li><a class="subheader">Processes & Services</a></li>

View File

@ -1,4 +1,5 @@
{% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %}
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% 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="row">
<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 class="col s4 m3 l2 right-align">
<p>&nbsp;</p>
@ -76,6 +85,33 @@
</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>
{% endblock page_content %}
@ -84,5 +120,56 @@
{{ super() }}
<script>
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>
{% endblock scripts %}

View File

@ -42,6 +42,23 @@
</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>
{% endblock page_content %}
@ -96,3 +113,14 @@
</div>
</div>
{% 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 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 %}