diff --git a/.gitignore b/.gitignore index 59ada396..b7a84431 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ logs/ !logs/dummy *.env +*.pjentsch-testing + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 55de99f3..be3e7814 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -3,6 +3,7 @@ from flask import ( abort, current_app, flash, + make_response, Markup, redirect, render_template, @@ -19,7 +20,7 @@ from app.models import ( Corpus, CorpusFile, CorpusFollowerAssociation, - CorpusFollowPermission, + CorpusFollowerPermission, CorpusStatus, User ) @@ -31,7 +32,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) @@ -42,7 +43,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) @@ -69,49 +70,57 @@ def follow_corpus(corpus_id, token): return redirect(url_for('.corpus', corpus_id=corpus_id)) -@bp.route('//unfollow', methods=['GET', 'POST']) +@bp.route('//followers//unfollow', methods=['POST']) @login_required -def unfollow_corpus(corpus_id): +def unfollow_corpus(corpus_id, follower_id): corpus = Corpus.query.get_or_404(corpus_id) - user_hashid = request.args.get('user_id') - if user_hashid is None: - user = current_user - elif current_user.is_administrator(): - user_id = hashids.decode(user_hashid) - user = User.query.get_or_404(user_id) - else: + follower = User.query.get_or_404(follower_id) + if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()): abort(403) - if user.is_following_corpus(corpus): - user.unfollow_corpus(corpus) + if not follower.is_following_corpus(corpus): + abort(409) # 'User is not following the corpus' + follower.unfollow_corpus(corpus) + db.session.commit() + flash(f'{follower.username} is not following {corpus.title} anymore', category='corpus') + return '', 204 + + +@bp.route('//unfollow', methods=['POST']) +@login_required +def current_user_unfollow_corpus(corpus_id): + corpus = Corpus.query.get_or_404(corpus_id) + if not current_user.is_following_corpus(corpus): + abort(409) # 'You are not following the corpus' + current_user.unfollow_corpus(corpus) db.session.commit() flash(f'You are not following {corpus.title} anymore', category='corpus') 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 - if not (corpus.user == current_user or current_user.is_administrator()): +@bp.route('//followers//permissions//add', methods=['POST']) +def add_permission(corpus_id, follower_id, permission_name): + try: + permission = CorpusFollowerPermission[permission_name] + except KeyError: + abort(409) # f'Permission "{permission_name}" does not exist' + corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404() + if not (corpus_follower_association.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) + corpus_follower_association.add_permission(permission) 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 - if not (corpus.user == current_user or current_user.is_administrator()): +@bp.route('//followers//permissions//remove', methods=['POST']) +def remove_permission(corpus_id, follower_id, permission_name): + try: + permission = CorpusFollowerPermission[permission_name] + except KeyError: + return make_response(f'Permission "{permission_name}" does not exist', 409) + corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404() + if not (corpus_follower_association.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) + corpus_follower_association.remove_permission(permission) db.session.commit() return '', 204 diff --git a/app/models.py b/app/models.py index 4c7a1362..4e40793b 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,32 +291,45 @@ 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) # Foreign keys - following_user_id = db.Column(db.Integer, db.ForeignKey('users.id')) - followed_corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id')) + corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id')) + follower_id = db.Column(db.Integer, db.ForeignKey('users.id')) # Fields permissions = db.Column(db.Integer, default=0, nullable=False) # Relationships - followed_corpus = db.relationship('Corpus', back_populates='following_user_associations') - following_user = db.relationship('User', back_populates='followed_corpus_associations') + corpus = db.relationship('Corpus', back_populates='corpus_follower_associations') + follower = db.relationship('User', back_populates='corpus_follower_associations') def __repr__(self): - return f'' + return f'' - def has_permission(self, permission): - return self.permissions & permission == permission + def has_permission(self, permission: CorpusFollowerPermission): + return self.permissions & permission.value == permission.value - def add_permission(self, permission): + def add_permission(self, permission: CorpusFollowerPermission): if not self.has_permission(permission): - self.permissions += permission + self.permissions += permission.value - def remove_permission(self, permission): + def remove_permission(self, permission: CorpusFollowerPermission): if self.has_permission(permission): - self.permissions -= permission + self.permissions -= permission.value + + 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) + ], + 'corpus': self.corpus.to_json_serializeable(), + 'follower': self.follower.to_json_serializeable() + } + return json_serializeable + class User(HashidMixin, UserMixin, db.Model): __tablename__ = 'users' @@ -351,14 +368,15 @@ class User(HashidMixin, UserMixin, db.Model): cascade='all, delete-orphan', lazy='dynamic' ) - followed_corpus_associations = db.relationship( + corpus_follower_associations = db.relationship( 'CorpusFollowerAssociation', - back_populates='following_user' + back_populates='follower', + cascade='all, delete-orphan' ) followed_corpora = association_proxy( - 'followed_corpus_associations', - 'followed_corpus', - creator=lambda c: CorpusFollowerAssociation(followed_corpus=c) + 'corpus_follower_associations', + 'corpus', + creator=lambda c: CorpusFollowerAssociation(corpus=c) ) jobs = db.relationship( 'Job', @@ -634,6 +652,10 @@ class User(HashidMixin, UserMixin, db.Model): json_serializeable['role'] = \ self.role.to_json_serializeable(backrefs=True) if relationships: + json_serializeable['corpus_follower_associations'] = { + x.hashid: x.to_json_serializeable(relationships=True) + for x in self.corpus_follower_associations + } json_serializeable['corpora'] = { x.hashid: x.to_json_serializeable(relationships=True) for x in self.corpora @@ -650,10 +672,6 @@ 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): @@ -1297,14 +1315,15 @@ class Corpus(HashidMixin, db.Model): lazy='dynamic', cascade='all, delete-orphan' ) - following_user_associations = db.relationship( + corpus_follower_associations = db.relationship( 'CorpusFollowerAssociation', - back_populates='followed_corpus' + back_populates='corpus', + cascade='all, delete-orphan' ) - following_users = association_proxy( - 'following_user_associations', - 'following_user', - creator=lambda u: CorpusFollowerAssociation(following_user=u) + followers = association_proxy( + 'corpus_follower_associations', + 'follower', + creator=lambda u: CorpusFollowerAssociation(followers=u) ) user = db.relationship('User', back_populates='corpora') # "static" attributes @@ -1403,8 +1422,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['corpus_follower_associations'] = { + x.hashid: x.to_json_serializeable(relationships=True) + for x in self.corpus_follower_associations + } json_serializeable['files'] = { x.hashid: x.to_json_serializeable(relationships=True) for x in self.files @@ -1426,12 +1450,27 @@ class Corpus(HashidMixin, db.Model): @db.event.listens_for(TesseractOCRPipelineModel, 'after_delete') def ressource_after_delete(mapper, connection, ressource): jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}] - room = f'users.{ressource.user_hashid}' - socketio.emit('users.patch', jsonpatch, room=room) room = f'/users/{ressource.user_hashid}' socketio.emit('PATCH', jsonpatch, room=room) +@db.event.listens_for(CorpusFollowerAssociation, 'after_delete') +def corpus_follower_association_after_delete_handler(mapper, connection, ressource): + corpus_owner_hashid = ressource.corpus.user.hashid + corpus_hashid = hashids.encode(ressource.corpus_id) + follower_hashid = hashids.encode(ressource.follower_id) + # Send a PATCH to the corpus owner + jsonpatch_path = f'/users/{corpus_owner_hashid}/corpora/{corpus_hashid}/corpus_follower_associations/{ressource.hashid}' + jsonpatch = [{'op': 'remove', 'path': jsonpatch_path}] + room = f'/users/{corpus_owner_hashid}' + socketio.emit('PATCH', jsonpatch, room=room) + # Send a PATCH to the follower + jsonpatch_path = f'/users/{follower_hashid}/corpus_follower_associations/{ressource.hashid}' + jsonpatch = [{'op': 'remove', 'path': jsonpatch_path}] + room = f'/users/{follower_hashid}' + socketio.emit('PATCH', jsonpatch, room=room) + + @db.event.listens_for(Corpus, 'after_insert') @db.event.listens_for(CorpusFile, 'after_insert') @db.event.listens_for(Job, 'after_insert') @@ -1450,6 +1489,24 @@ def ressource_after_insert_handler(mapper, connection, ressource): socketio.emit('PATCH', jsonpatch, room=room) +# @db.event.listens_for(CorpusFollowerAssociation, 'after_insert') +# def corpus_follower_association_after_insert_handler(mapper, connection, ressource): +# corpus_owner_hashid = ressource.corpus.user.hashid +# corpus_hashid = hashids.encode(ressource.corpus_id) +# follower_hashid = hashids.encode(ressource.follower_id) +# value = ressource.to_json_serializeable() +# # Send a PATCH to the corpus owner +# jsonpatch_path = f'/users/{corpus_owner_hashid}/corpora/{corpus_hashid}/corpus_follower_associations/{ressource.hashid}' +# jsonpatch = [{'op': 'add', 'path': jsonpatch_path, 'value': value}] +# room = f'/users/{corpus_owner_hashid}' +# socketio.emit('PATCH', jsonpatch, room=room) +# # Send a PATCH to the follower +# jsonpatch_path = f'/users/{follower_hashid}/corpus_follower_associations/{ressource.hashid}' +# jsonpatch = [{'op': 'add', 'path': jsonpatch_path, 'value': value}] +# room = f'/users/{follower_hashid}' +# socketio.emit('PATCH', jsonpatch, room=room) + + @db.event.listens_for(Corpus, 'after_update') @db.event.listens_for(CorpusFile, 'after_update') @db.event.listens_for(Job, 'after_update') diff --git a/app/static/js/ResourceLists/CorpusFollowerList.js b/app/static/js/ResourceLists/CorpusFollowerList.js new file mode 100644 index 00000000..616f3793 --- /dev/null +++ b/app/static/js/ResourceLists/CorpusFollowerList.js @@ -0,0 +1,204 @@ +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].corpus_follower_associations)); + this.isInitialized = true; + }); + } + + get item() { + return (values) => { + return ` + + user-image + +
+ + + + +
+ + + +
+ + + + + + delete + send + + + `.trim(); + } + } + + get valueNames() { + return [ + {data: ['id']}, + {data: ['follower-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) { + return { + 'id': corpusFollowerAssociation.id, + 'follower-id': corpusFollowerAssociation.follower.id, + 'avatar': corpusFollowerAssociation.follower.avatar ? `/users/${corpusFollowerAssociation.follower.id}/avatar` : '/static/images/user_avatar.png', + 'username': corpusFollowerAssociation.follower.username, + 'full-name': corpusFollowerAssociation.follower.full_name ? corpusFollowerAssociation.follower.full_name : '', + 'about-me': corpusFollowerAssociation.follower.about_me ? corpusFollowerAssociation.follower.about_me : '', + 'permission-can-VIEW': corpusFollowerAssociation.permissions.includes('VIEW'), + 'permission-can-CONTRIBUTE': corpusFollowerAssociation.permissions.includes('CONTRIBUTE'), + 'permission-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': { + let followerId = listItemElement.dataset.followerId; + let permission = listActionElement.dataset.permission; + if (event.target.checked) { + Utils.addCorpusFollowerPermissionRequest(this.corpusId, followerId, permission) + .catch((error) => {event.target.checked = !event.target.checked;}); + } else { + Utils.removeCorpusFollowerPermissionRequest(this.corpusId, followerId, permission) + .catch((error) => {event.target.checked = !event.target.checked;}); + } + break; + } + default: { + break; + } + } + } + + onClick(event) { + if (event.target.closest('.disable-on-click') !== null) {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]'); + let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; + switch (listAction) { + case 'unfollow-request': { + let followerId = listItemElement.dataset.followerId; + Utils.unfollowCorpusRequest(this.corpusId, followerId); + break; + } + case 'view': { + let followerId = listItemElement.dataset.followerId; + window.location.href = `/users/${followerId}`; + break; + } + default: { + break; + } + } + } + + onPatch(patch) { + let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)`); + let filteredPatch = patch.filter(operation => re.test(operation.path)); + for (let operation of filteredPatch) { + switch(operation.op) { + case 'add': { + // let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)$`); + // if (re.test(operation.path)) {this.add(operation.value);} + break; + } + case 'remove': { + let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)$`); + if (re.test(operation.path)) { + let [match, jobId] = operation.path.match(re); + this.remove(jobId); + } + break; + } + case 'replace': { + // let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)/(service|status|description|title)$`); + // if (re.test(operation.path)) { + // let [match, jobId, valueName] = operation.path.match(re); + // this.replace(jobId, valueName, operation.value); + // } + break; + } + default: { + break; + } + } + } + } +} 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/ResourceLists/SpacyNLPPipelineModelList.js b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js index b90fb06b..f7901528 100644 --- a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js +++ b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js @@ -30,14 +30,12 @@ class SpaCyNLPPipelineModelList extends ResourceList {
    ()
    -
    - + -
    + delete @@ -80,6 +78,7 @@ class SpaCyNLPPipelineModelList extends ResourceList { Title and Description Publisher + Availability @@ -111,6 +110,7 @@ class SpaCyNLPPipelineModelList extends ResourceList { } 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; @@ -118,7 +118,7 @@ class SpaCyNLPPipelineModelList extends ResourceList { if (listActionElement === null) {return;} let listAction = listActionElement.dataset.listAction; switch (listAction) { - case 'share-request': { + case 'toggle-is-public': { Utils.spaCyNLPPipelineModelToggleIsPublicRequest(this.userId, itemId); break; } @@ -129,15 +129,11 @@ class SpaCyNLPPipelineModelList extends ResourceList { } onClick(event) { + if (event.target.closest('.disable-on-click') !== null) {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]'); - // ignore switch clicks, handle them by the onChange method instead - if (listActionElement.classList.contains('switch')) { - event.preventDefault(); - this.onChange(event); - } let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete-request': { diff --git a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js index 4ad3f5b1..c5e08b1d 100644 --- a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js +++ b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js @@ -38,14 +38,12 @@ class TesseractOCRPipelineModelList extends ResourceList {
    ()
    -
    - + -
    + delete @@ -89,6 +87,7 @@ class TesseractOCRPipelineModelList extends ResourceList { Title and Description Publisher + Availability @@ -120,6 +119,7 @@ class TesseractOCRPipelineModelList extends ResourceList { } 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; @@ -127,7 +127,7 @@ class TesseractOCRPipelineModelList extends ResourceList { if (listActionElement === null) {return;} let listAction = listActionElement.dataset.listAction; switch (listAction) { - case 'share-request': { + case 'toggle-is-public': { Utils.tesseractOCRPipelineModelToggleIsPublicRequest(this.userId, itemId); break; } @@ -138,6 +138,7 @@ class TesseractOCRPipelineModelList extends ResourceList { } onClick(event) { + if (event.target.closest('.disable-on-click') !== null) {return;} let listItemElement = event.target.closest('.list-item[data-id]'); if (listItemElement === null) {return;} let itemId = listItemElement.dataset.id; diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js index b3da4ca1..80340d6c 100644 --- a/app/static/js/Utils.js +++ b/app/static/js/Utils.js @@ -71,14 +71,17 @@ class Utils { static addCorpusFollowerPermissionRequest(corpusId, followerId, 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/${followerId}/permissions/${permission}/add`, {method: 'POST', headers: {Accept: 'application/json'}}) .then( (response) => { - if (response.status === 400) {app.flash('Bad Request', 'error'); reject(response);} - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - app.flash(`Permission added`, 'corpus'); - resolve(response); + if (response.ok) { + app.flash(`Permission added`, 'corpus'); + resolve(response); + return; + } else { + app.flash(`${response.statusText}`, 'error'); + reject(response); + } }, (response) => { app.flash('Something went wrong', 'error'); @@ -90,21 +93,23 @@ class Utils { static removeCorpusFollowerPermissionRequest(corpusId, followerId, 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/${followerId}/permissions/${permission}/remove`, {method: 'POST', headers: {Accept: 'application/json'}}) .then( (response) => { - if (response.status === 400) {app.flash('Bad Request', 'error'); reject(response);} - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - app.flash(`Permission removed`, 'corpus'); - resolve(response); + if (response.ok) { + app.flash(`Permission removed`, 'corpus'); + resolve(response); + } else { + app.flash(`${response.statusText}`, 'error'); + reject(response); + } }, (response) => { app.flash('Something went wrong', 'error'); reject(response); } ); - }); + }); } static enableCorpusIsPublicRequest(userId, corpusId) { @@ -145,7 +150,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 +178,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);} @@ -215,6 +220,27 @@ class Utils { }); } + static unfollowCorpusRequest(corpusId, followerId) { + return new Promise((resolve, reject) => { + fetch(`/corpora/${corpusId}/followers/${followerId}/unfollow`, {method: 'POST', headers: {Accept: 'application/json'}}) + .then( + (response) => { + if (response.ok) { + app.flash(`User unfollowed from Corpus`, 'corpus'); + resolve(response); + } else { + app.flash(`${response.statusText}`, 'error'); + reject(response); + } + }, + (response) => { + app.flash('Something went wrong', 'error'); + reject(response); + } + ); + }); + } + static deleteCorpusRequest(userId, corpusId) { return new Promise((resolve, reject) => { let corpus; 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 c84d6c0e..0e982d70 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -6,8 +6,8 @@ {% block page_content %}
    -
    -
    +
    +

    @@ -89,7 +89,7 @@
    @@ -130,7 +130,7 @@
    Corpus followers -
    +
    diff --git a/migrations/versions/03c7211f089d_.py b/migrations/versions/03c7211f089d_.py new file mode 100644 index 00000000..2f73e3aa --- /dev/null +++ b/migrations/versions/03c7211f089d_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 03c7211f089d +Revises: 5fe6a6c7870c +Create Date: 2023-02-21 09:57:22.005883 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '03c7211f089d' +down_revision = '5fe6a6c7870c' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + 'corpus_follower_associations', + 'followed_corpus_id', + new_column_name='corpus_id' + ) + op.alter_column( + 'corpus_follower_associations', + 'following_user_id', + new_column_name='follower_id' + ) + + +def downgrade(): + op.alter_column( + 'corpus_follower_associations', + 'corpus_id', + new_column_name='followed_corpus_id' + ) + op.alter_column( + 'corpus_follower_associations', + 'follower_id', + new_column_name='following_user_id' + )