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 ` + + user-image + +
+ + +
+ +
+ + + + 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 + + +
+ + + + + + + + + + + +
UsernameUser detailsPermissions
+
    + `.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 %}
    -
    -
    +
    +

    @@ -80,21 +80,21 @@
    - {#
    + @@ -103,10 +103,10 @@
    Corpus followers -
    +
    -
    #} +
    {% endblock page_content %}