Loading .vscode/settings.json 0 → 100644 +3 −0 Original line number Diff line number Diff line { "python.linting.enabled": true } No newline at end of file README.md +16 −5 Original line number Diff line number Diff line Loading @@ -4,17 +4,28 @@ Fablab 3d printing request application ## Minimum Viable Version - [ ] Tests - [x] User Connection - [ ] User administration - [ ] List users, - [ ] Change access level - [ ] Submit a print request - [ ] Upload a STL file - [ ] Create a new Print Request - [ ] Accept/Reject print request - [ ] See STL file in 3d - [ ] See print status - [x] Upload a STL file - [x] Backend - [x] Front - [x] Create a new Print Request - [x] Backend - [x] Front - [ ] Accept/Reject print request (front) - [x] See STL file in 3d - [x] See print status - [ ] Change Print status (see enum in api) - [ ] Slice - [ ] Upload GCODE - [ ] **V2**: Open slicer window - [ ] Message system - [ ] Post message on each request - [ ] New message indication - [ ] **V2**: Send to printer - [ ] Working queues - [ ] Printer management Loading back/myfab/__pycache__/api.cpython-38.pyc +2.11 KiB (6.91 KiB) File changed.No diff preview for this file type. View original file View changed file back/myfab/__pycache__/model.cpython-38.pyc +224 B (2.7 KiB) File changed.No diff preview for this file type. View original file View changed file back/myfab/api.py +131 −37 Original line number Diff line number Diff line Loading @@ -5,21 +5,25 @@ from flask_jwt_extended import ( current_user ) from myfab.model import * import datetime from myfab.model import Event, PrintRequest, User, init_db_connection, Queue from flask_api import status from flask import jsonify import string import random import os from flask_cors import CORS init_db_connection() log.info("Database connected") app = Flask(__name__) CORS(app) app.debug = True app.config['SECRET_KEY'] = 'super-secret' #! CHANGEME app.config['JWT_IDENTITY_CLAIM'] = "login_name" FILE_SAVE_PATH = "./files" FILE_SAVE_PATH = "../ui/static/files" jwt = JWTManager(app) Loading @@ -32,13 +36,14 @@ def identity(payload): REQUEST_STATUS_PENDING_REVIEW = 0 # initial state REQUEST_STATUS_APPROVED = 1 # approved, waiting slicing REQUEST_STATUS_AWAITING_PRINT = 2 # sliced, in print queur REQUEST_STATUS_AWAITING_PRINT = 2 # sliced, in print queue REQUEST_STATUS_PRINTING = 3 # currently printing REQUEST_STATUS_FINISHED = 4 # done printing, waiting for recuperation REQUEST_STATUS_CLOSED = 5 # archived REQUEST_STATUS_REJECTED = 10 # refused by operator REQUEST_STATUS_PRINT_FAILED = 11 # print failed more than allowed REQUEST_STATUS_STANDBY = 12 # on hold, from any state REQUEST_STATUS_CANCELED = 13 # annulée REQUEST_ACCESS_DENIED = 0 # Not allowed to see this request REQUEST_ACCESS_OBSERVER = 1 # Allowed to see this request Loading @@ -48,8 +53,7 @@ REQUEST_ACCESS_OPERATOR = 3 # Is an operator of the system # Elements that can be modified by a user (not op) REQUEST_USER_ELEMENTS = set(["title","description","project"]) ### UTILS #### #region UTILS def get_or_create_user(username: str, fullname: str = None): req = User.select().where(User.username == username) if(req.count()): Loading @@ -59,40 +63,70 @@ def get_or_create_user(username: str, fullname: str = None): raise Exception("fullname cannot be None") return User.create(username=username, fullname=fullname) def lookup_requests(author: str = None, status: int = None): def lookup_requests(author: str = None, status = None, req_id = None): where_req = [] if author: where_req.append(PrintRequest.author.username == author) if status: where_req.append(PrintRequest.status == status) req = PrintRequest.select().where(*where_req).limit(30) return req.get() if status and len(status) > 0: w = None for sts in status: if w is None: w = PrintRequest.status == sts else: w = w or PrintRequest.status == sts where_req.append(w) if req_id: where_req.append(PrintRequest.id == req_id) req = PrintRequest.select().join(User, on=(PrintRequest.author == User.username)).limit(30) # .join(Queue, on=(PrintRequest.queue == Queue.id)) if len(where_req) > 0: req = req.where(*where_req) req = req.order_by(PrintRequest.last_modification.desc()) return req def get_request_by_id(rid): req = PrintRequest.select.where(PrintRequest.id == rid) req = PrintRequest.select().where(PrintRequest.id == rid) return req.get() if req.count() else None def add_request_event(req: PrintRequest, desc: str, op: User = None): log.info("Req %s: %s"%(req.id, desc)) req.last_modification = datetime.datetime.now() req.save() Event.create(request = req, description = desc, operator = op) def extract_date_time(elem): if type(elem) == tuple: elem = elem[0] # elem = datetime.datetime.fromisoformat(repr(elem)) return f'{elem.day}/{elem.month}/{elem.year} {elem.hour}:{elem.minute}:{elem.second}' def loop_requests(reqs): rqs = [] # if type(reqs) != list: # reqs = [reqs] for req in reqs: rqs.append({ request_id: req.id, title: req.title, description: req.description, stl: req.stl, status: req.status, project: req.project, operator: req.operator.username if req.operator is not None else "", queue: req.queue.name if req.queue is not None else "", recup: req.recup_id "request_id": req.id, "title": req.title, "creation": extract_date_time(req.creation), "last_modification": extract_date_time(req.last_modification), "author": req.author.fullname, "description": req.description, "stl": req.stl, "status": req.status, "reject_message": req.reject_message, "project": req.project, "gcode": req.gcode, "operator": req.operator.username if req.operator is not None else "", "queue": req.queue.name if req.queue is not None else "", "recup": req.recup_id }) return rqs def get_access_level_to_request(req, user): def get_access_level_to_request(req: PrintRequest, user): #req is id or req same for user # TODO return REQUEST_ACCESS_OPERATOR # FIXME Loading @@ -102,7 +136,11 @@ def update_requires_operator_rights(update_json): s.discard(REQUEST_USER_ELEMENTS) return len(s) > 0 #### ROUTES #### #endregion UTILS #region routes #region Project management & basic routes @app.route("/whoami") @jwt_required Loading @@ -114,7 +152,6 @@ def whoami(): def get_my_requests(): return jsonify(loop_requests(lookup_requests(current_user.username))) @app.route("/requests/<req_id>", methods=['PUT']) @jwt_required def update_request(req_id): Loading @@ -122,16 +159,24 @@ def update_request(req_id): # Operator can update all request parameters # req = get_request_by_id(req_id) req: PrintRequest = get_request_by_id(req_id) changes = request.get_json() if not changes: return jsonify({'error': 'No modifications'}), status.HTTP_400_BAD_REQUEST if req is None: return jsonify({'error': 'No such request'}, status.HTTP_404_NOT_FOUND) return jsonify({'error': 'No such request'}), status.HTTP_404_NOT_FOUND required_access = REQUEST_ACCESS_OPERATOR if req.status > 0 and required_access = REQUEST_ACCESS_OPERATOR if req.status > 0 or update_requires_operator_rights(changes) else REQUEST_ACCESS_USER if get_access_level_to_request(req, current_user) < REQUEST_ACCESS_OPERATOR: return jsonify({'error': 'Access denied'}, status.HTTP_401_UNAUTHORIZED) return jsonify({'error': 'Access denied'}), status.HTTP_401_UNAUTHORIZED # TODO access request.get_json() for e in changes: setattr(req, e, changes[e]) req.last_modification = datetime.datetime.now() req.save() return jsonify({}) @app.route("/requests/new", methods=['POST']) @jwt_required Loading @@ -152,7 +197,7 @@ def add_new_request(): add_request_event(req, "Request created") return jsonify({ request_id: req.id "request_id": req.id }) @app.route("/files/<kind>/upload/<req_id>", methods=['POST']) Loading @@ -161,8 +206,8 @@ def stl_upload(kind, req_id): if not kind in ['stl', 'gcode']: return jsonify({'error': 'Invalid file kind'}, status.HTTP_400_BAD_REQUEST) request: PrintRequest = get_request_by_id(int(req_id)) if request == None: req: PrintRequest = get_request_by_id(int(req_id)) if req == None: return jsonify({'error': 'No such print request'}, status.HTTP_404_NOT_FOUND) required_access = REQUEST_ACCESS_USER if kind == "stl" else REQUEST_ACCESS_OPERATOR Loading @@ -175,21 +220,27 @@ def stl_upload(kind, req_id): name = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(64)]) if kind == "stl": request.stl = name req.stl = name elif kind == "gcode": request.gcode = name req.gcode = name req.status = REQUEST_STATUS_AWAITING_PRINT else: return jsonify({'error': "Invalid file kind"}, status.HTTP_400_BAD_REQUEST) file.save(os.path.join(FILE_SAVE_PATH, name)) request.save() add_request_event(request, f'Added {kind} file') req.save() add_request_event(req, f'Added {kind} file') return jsonify({'ok': True}) @app.route("/requests/<req_id>") @jwt_required def get_request_infos(req_id): return jsonify(loop_requests(lookup_requests(req_id=req_id))) # ! FIXME Don't get request if no access @app.route("/requests/list", methods=['POST']) @jwt_required def get_requests(): def get_requests(): #! FIXME Don't get request if not access params = request.get_json() if not 'username' in params: params['username'] = None Loading @@ -197,6 +248,49 @@ def get_requests(): params['status'] = None return jsonify(loop_requests(lookup_requests(params['username'], params['status']))) #endregion #region user management routes #endregion user management #region Printer manager routes #endregion printer management #region queues management #endregion queues #region Slicer manager routes """ GET /slicer/update Authorization: Slicer key { "slicer_id", } 200 OK { "print_id" "title", "operator_name" "upload_token" } ------- POST /slicer/usage { ??? infos & crash status } """ #endregion slicer #endregion routes # peewee usage Loading Loading
.vscode/settings.json 0 → 100644 +3 −0 Original line number Diff line number Diff line { "python.linting.enabled": true } No newline at end of file
README.md +16 −5 Original line number Diff line number Diff line Loading @@ -4,17 +4,28 @@ Fablab 3d printing request application ## Minimum Viable Version - [ ] Tests - [x] User Connection - [ ] User administration - [ ] List users, - [ ] Change access level - [ ] Submit a print request - [ ] Upload a STL file - [ ] Create a new Print Request - [ ] Accept/Reject print request - [ ] See STL file in 3d - [ ] See print status - [x] Upload a STL file - [x] Backend - [x] Front - [x] Create a new Print Request - [x] Backend - [x] Front - [ ] Accept/Reject print request (front) - [x] See STL file in 3d - [x] See print status - [ ] Change Print status (see enum in api) - [ ] Slice - [ ] Upload GCODE - [ ] **V2**: Open slicer window - [ ] Message system - [ ] Post message on each request - [ ] New message indication - [ ] **V2**: Send to printer - [ ] Working queues - [ ] Printer management Loading
back/myfab/__pycache__/api.cpython-38.pyc +2.11 KiB (6.91 KiB) File changed.No diff preview for this file type. View original file View changed file
back/myfab/__pycache__/model.cpython-38.pyc +224 B (2.7 KiB) File changed.No diff preview for this file type. View original file View changed file
back/myfab/api.py +131 −37 Original line number Diff line number Diff line Loading @@ -5,21 +5,25 @@ from flask_jwt_extended import ( current_user ) from myfab.model import * import datetime from myfab.model import Event, PrintRequest, User, init_db_connection, Queue from flask_api import status from flask import jsonify import string import random import os from flask_cors import CORS init_db_connection() log.info("Database connected") app = Flask(__name__) CORS(app) app.debug = True app.config['SECRET_KEY'] = 'super-secret' #! CHANGEME app.config['JWT_IDENTITY_CLAIM'] = "login_name" FILE_SAVE_PATH = "./files" FILE_SAVE_PATH = "../ui/static/files" jwt = JWTManager(app) Loading @@ -32,13 +36,14 @@ def identity(payload): REQUEST_STATUS_PENDING_REVIEW = 0 # initial state REQUEST_STATUS_APPROVED = 1 # approved, waiting slicing REQUEST_STATUS_AWAITING_PRINT = 2 # sliced, in print queur REQUEST_STATUS_AWAITING_PRINT = 2 # sliced, in print queue REQUEST_STATUS_PRINTING = 3 # currently printing REQUEST_STATUS_FINISHED = 4 # done printing, waiting for recuperation REQUEST_STATUS_CLOSED = 5 # archived REQUEST_STATUS_REJECTED = 10 # refused by operator REQUEST_STATUS_PRINT_FAILED = 11 # print failed more than allowed REQUEST_STATUS_STANDBY = 12 # on hold, from any state REQUEST_STATUS_CANCELED = 13 # annulée REQUEST_ACCESS_DENIED = 0 # Not allowed to see this request REQUEST_ACCESS_OBSERVER = 1 # Allowed to see this request Loading @@ -48,8 +53,7 @@ REQUEST_ACCESS_OPERATOR = 3 # Is an operator of the system # Elements that can be modified by a user (not op) REQUEST_USER_ELEMENTS = set(["title","description","project"]) ### UTILS #### #region UTILS def get_or_create_user(username: str, fullname: str = None): req = User.select().where(User.username == username) if(req.count()): Loading @@ -59,40 +63,70 @@ def get_or_create_user(username: str, fullname: str = None): raise Exception("fullname cannot be None") return User.create(username=username, fullname=fullname) def lookup_requests(author: str = None, status: int = None): def lookup_requests(author: str = None, status = None, req_id = None): where_req = [] if author: where_req.append(PrintRequest.author.username == author) if status: where_req.append(PrintRequest.status == status) req = PrintRequest.select().where(*where_req).limit(30) return req.get() if status and len(status) > 0: w = None for sts in status: if w is None: w = PrintRequest.status == sts else: w = w or PrintRequest.status == sts where_req.append(w) if req_id: where_req.append(PrintRequest.id == req_id) req = PrintRequest.select().join(User, on=(PrintRequest.author == User.username)).limit(30) # .join(Queue, on=(PrintRequest.queue == Queue.id)) if len(where_req) > 0: req = req.where(*where_req) req = req.order_by(PrintRequest.last_modification.desc()) return req def get_request_by_id(rid): req = PrintRequest.select.where(PrintRequest.id == rid) req = PrintRequest.select().where(PrintRequest.id == rid) return req.get() if req.count() else None def add_request_event(req: PrintRequest, desc: str, op: User = None): log.info("Req %s: %s"%(req.id, desc)) req.last_modification = datetime.datetime.now() req.save() Event.create(request = req, description = desc, operator = op) def extract_date_time(elem): if type(elem) == tuple: elem = elem[0] # elem = datetime.datetime.fromisoformat(repr(elem)) return f'{elem.day}/{elem.month}/{elem.year} {elem.hour}:{elem.minute}:{elem.second}' def loop_requests(reqs): rqs = [] # if type(reqs) != list: # reqs = [reqs] for req in reqs: rqs.append({ request_id: req.id, title: req.title, description: req.description, stl: req.stl, status: req.status, project: req.project, operator: req.operator.username if req.operator is not None else "", queue: req.queue.name if req.queue is not None else "", recup: req.recup_id "request_id": req.id, "title": req.title, "creation": extract_date_time(req.creation), "last_modification": extract_date_time(req.last_modification), "author": req.author.fullname, "description": req.description, "stl": req.stl, "status": req.status, "reject_message": req.reject_message, "project": req.project, "gcode": req.gcode, "operator": req.operator.username if req.operator is not None else "", "queue": req.queue.name if req.queue is not None else "", "recup": req.recup_id }) return rqs def get_access_level_to_request(req, user): def get_access_level_to_request(req: PrintRequest, user): #req is id or req same for user # TODO return REQUEST_ACCESS_OPERATOR # FIXME Loading @@ -102,7 +136,11 @@ def update_requires_operator_rights(update_json): s.discard(REQUEST_USER_ELEMENTS) return len(s) > 0 #### ROUTES #### #endregion UTILS #region routes #region Project management & basic routes @app.route("/whoami") @jwt_required Loading @@ -114,7 +152,6 @@ def whoami(): def get_my_requests(): return jsonify(loop_requests(lookup_requests(current_user.username))) @app.route("/requests/<req_id>", methods=['PUT']) @jwt_required def update_request(req_id): Loading @@ -122,16 +159,24 @@ def update_request(req_id): # Operator can update all request parameters # req = get_request_by_id(req_id) req: PrintRequest = get_request_by_id(req_id) changes = request.get_json() if not changes: return jsonify({'error': 'No modifications'}), status.HTTP_400_BAD_REQUEST if req is None: return jsonify({'error': 'No such request'}, status.HTTP_404_NOT_FOUND) return jsonify({'error': 'No such request'}), status.HTTP_404_NOT_FOUND required_access = REQUEST_ACCESS_OPERATOR if req.status > 0 and required_access = REQUEST_ACCESS_OPERATOR if req.status > 0 or update_requires_operator_rights(changes) else REQUEST_ACCESS_USER if get_access_level_to_request(req, current_user) < REQUEST_ACCESS_OPERATOR: return jsonify({'error': 'Access denied'}, status.HTTP_401_UNAUTHORIZED) return jsonify({'error': 'Access denied'}), status.HTTP_401_UNAUTHORIZED # TODO access request.get_json() for e in changes: setattr(req, e, changes[e]) req.last_modification = datetime.datetime.now() req.save() return jsonify({}) @app.route("/requests/new", methods=['POST']) @jwt_required Loading @@ -152,7 +197,7 @@ def add_new_request(): add_request_event(req, "Request created") return jsonify({ request_id: req.id "request_id": req.id }) @app.route("/files/<kind>/upload/<req_id>", methods=['POST']) Loading @@ -161,8 +206,8 @@ def stl_upload(kind, req_id): if not kind in ['stl', 'gcode']: return jsonify({'error': 'Invalid file kind'}, status.HTTP_400_BAD_REQUEST) request: PrintRequest = get_request_by_id(int(req_id)) if request == None: req: PrintRequest = get_request_by_id(int(req_id)) if req == None: return jsonify({'error': 'No such print request'}, status.HTTP_404_NOT_FOUND) required_access = REQUEST_ACCESS_USER if kind == "stl" else REQUEST_ACCESS_OPERATOR Loading @@ -175,21 +220,27 @@ def stl_upload(kind, req_id): name = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(64)]) if kind == "stl": request.stl = name req.stl = name elif kind == "gcode": request.gcode = name req.gcode = name req.status = REQUEST_STATUS_AWAITING_PRINT else: return jsonify({'error': "Invalid file kind"}, status.HTTP_400_BAD_REQUEST) file.save(os.path.join(FILE_SAVE_PATH, name)) request.save() add_request_event(request, f'Added {kind} file') req.save() add_request_event(req, f'Added {kind} file') return jsonify({'ok': True}) @app.route("/requests/<req_id>") @jwt_required def get_request_infos(req_id): return jsonify(loop_requests(lookup_requests(req_id=req_id))) # ! FIXME Don't get request if no access @app.route("/requests/list", methods=['POST']) @jwt_required def get_requests(): def get_requests(): #! FIXME Don't get request if not access params = request.get_json() if not 'username' in params: params['username'] = None Loading @@ -197,6 +248,49 @@ def get_requests(): params['status'] = None return jsonify(loop_requests(lookup_requests(params['username'], params['status']))) #endregion #region user management routes #endregion user management #region Printer manager routes #endregion printer management #region queues management #endregion queues #region Slicer manager routes """ GET /slicer/update Authorization: Slicer key { "slicer_id", } 200 OK { "print_id" "title", "operator_name" "upload_token" } ------- POST /slicer/usage { ??? infos & crash status } """ #endregion slicer #endregion routes # peewee usage Loading