From e2cdcd3f7cf3944873ec146cc492dd3a09dcb5b8 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick <inga.kirschnick@uni-bielefeld.de> Date: Tue, 31 Jan 2023 08:59:42 +0100 Subject: [PATCH] first implementation of follow mechanics --- app/corpora/forms.py | 4 ++ app/corpora/routes.py | 76 +++++++++++++++++++++++-- app/main/routes.py | 8 ++- app/models.py | 27 +++++++++ app/static/js/ResourceLists/UserList.js | 10 ++-- app/templates/_sidenav.html.j2 | 1 + app/templates/corpora/corpus.html.j2 | 42 ++++++++++++++ app/templates/main/dashboard.html.j2 | 28 +++++++++ app/templates/users/profile.html.j2 | 18 ++++++ 9 files changed, 202 insertions(+), 12 deletions(-) diff --git a/app/corpora/forms.py b/app/corpora/forms.py index 12ec1d9c..8950622d 100644 --- a/app/corpora/forms.py +++ b/app/corpora/forms.py @@ -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 diff --git a/app/corpora/routes.py b/app/corpora/routes.py index f3cdf8f1..a5035087 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -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' diff --git a/app/main/routes.py b/app/main/routes.py index 9935c479..aed63853 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -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') diff --git a/app/models.py b/app/models.py index af3d1cfc..24f59d60 100644 --- a/app/models.py +++ b/app/models.py @@ -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, diff --git a/app/static/js/ResourceLists/UserList.js b/app/static/js/ResourceLists/UserList.js index d871ec31..03fabd73 100644 --- a/app/static/js/ResourceLists/UserList.js +++ b/app/static/js/ResourceLists/UserList.js @@ -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': '-' }; }; diff --git a/app/templates/_sidenav.html.j2 b/app/templates/_sidenav.html.j2 index eb547454..676b7069 100644 --- a/app/templates/_sidenav.html.j2 +++ b/app/templates/_sidenav.html.j2 @@ -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> diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2 index 16af7d31..00e10bba 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -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> </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 %} diff --git a/app/templates/main/dashboard.html.j2 b/app/templates/main/dashboard.html.j2 index 5474db97..5391a8af 100644 --- a/app/templates/main/dashboard.html.j2 +++ b/app/templates/main/dashboard.html.j2 @@ -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 %} + diff --git a/app/templates/users/profile.html.j2 b/app/templates/users/profile.html.j2 index a98a372d..a6ad5b30 100644 --- a/app/templates/users/profile.html.j2 +++ b/app/templates/users/profile.html.j2 @@ -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 %} -- GitLab