first implementation of follow mechanics

This commit is contained in:
Inga Kirschnick 2023-01-31 08:59:42 +01:00
parent 97bba2498b
commit e2cdcd3f7c
9 changed files with 202 additions and 12 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,30 @@ 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))
return render_template(
'corpora/corpus.html.j2',
corpus_settings_form=corpus_settings_form,
corpus=corpus,
title='Corpus'
)
# @bp.route('/<hashid:corpus_id>/update')
# @login_required
# def update_corpus(corpus_id):
@ -263,3 +276,56 @@ def import_corpus():
@login_required
def export_corpus(corpus_id):
abort(503)
@bp.route('/<hashid:corpus_id>/follow')
@login_required
# TODO: Wenn Query Paramter genutzt wird, prüfen, ob user_id ungleich current_user.id ist und dann gucken, ob es ein Admin ist.
# Sonst 403.
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')
@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,

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 %}
@ -10,6 +11,9 @@
<div class="row">
<div class="col s8 m9 l10">
<h1 id="title"><span class="corpus-title"></span></h1>
{% if not corpus.user == current_user %}
<a class="btn waves-effect waves-light" id="follow-corpus-request"><i class="material-icons left">add</i>Follow Corpus</a>
{% endif %}
</div>
<div class="col s4 m3 l2 right-align">
<p>&nbsp;</p>
@ -76,6 +80,24 @@
</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) }}
</div>
<div class="card-action right-align">
{{ wtf.render_field(corpus_settings_form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
{% endif %}
</div>
</div>
{% endblock page_content %}
@ -84,5 +106,25 @@
{{ super() }}
<script>
let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
let followCorpusRequest = document.querySelector('#follow-corpus-request');
followCorpusRequest.addEventListener('click', function() {
return new Promise((resolve, reject) => {
fetch(`/corpora/${corpusId}/build`, {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(`Corpus "${corpus?.title}" marked for building`, 'corpus');
resolve(response);
},
(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 %}