mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-10-30 18:22:45 +00:00 
			
		
		
		
	Change the user session SocketIO Logic
This commit is contained in:
		| @@ -2,4 +2,4 @@ from flask import Blueprint | |||||||
|  |  | ||||||
|  |  | ||||||
| bp = Blueprint('main', __name__) | bp = Blueprint('main', __name__) | ||||||
| from . import events, routes | from . import routes | ||||||
|   | |||||||
| @@ -1,41 +0,0 @@ | |||||||
| from app import hashids, socketio |  | ||||||
| from app.models import User |  | ||||||
| from flask_login import current_user |  | ||||||
| from flask_socketio import join_room |  | ||||||
| from app.decorators import socketio_login_required |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @socketio.on('users.user.get') |  | ||||||
| @socketio_login_required |  | ||||||
| def users_user_get(user_hashid): |  | ||||||
|     user_id = hashids.decode(user_hashid) |  | ||||||
|     user = User.query.get(user_id) |  | ||||||
|     if user is None: |  | ||||||
|         return {'code': 404, 'msg': 'Not found'} |  | ||||||
|     if not (user == current_user or current_user.is_administrator): |  | ||||||
|         return {'code': 403, 'msg': 'Forbidden'} |  | ||||||
|     # corpora = [x.to_dict() for x in user.corpora] |  | ||||||
|     # jobs = [x.to_dict() for x in user.jobs] |  | ||||||
|     # transkribus_htr_models = TranskribusHTRModel.query.filter( |  | ||||||
|     #     (TranskribusHTRModel.shared == True) | (TranskribusHTRModel.user == user) |  | ||||||
|     # ).all() |  | ||||||
|     # tesseract_ocr_models = TesseractOCRModel.query.filter( |  | ||||||
|     #     (TesseractOCRModel.shared == True) | (TesseractOCRModel.user == user) |  | ||||||
|     # ).all() |  | ||||||
|     # response = { |  | ||||||
|     #     'code': 200, |  | ||||||
|     #     'msg': 'OK', |  | ||||||
|     #     'payload': { |  | ||||||
|     #         'user': user.to_dict(), |  | ||||||
|     #         'corpora': corpora, |  | ||||||
|     #         'jobs': jobs, |  | ||||||
|     #         'transkribus_htr_models': transkribus_htr_models, |  | ||||||
|     #         'tesseract_ocr_models': tesseract_ocr_models |  | ||||||
|     #     } |  | ||||||
|     # } |  | ||||||
|     join_room(f'users.{user.hashid}') |  | ||||||
|     return { |  | ||||||
|         'code': 200, |  | ||||||
|         'msg': 'OK', |  | ||||||
|         'payload': user.to_dict(backrefs=True, relationships=True) |  | ||||||
|     } |  | ||||||
| @@ -1033,6 +1033,8 @@ def ressource_after_delete(mapper, connection, ressource): | |||||||
|     jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}] |     jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}] | ||||||
|     room = f'users.{ressource.user_hashid}' |     room = f'users.{ressource.user_hashid}' | ||||||
|     socketio.emit('users.patch', jsonpatch, room=room) |     socketio.emit('users.patch', jsonpatch, room=room) | ||||||
|  |     room = f'/users/{ressource.user_hashid}' | ||||||
|  |     socketio.emit('PATCH', jsonpatch, room=room) | ||||||
|  |  | ||||||
|  |  | ||||||
| @db.event.listens_for(Corpus, 'after_insert') | @db.event.listens_for(Corpus, 'after_insert') | ||||||
| @@ -1047,8 +1049,8 @@ def ressource_after_insert_handler(mapper, connection, ressource): | |||||||
|     jsonpatch = [ |     jsonpatch = [ | ||||||
|         {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value} |         {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value} | ||||||
|     ] |     ] | ||||||
|     room = f'users.{ressource.user_hashid}' |     room = f'/users/{ressource.user_hashid}' | ||||||
|     socketio.emit('users.patch', jsonpatch, room=room) |     socketio.emit('PATCH', jsonpatch, room=room) | ||||||
|  |  | ||||||
|  |  | ||||||
| @db.event.listens_for(Corpus, 'after_update') | @db.event.listens_for(Corpus, 'after_update') | ||||||
| @@ -1077,8 +1079,8 @@ def ressource_after_update_handler(mapper, connection, ressource): | |||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|     if jsonpatch: |     if jsonpatch: | ||||||
|         room = f'users.{ressource.user_hashid}' |         room = f'/users/{ressource.user_hashid}' | ||||||
|         socketio.emit('users.patch', jsonpatch, room=room) |         socketio.emit('PATCH', jsonpatch, room=room) | ||||||
|  |  | ||||||
|  |  | ||||||
| @db.event.listens_for(Job, 'after_update') | @db.event.listens_for(Job, 'after_update') | ||||||
|   | |||||||
| @@ -1,21 +1,30 @@ | |||||||
| class App { | class App { | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.data = {users: {}}; |     this.data = {users: {}}; | ||||||
|     this.eventListeners = {'users.patch': []}; |  | ||||||
|     this.promises = {users: {}}; |     this.promises = {users: {}}; | ||||||
|     this.socket = io({transports: ['websocket'], upgrade: false}); |     this.socket = io({transports: ['websocket'], upgrade: false}); | ||||||
|     this.socket.on('users.patch', patch => this.usersPatchHandler(patch)); |     this.socket.on('PATCH', (patch) => {this.data = jsonpatch.applyPatch(this.data, patch).newDocument;}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get users() { |   get users() { | ||||||
|     return this.data.users; |     return this.data.users; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   addEventListener(type, listener) { |   subscribeUser(userId) { | ||||||
|     if (!(type in this.eventListeners)) { |     if (userId in this.promises.users) { | ||||||
|       throw `Unknown event type: ${type}`; |       return this.promises.users[userId]; | ||||||
|     } |     } | ||||||
|     this.eventListeners[type].push(listener); |     this.promises.users[userId] = new Promise((resolve, reject) => { | ||||||
|  |       this.socket.emit('SUBSCRIBE /users/<user_id>', userId, response => { | ||||||
|  |         if (response.code === 200) { | ||||||
|  |           this.data.users[userId] = response.payload; | ||||||
|  |           resolve(this.data.users[userId]); | ||||||
|  |         } else { | ||||||
|  |           reject(response); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     return this.promises.users[userId]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   flash(message, category) { |   flash(message, category) { | ||||||
| @@ -50,29 +59,4 @@ class App { | |||||||
|     toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]'); |     toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]'); | ||||||
|     toastCloseActionElement.addEventListener('click', () => {toast.dismiss();}); |     toastCloseActionElement.addEventListener('click', () => {toast.dismiss();}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getUserById(userId) { |  | ||||||
|     if (userId in this.promises.users) { |  | ||||||
|       return this.promises.users[userId]; |  | ||||||
|     } |  | ||||||
|     this.promises.users[userId] = new Promise((resolve, reject) => { |  | ||||||
|       this.socket.emit('users.user.get', userId, response => { |  | ||||||
|         if (response.code === 200) { |  | ||||||
|           this.data.users[userId] = response.payload; |  | ||||||
|           resolve(this.data.users[userId]); |  | ||||||
|         } else { |  | ||||||
|           reject(response); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     return this.promises.users[userId]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |  | ||||||
|     let listener; |  | ||||||
|  |  | ||||||
|     this.data = jsonpatch.applyPatch(this.data, patch).newDocument; |  | ||||||
|     //this.data = jsonpatch.apply_patch(this.data, patch); |  | ||||||
|     for (listener of this.eventListeners['users.patch']) {listener(patch);} |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +1,17 @@ | |||||||
| class JobStatusNotifier { | class JobStatusNotifier { | ||||||
|   constructor(userId) { |   constructor(userId) { | ||||||
|     this.userId = userId; |     this.userId = userId; | ||||||
|  |     app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let jobId; |     let jobId; | ||||||
|     let match; |     let match; | ||||||
|     let operation; |     let operation; | ||||||
|     let re; |     let re; | ||||||
|  |  | ||||||
|     re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`) |     re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`); | ||||||
|     filteredPatch = patch |     filteredPatch = patch | ||||||
|       .filter(operation => operation.op === 'replace') |       .filter(operation => operation.op === 'replace') | ||||||
|       .filter(operation => re.test(operation.path)); |       .filter(operation => re.test(operation.path)); | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ class CorpusDisplay extends RessourceDisplay { | |||||||
|     this.setNumTokens(corpus.num_tokens); |     this.setNumTokens(corpus.num_tokens); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let operation; |     let operation; | ||||||
|     let re; |     let re; | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ class JobDisplay extends RessourceDisplay { | |||||||
|     this.setTitle(job.title); |     this.setTitle(job.title); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let operation; |     let operation; | ||||||
|     let re; |     let re; | ||||||
|   | |||||||
| @@ -2,13 +2,13 @@ class RessourceDisplay { | |||||||
|   constructor(displayElement) { |   constructor(displayElement) { | ||||||
|     this.displayElement = displayElement; |     this.displayElement = displayElement; | ||||||
|     this.userId = this.displayElement.dataset.userId; |     this.userId = this.displayElement.dataset.userId; | ||||||
|     app.addEventListener('users.patch', patch => this.usersPatchHandler(patch)); |     app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); | ||||||
|     app.getUserById(this.userId).then(user => this.init(user)); |     app.subscribeUser(this.userId).then((user) => {this.init(user);}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   init(user) {throw 'Not implemented';} |   init(user) {throw 'Not implemented';} | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) {throw 'Not implemented';} |   onPATCH(patch) {throw 'Not implemented';} | ||||||
|  |  | ||||||
|   setElement(element, value) { |   setElement(element, value) { | ||||||
|     switch (element.tagName) { |     switch (element.tagName) { | ||||||
|   | |||||||
| @@ -96,7 +96,7 @@ class CorpusFileList extends RessourceList { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let corpusFileId; |     let corpusFileId; | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let match; |     let match; | ||||||
|   | |||||||
| @@ -88,7 +88,7 @@ class CorpusList extends RessourceList { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let corpusId; |     let corpusId; | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let match; |     let match; | ||||||
|   | |||||||
| @@ -54,5 +54,5 @@ class JobInputList extends RessourceList { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) {return;} |   onPATCH(patch) {return;} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -94,7 +94,7 @@ class JobList extends RessourceList { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let jobId; |     let jobId; | ||||||
|     let match; |     let match; | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ class JobResultList extends RessourceList { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let operation; |     let operation; | ||||||
|     let re; |     let re; | ||||||
|   | |||||||
| @@ -89,7 +89,7 @@ class QueryResultList extends RessourceList { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) { |   onPATCH(patch) { | ||||||
|     let filteredPatch; |     let filteredPatch; | ||||||
|     let match; |     let match; | ||||||
|     let operation; |     let operation; | ||||||
|   | |||||||
| @@ -91,10 +91,10 @@ class RessourceList { | |||||||
|     this.userId = this.listjs.listContainer.dataset.userId; |     this.userId = this.listjs.listContainer.dataset.userId; | ||||||
|     this.listjs.list.addEventListener('click', event => this.onclick(event)); |     this.listjs.list.addEventListener('click', event => this.onclick(event)); | ||||||
|     if (this.userId) { |     if (this.userId) { | ||||||
|       app.addEventListener('users.patch', patch => this.usersPatchHandler(patch)); |       app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); | ||||||
|       app.getUserById(this.userId).then( |       app.subscribeUser(this.userId).then( | ||||||
|         user => this.init(user), |         (user) => {this.init(user);}, | ||||||
|         error => {throw JSON.stringify(error);} |         (error) => {throw JSON.stringify(error);} | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -117,7 +117,7 @@ class RessourceList { | |||||||
|  |  | ||||||
|   onclick(event) {throw 'Not implemented';} |   onclick(event) {throw 'Not implemented';} | ||||||
|  |  | ||||||
|   usersPatchHandler(patch) {throw 'Not implemented';} |   onPATCH(patch) {throw 'Not implemented';} | ||||||
|  |  | ||||||
|   add(ressources) { |   add(ressources) { | ||||||
|     let values = Array.isArray(ressources) ? ressources : [ressources]; |     let values = Array.isArray(ressources) ? ressources : [ressources]; | ||||||
|   | |||||||
| @@ -32,11 +32,10 @@ | |||||||
|   const jobStatusNotifier = new JobStatusNotifier(currentUserId); |   const jobStatusNotifier = new JobStatusNotifier(currentUserId); | ||||||
|  |  | ||||||
|   // Initialize components for current user |   // Initialize components for current user | ||||||
|   app.addEventListener('users.patch', patch => jobStatusNotifier.usersPatchHandler(patch)); |   app.subscribeUser(currentUserId) | ||||||
|   app.getUserById(currentUserId) |  | ||||||
|     .then( |     .then( | ||||||
|       user => {return;}, |       (user) => {return;}, | ||||||
|       error => {throw JSON.stringify(error);} |       (error) => {throw JSON.stringify(error);} | ||||||
|     ); |     ); | ||||||
|   {%- endif %} |   {%- endif %} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								app/templates/users/users.html.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								app/templates/users/users.html.j2
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | {% extends "base.html.j2" %} | ||||||
|  |  | ||||||
|  | {% block page_content %} | ||||||
|  | <div class="container"> | ||||||
|  |   <div class="row"> | ||||||
|  |     <div class="col s12"> | ||||||
|  |       <h1 id="title">{{ title }}</h1> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="col s12"> | ||||||
|  |       <div class="card"> | ||||||
|  |         <div class="card-content"> | ||||||
|  |           <table class="" id="users"></table> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | {% endblock page_content %} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | {% block scripts %} | ||||||
|  | {{ super() }} | ||||||
|  | <script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script> | ||||||
|  | <script> | ||||||
|  |   const updateUrl = (prev, query) => { | ||||||
|  |     return prev + (prev.indexOf('?') >= 0 ? '&' : '?') + new URLSearchParams(query).toString(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   new gridjs.Grid({ | ||||||
|  |     columns: [ | ||||||
|  |       { id: 'username', name: 'Username' }, | ||||||
|  |       { id: 'email', name: 'Email' }, | ||||||
|  |     ], | ||||||
|  |     server: { | ||||||
|  |       url: '/users/api_users', | ||||||
|  |       then: results => results.data, | ||||||
|  |       total: results => results.total, | ||||||
|  |     }, | ||||||
|  |     search: { | ||||||
|  |       enabled: true, | ||||||
|  |       server: { | ||||||
|  |         url: (prev, search) => { | ||||||
|  |           return updateUrl(prev, {search}); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     sort: { | ||||||
|  |       enabled: true, | ||||||
|  |       multiColumn: true, | ||||||
|  |       server: { | ||||||
|  |         url: (prev, columns) => { | ||||||
|  |           const columnIds = ['username', 'email']; | ||||||
|  |           const sort = columns.map(col => (col.direction === 1 ? '+' : '-') + columnIds[col.index]); | ||||||
|  |           return updateUrl(prev, {sort}); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     pagination: { | ||||||
|  |       enabled: true, | ||||||
|  |       server: { | ||||||
|  |         url: (prev, page, limit) => { | ||||||
|  |           return updateUrl(prev, {offset: page * limit, limit: limit}); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     } | ||||||
|  |   }).render(document.getElementById('users')); | ||||||
|  | </script> | ||||||
|  | {% endblock scripts %} | ||||||
							
								
								
									
										5
									
								
								app/users/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/users/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | from flask import Blueprint | ||||||
|  |  | ||||||
|  |  | ||||||
|  | bp = Blueprint('users', __name__) | ||||||
|  | from . import events, routes  # noqa | ||||||
							
								
								
									
										32
									
								
								app/users/events.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/users/events.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | from app import hashids, socketio | ||||||
|  | from app.decorators import socketio_login_required | ||||||
|  | from app.models import User | ||||||
|  | from flask_login import current_user | ||||||
|  | from flask_socketio import join_room, leave_room | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @socketio.on('SUBSCRIBE /users/<user_id>') | ||||||
|  | @socketio_login_required | ||||||
|  | def subscribe_user(user_hashid): | ||||||
|  |     user_id = hashids.decode(user_hashid) | ||||||
|  |     user = User.query.get(user_id) | ||||||
|  |     if user is None: | ||||||
|  |         return {'code': 404, 'msg': 'Not found'} | ||||||
|  |     if not (user == current_user or current_user.is_administrator): | ||||||
|  |         return {'code': 403, 'msg': 'Forbidden'} | ||||||
|  |     dict_user = user.to_dict(backrefs=True, relationships=True) | ||||||
|  |     join_room(f'/users/{user.hashid}') | ||||||
|  |     return {'code': 200, 'msg': 'OK', 'payload': dict_user} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @socketio.on('UNSUBSCRIBE /users/<user_id>') | ||||||
|  | @socketio_login_required | ||||||
|  | def subscribe_user(user_hashid): | ||||||
|  |     user_id = hashids.decode(user_hashid) | ||||||
|  |     user = User.query.get(user_id) | ||||||
|  |     if user is None: | ||||||
|  |         return {'code': 404, 'msg': 'Not found'} | ||||||
|  |     if not (user == current_user or current_user.is_administrator): | ||||||
|  |         return {'code': 403, 'msg': 'Forbidden'} | ||||||
|  |     leave_room(f'/users/{user.hashid}') | ||||||
|  |     return {'code': 200, 'msg': 'OK'} | ||||||
							
								
								
									
										50
									
								
								app/users/routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/users/routes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | from app.models import User | ||||||
|  | from flask import render_template, request, url_for | ||||||
|  | from . import bp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @bp.route('/') | ||||||
|  | def users(): | ||||||
|  |     return render_template( | ||||||
|  |         'users/users.html.j2', | ||||||
|  |         title='Users' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @bp.route('/api_users') | ||||||
|  | def api_users(): | ||||||
|  |     query = User.query | ||||||
|  |  | ||||||
|  |     # search filter | ||||||
|  |     search = request.args.get('search') | ||||||
|  |     if search: | ||||||
|  |         query = query.filter(User.username.like(f'%{search}%') | User.email.like(f'%{search}%')) | ||||||
|  |     total = query.count() | ||||||
|  |  | ||||||
|  |     # sorting | ||||||
|  |     sort = request.args.get('sort') | ||||||
|  |     if sort: | ||||||
|  |         order = [] | ||||||
|  |         for s in sort.split(','): | ||||||
|  |             direction = s[0] | ||||||
|  |             name = s[1:] | ||||||
|  |             if name not in ['username', 'email']: | ||||||
|  |                 name = 'username' | ||||||
|  |             col = getattr(User, name) | ||||||
|  |             if direction == '-': | ||||||
|  |                 col = col.desc() | ||||||
|  |             order.append(col) | ||||||
|  |         if order: | ||||||
|  |             query = query.order_by(*order) | ||||||
|  |  | ||||||
|  |     # pagination | ||||||
|  |     offset = request.args.get('offset', type=int, default=-1) | ||||||
|  |     limit = request.args.get('limit', type=int, default=-1) | ||||||
|  |     if offset != -1 and limit != -1: | ||||||
|  |         query = query.offset(offset).limit(limit) | ||||||
|  |  | ||||||
|  |     # response | ||||||
|  |     return { | ||||||
|  |         'data': [user.to_dict() for user in query], | ||||||
|  |         'total': total | ||||||
|  |     } | ||||||
		Reference in New Issue
	
	Block a user