From 2dc41fd3871a3624362c9a19e8e16b06390fe09c Mon Sep 17 00:00:00 2001
From: Patrick Jentsch
Date: Mon, 20 Feb 2023 10:40:33 +0100
Subject: [PATCH] Add CorpusFollowerList and functions
---
app/corpora/routes.py | 38 ++---
app/models.py | 34 +++-
.../js/ResourceLists/CorpusFollowerList.js | 158 ++++++++++++++++++
app/static/js/ResourceLists/ResourceList.js | 1 +
app/static/js/Utils.js | 14 +-
app/static/js_new/App.js | 10 ++
app/templates/_scripts.html.j2 | 1 +
app/templates/corpora/corpus.html.j2 | 16 +-
8 files changed, 234 insertions(+), 38 deletions(-)
create mode 100644 app/static/js/ResourceLists/CorpusFollowerList.js
create mode 100644 app/static/js_new/App.js
diff --git a/app/corpora/routes.py b/app/corpora/routes.py
index 8d2dd45f..a3aca488 100644
--- a/app/corpora/routes.py
+++ b/app/corpora/routes.py
@@ -18,7 +18,7 @@ from app.models import (
Corpus,
CorpusFile,
CorpusFollowerAssociation,
- CorpusFollowPermission,
+ CorpusFollowerPermission,
CorpusStatus,
User
)
@@ -30,7 +30,7 @@ from .forms import (
)
-@bp.route('//enable_is_public', methods=['POST'])
+@bp.route('//is_public/enable', methods=['POST'])
@login_required
def enable_corpus_is_public(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
@@ -41,7 +41,7 @@ def enable_corpus_is_public(corpus_id):
return '', 204
-@bp.route('//disable_is_public', methods=['POST'])
+@bp.route('//is_public/disable', methods=['POST'])
@login_required
def disable_corpus_is_public(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
@@ -89,30 +89,30 @@ def unfollow_corpus(corpus_id):
return '', 204
-@bp.route('//followers//permissions/add', methods=['POST'])
-def add_permission(corpus_id, user_id, permission):
- corpus_follow_association = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
- permission = request.args.get('permission', type=int)
- corpus = corpus_follow_association.followed_corpus
+@bp.route('//followers//permissions//add', methods=['POST'])
+def add_permission(corpus_id, corpus_follower_id, permission_name):
+ if permission_name not in [x.name for x in CorpusFollowerPermission]:
+ abort(400)
+ corpus_follower_association = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, id=corpus_follower_id).first_or_404()
+ corpus = corpus_follower_association.followed_corpus
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
- if permission is None or permission not in iter(CorpusFollowPermission):
- abort(400)
- corpus_follow_association.add_permission(permission)
+ permission_value = CorpusFollowerPermission[permission_name].value
+ corpus_follower_association.add_permission(permission_value)
db.session.commit()
return '', 204
-@bp.route('//followers//permissions/remove', methods=['POST'])
-def remove_permission(corpus_id, user_id, permission):
- corpus_follow_association = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
- permission = request.args.get('permission')
- corpus = corpus_follow_association.followed_corpus
+@bp.route('//followers//permissions//remove', methods=['POST'])
+def remove_permission(corpus_id, corpus_follower_id, permission_name):
+ if permission_name not in [x.name for x in CorpusFollowerPermission]:
+ abort(400)
+ corpus_follower_association = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, id=corpus_follower_id).first_or_404()
+ corpus = corpus_follower_association.followed_corpus
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
- if permission is None or permission not in iter(CorpusFollowPermission):
- abort(400)
- corpus_follow_association.remove_permission(permission)
+ permission_value = CorpusFollowerPermission[permission_name].value
+ corpus_follower_association.remove_permission(permission_value)
db.session.commit()
return '', 204
diff --git a/app/models.py b/app/models.py
index 4c7a1362..314ebf2a 100644
--- a/app/models.py
+++ b/app/models.py
@@ -69,7 +69,8 @@ class ProfilePrivacySettings(IntEnum):
SHOW_LAST_SEEN = 2
SHOW_MEMBER_SINCE = 4
-class CorpusFollowPermission(IntEnum):
+
+class CorpusFollowerPermission(IntEnum):
VIEW = 1
CONTRIBUTE = 2
ADMINISTRATE = 4
@@ -199,7 +200,10 @@ class Role(HashidMixin, db.Model):
'id': self.hashid,
'default': self.default,
'name': self.name,
- 'permissions': self.permissions
+ 'permissions': [
+ x.name for x in Permission
+ if self.has_permission(x.value)
+ ]
}
if relationships:
json_serializeable['users'] = {
@@ -287,7 +291,7 @@ class Avatar(HashidMixin, FileMixin, db.Model):
return json_serializeable
-class CorpusFollowerAssociation(db.Model):
+class CorpusFollowerAssociation(HashidMixin, db.Model):
__tablename__ = 'corpus_follower_associations'
# Primary key
id = db.Column(db.Integer, primary_key=True)
@@ -314,6 +318,19 @@ class CorpusFollowerAssociation(db.Model):
if self.has_permission(permission):
self.permissions -= permission
+ def to_json_serializeable(self, backrefs=False, relationships=False):
+ json_serializeable = {
+ 'id': self.hashid,
+ 'permissions': [
+ x.name for x in CorpusFollowerPermission
+ if self.has_permission(x.value)
+ ],
+ 'followed_corpus': self.followed_corpus.to_json_serializeable(),
+ 'following_user': self.following_user.to_json_serializeable()
+ }
+ return json_serializeable
+
+
class User(HashidMixin, UserMixin, db.Model):
__tablename__ = 'users'
# Primary key
@@ -634,6 +651,10 @@ class User(HashidMixin, UserMixin, db.Model):
json_serializeable['role'] = \
self.role.to_json_serializeable(backrefs=True)
if relationships:
+ json_serializeable['followed_corpus_associations'] = {
+ x.hashid: x.to_json_serializeable()
+ for x in self.followed_corpus_associations
+ }
json_serializeable['corpora'] = {
x.hashid: x.to_json_serializeable(relationships=True)
for x in self.corpora
@@ -1403,8 +1424,13 @@ class Corpus(HashidMixin, db.Model):
'is_public': self.is_public
}
if backrefs:
- json_serializeable['user'] = self.user.to_json_serializeable(backrefs=True)
+ json_serializeable['user'] = \
+ self.user.to_json_serializeable(backrefs=True)
if relationships:
+ json_serializeable['following_user_associations'] = {
+ x.hashid: x.to_json_serializeable()
+ for x in self.following_user_associations
+ }
json_serializeable['files'] = {
x.hashid: x.to_json_serializeable(relationships=True)
for x in self.files
diff --git a/app/static/js/ResourceLists/CorpusFollowerList.js b/app/static/js/ResourceLists/CorpusFollowerList.js
new file mode 100644
index 00000000..cc7471d0
--- /dev/null
+++ b/app/static/js/ResourceLists/CorpusFollowerList.js
@@ -0,0 +1,158 @@
+class CorpusFollowerList extends ResourceList {
+ static autoInit() {
+ for (let corpusFollowerListElement of document.querySelectorAll('.corpus-follower-list:not(.no-autoinit)')) {
+ new CorpusFollowerList(corpusFollowerListElement);
+ }
+ }
+
+ constructor(listContainerElement, options = {}) {
+ super(listContainerElement, options);
+ this.listjs.list.addEventListener('change', (event) => {this.onChange(event)});
+ this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
+ this.isInitialized = false;
+ this.userId = listContainerElement.dataset.userId;
+ this.corpusId = listContainerElement.dataset.corpusId;
+ if (this.userId === undefined || this.corpusId === undefined) {return;}
+ app.subscribeUser(this.userId).then((response) => {
+ app.socket.on('PATCH', (patch) => {
+ if (this.isInitialized) {this.onPatch(patch);}
+ });
+ });
+ app.getUser(this.userId).then((user) => {
+ this.add(Object.values(user.corpora[this.corpusId].following_user_associations));
+ this.isInitialized = true;
+ });
+ }
+
+ get item() {
+ return (values) => {
+ return `
+
+ |
+ |
+
|
+
+
+
+
+
+
+ |
+
+ delete
+ send
+ |
+
+ `.trim();
+ }
+ }
+
+ get valueNames() {
+ return [
+ {data: ['id']},
+ {name: 'avatar', attr: 'src'},
+ 'username',
+ 'about-me',
+ 'full-name'
+ ];
+ }
+
+ initListContainerElement() {
+ if (!this.listContainerElement.hasAttribute('id')) {
+ this.listContainerElement.id = Utils.generateElementId('corpus-follower-list-');
+ }
+ let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`);
+ this.listContainerElement.innerHTML = `
+
+ search
+
+
+
+
+
+
+ |
+ Username |
+ User details |
+ Permissions |
+ |
+
+
+
+
+
+ `.trim();
+ }
+
+ mapResourceToValue(corpusFollowerAssociation) {
+ let user = corpusFollowerAssociation.following_user;
+ return {
+ 'id': corpusFollowerAssociation.id,
+ 'avatar': user.avatar ? `/users/${user.id}/avatar` : '/static/images/user_avatar.png',
+ 'username': user.username,
+ 'full-name': user.full_name ? user.full_name : '',
+ 'about-me': user.about_me ? user.about_me : '',
+ 'permissions-can-VIEW': corpusFollowerAssociation.permissions.includes('VIEW'),
+ 'permissions-can-CONTRIBUTE': corpusFollowerAssociation.permissions.includes('CONTRIBUTE'),
+ 'permissions-can-ADMINISTRATE': corpusFollowerAssociation.permissions.includes('ADMINISTRATE')
+ };
+ }
+
+ sort() {
+ this.listjs.sort('username', {order: 'desc'});
+ }
+
+ onChange(event) {
+ if (event.target.tagName !== 'INPUT') {return;}
+ let listItemElement = event.target.closest('.list-item[data-id]');
+ if (listItemElement === null) {return;}
+ let itemId = listItemElement.dataset.id;
+ let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
+ if (listActionElement === null) {return;}
+ let listAction = listActionElement.dataset.listAction;
+ switch (listAction) {
+ case 'toggle-permission-view': {
+ if (event.target.checked) {
+ Utils.addCorpusFollowerPermissionRequest(this.userId, this.corpusId, itemId, 'VIEW');
+ } else {
+ Utils.removeCorpusFollowerPermissionRequest(this.userId, this.corpusId, itemId, 'VIEW');
+ }
+ break;
+ }
+ case 'toggle-permission-contribute': {
+ if (event.target.checked) {
+ Utils.addCorpusFollowerPermissionRequest(this.userId, this.corpusId, itemId, 'CONTRIBUTE');
+ } else {
+ Utils.removeCorpusFollowerPermissionRequest(this.userId, this.corpusId, itemId, 'CONTRIBUTE');
+ }
+ break;
+ }
+ case 'toggle-permission-administrate': {
+ if (event.target.checked) {
+ Utils.addCorpusFollowerPermissionRequest(this.userId, this.corpusId, itemId, 'ADMINISTRATE');
+ } else {
+ Utils.removeCorpusFollowerPermissionRequest(this.userId, this.corpusId, itemId, 'ADMINISTRATE');
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ onClick(event) {
+
+ }
+
+ onPatch(patch) {}
+}
diff --git a/app/static/js/ResourceLists/ResourceList.js b/app/static/js/ResourceLists/ResourceList.js
index 7fe7dec6..b7445553 100644
--- a/app/static/js/ResourceLists/ResourceList.js
+++ b/app/static/js/ResourceLists/ResourceList.js
@@ -14,6 +14,7 @@ class ResourceList {
TesseractOCRPipelineModelList.autoInit();
UserList.autoInit();
AdminUserList.autoInit();
+ CorpusFollowerList.autoInit();
}
static defaultOptions = {
diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js
index e2ac84ab..af72bbe0 100644
--- a/app/static/js/Utils.js
+++ b/app/static/js/Utils.js
@@ -69,9 +69,9 @@ class Utils {
return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
}
- static addCorpusFollowerPermissionRequest(corpusId, followerId, permission) {
+ static addCorpusFollowerPermissionRequest(userId, corpusId, corpusFollowerAssociationId, permission) {
return new Promise((resolve, reject) => {
- fetch(`/corpora/${corpusId}/followers/${followerId}/add_permission?permission=${permission}`, {method: 'POST', headers: {Accept: 'application/json'}})
+ fetch(`/corpora/${corpusId}/followers/${corpusFollowerAssociationId}/permissions/${permission}/add`, {method: 'POST', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 400) {app.flash('Bad Request', 'error'); reject(response);}
@@ -88,9 +88,9 @@ class Utils {
});
}
- static removeCorpusFollowerPermissionRequest(corpusId, followerId, permission) {
+ static removeCorpusFollowerPermissionRequest(userId, corpusId, corpusFollowerAssociationId, permission) {
return new Promise((resolve, reject) => {
- fetch(`/corpora/${corpusId}/followers/${followerId}/remove_permission?permission=${permission}`, {method: 'POST', headers: {Accept: 'application/json'}})
+ fetch(`/corpora/${corpusId}/followers/${corpusFollowerAssociationId}/permissions/${permission}/remove`, {method: 'POST', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 400) {app.flash('Bad Request', 'error'); reject(response);}
@@ -104,7 +104,7 @@ class Utils {
reject(response);
}
);
- });
+ });
}
static enableCorpusIsPublicRequest(userId, corpusId) {
@@ -145,7 +145,7 @@ class Utils {
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
confirmElement.addEventListener('click', (event) => {
let corpusTitle = corpus?.title;
- fetch(`/corpora/${corpusId}/enable_is_public`, {method: 'POST', headers: {Accept: 'application/json'}})
+ fetch(`/corpora/${corpusId}/is_public/enable`, {method: 'POST', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
@@ -173,7 +173,7 @@ class Utils {
}
let corpusTitle = corpus?.title;
- fetch(`/corpora/${corpusId}/disable_is_public`, {method: 'POST', headers: {Accept: 'application/json'}})
+ fetch(`/corpora/${corpusId}/is_public/disable`, {method: 'POST', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
diff --git a/app/static/js_new/App.js b/app/static/js_new/App.js
new file mode 100644
index 00000000..8356642d
--- /dev/null
+++ b/app/static/js_new/App.js
@@ -0,0 +1,10 @@
+import DataStore from './DataStore';
+import EventBroker from './EventBroker';
+
+
+const dataStore = new DataStore();
+const eventBroker = new EventBroker();
+const socket = io({transports: ['websocket'], upgrade: false});
+
+
+export {eventBroker, socket};
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index 53a0469c..2a3626ab 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -30,6 +30,7 @@
'js/ResourceLists/TesseractOCRPipelineModelList.js',
'js/ResourceLists/UserList.js',
'js/ResourceLists/AdminUserList.js',
+ 'js/ResourceLists/CorpusFollowerList.js',
'js/XMLtoObject.js'
%}
diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2
index 34228347..30e40e89 100644
--- a/app/templates/corpora/corpus.html.j2
+++ b/app/templates/corpora/corpus.html.j2
@@ -6,8 +6,8 @@
{% block page_content %}
-
-
+
+
- {#
+
@@ -103,10 +103,10 @@
-
#}
+
{% endblock page_content %}