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>&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 %}
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