From e2cdcd3f7cf3944873ec146cc492dd3a09dcb5b8 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick Date: Tue, 31 Jan 2023 08:59:42 +0100 Subject: [PATCH 1/3] 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('/') +@bp.route('/', 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('//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('//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('//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///') +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///') +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'' + 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 = `
search - +
@@ -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 @@
  • dashboardDashboard
  • IMy Corpora
  • JMy Jobs
  • +
  • groupsSocial
  • new_labelContribute
  • Processes & Services
  • 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 @@

    + {% if not corpus.user == current_user %} + addFollow Corpus + {% endif %}

     

    @@ -76,6 +80,24 @@
    + {% if current_user.can(Permission.ADMINISTRATE) or current_user.hashid == corpus.user.hashid %} +
    +
    + {{ corpus_settings_form.hidden_tag() }} +
    +
    + Corpus settings +
    +

    + {{ wtf.render_field(corpus_settings_form.is_public) }} +
    +
    + {{ wtf.render_field(corpus_settings_form.submit, material_icon='send') }} +
    +
    + +
    + {% endif %} {% endblock page_content %} @@ -84,5 +106,25 @@ {{ super() }} {% 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 @@ +
    +

    Social

    +
    +
    + Other users +

    Find other users and follow them to see their corpora.

    +
    +
    +
    +
    +
    + Public corpora +

    Find public corpora

    +
    +
    +
    +
    {% endblock page_content %} @@ -96,3 +113,14 @@ {% endblock modals %} + +{% block scripts %} +{{ super() }} + +{% 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 @@ +
    +
    +
    +
    +

    Groups

    +
    +
    +
    +
    +
    +
    +

    Public corpora

    +
    +
    +
    +
    +
    + {% endblock page_content %} From b0f61b28fe270bc61b9b74fe80b72602ecf17cf3 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick Date: Thu, 2 Feb 2023 09:14:50 +0100 Subject: [PATCH 2/3] Small changes on the Corpus page --- app/corpora/routes.py | 14 +++-- app/templates/corpora/corpus.html.j2 | 85 +++++++++++++++++++++------- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/app/corpora/routes.py b/app/corpora/routes.py index a5035087..f2128479 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -81,10 +81,18 @@ def corpus(corpus_id): db.session.commit() flash('Your changes have been saved') return redirect(url_for('.corpus', corpus_id=corpus.id)) + # if corpus.following_users == [None]: + # following_users = [] + # else: + # 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' ) @@ -277,10 +285,8 @@ def import_corpus(): def export_corpus(corpus_id): abort(503) -@bp.route('//follow') +@bp.route('//follow', methods=['GET', 'POST']) @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') @@ -297,7 +303,7 @@ def follow_corpus(corpus_id): db.session.commit() return {}, 202 -@bp.route('//unfollow') +@bp.route('//unfollow', methods=['GET', 'POST']) @login_required def unfollow_corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2 index 00e10bba..0f7b0bf4 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -10,10 +10,15 @@
    -

    + {#

    #} +

    {{ corpus.title }}

    {% if not corpus.user == current_user %} + {% if current_user.is_following_corpus(corpus) %} + addUnfollow Corpus + {% elif not current_user.is_following_corpus(corpus) %} addFollow Corpus {% endif %} + {% endif %}

     

    @@ -90,6 +95,7 @@

    {{ wtf.render_field(corpus_settings_form.is_public) }} +
    {{ wtf.render_field(corpus_settings_form.submit, material_icon='send') }} @@ -98,6 +104,14 @@
    {% endif %} +
    +
    +
    + Corpus followers +
    +
    +
    +
    {% endblock page_content %} @@ -106,25 +120,56 @@ {{ super() }} {% endblock scripts %} From bd6f94582b4e8b78b7c239f3898b2eea9679668f Mon Sep 17 00:00:00 2001 From: Inga Kirschnick Date: Mon, 6 Feb 2023 10:52:36 +0100 Subject: [PATCH 3/3] some testing --- app/corpora/routes.py | 11 ++++------- app/models.py | 4 ++++ app/static/js/ResourceLists/CorpusFileList.js | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/corpora/routes.py b/app/corpora/routes.py index f2128479..4c1ebd29 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -81,13 +81,10 @@ def corpus(corpus_id): db.session.commit() flash('Your changes have been saved') return redirect(url_for('.corpus', corpus_id=corpus.id)) - # if corpus.following_users == [None]: - # following_users = [] - # else: - # following_users = [ - # u.to_json_serializeable() for u - # in corpus.following_users - # ] + # 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, diff --git a/app/models.py b/app/models.py index 24f59d60..4c7a1362 100644 --- a/app/models.py +++ b/app/models.py @@ -650,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): diff --git a/app/static/js/ResourceLists/CorpusFileList.js b/app/static/js/ResourceLists/CorpusFileList.js index fd4ff480..ca27fe27 100644 --- a/app/static/js/ResourceLists/CorpusFileList.js +++ b/app/static/js/ResourceLists/CorpusFileList.js @@ -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; }); }