def create_purchase(admin): """ Insert a new purchase. :param admin: Is the administrator user, determined by @adminOptional. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If the user with this ID does not exist. :raises UserIsNotVerified: If the user has not yet been verified. :raises EntryNotFound: If the product with this ID does not exist. :raises EntryIsNotForSale: If the product is not for sale. :raises EntryIsInactive: If the product is inactive. :raises InvalidAmount: If amount is less than or equal to zero. :raises InsufficientCredit: If the credit balance of the user is not sufficient. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() # It is allowed to create multiple purchases at once if isinstance(data, list): for purchase in data: insert_purchase(admin, purchase) else: insert_purchase(admin, data) try: db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Purchase created.'}), 200
def create_deposit(admin): """ Insert a new deposit. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If the user with this ID does not exist. :raises UserIsNotVerified: If the user has not yet been verified. :raises InvalidAmount: If amount is equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() # Use the insert deposit helper function to create the deposit entry. insert_deposit(data, admin) # Try to commit the deposit. try: db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created deposit.'}), 200
def create_rank(admin): """ Route to create a new rank. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If one or more fields are missing to create the rank. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises EntryAlreadyExists: If a rank with this name already exists. :raises CouldNotCreateEntry: If the new rank cannot be added to the database. """ data = json_body() required = {'name': str} optional = {'is_system_user': bool, 'debt_limit': int} # Check all required fields check_fields_and_types(data, required, optional) # Check if a tag with this name already exists if Rank.query.filter_by(name=data['name']).first(): raise exc.EntryAlreadyExists() try: rank = Rank(**data) db.session.add(rank) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created Rank.'}), 201
def create_turnover(admin): """ Insert a new turnover. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises InvalidAmount: If amount is equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() required = {'amount': int, 'comment': str} check_fields_and_types(data, required) # Check amount if data['amount'] == 0: raise exc.InvalidAmount() # Create and insert turnover try: turnover = Turnover(**data) turnover.admin_id = admin.id db.session.add(turnover) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created turnover.'}), 200
def update_purchase(id): """ Update the purchase with the given id. :param id: Is the purchase id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the purchase with this ID does not exist. :raises EntryNotRevocable: An attempt is made to revoked a purchase whose product is not revocable. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If no change occurred after the update. :raises CouldNotUpdateEntry: If any other error occurs. """ # Check purchase purchase = Purchase.query.filter_by(id=id).first() if not purchase: raise exc.EntryNotFound() # Query the product product = Product.query.filter_by(id=purchase.product_id).first() data = json_body() updateable = {'revoked': bool, 'amount': int} check_forbidden(data, updateable, purchase) check_fields_and_types(data, None, updateable) updated_fields = [] # Handle purchase revoke if 'revoked' in data: # In case that the product is not revocable, an exception must be made. if not product.revocable: raise exc.EntryNotRevocable() if purchase.revoked == data['revoked']: raise exc.NothingHasChanged() purchase.toggle_revoke(revoked=data['revoked']) updated_fields.append('revoked') del data['revoked'] # Handle all other fields updated_fields = update_fields(data, purchase, updated=updated_fields) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated purchase.', 'updated_fields': updated_fields }), 201
def update_replenishment(admin, replenishment_id): """ Update the replenishment with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param replenishment_id: Is the replenishment id. :return: A message that the update was successful and a list of all updated fields. """ return generic_update(Replenishment, replenishment_id, json_body(), admin)
def update_stocktaking(admin, stocktaking_id): """ Update the stocktaking with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param stocktaking_id: Is the stocktaking id. :return: A message that the update was successful and a list of all updated fields. """ return generic_update(Stocktaking, stocktaking_id, json_body(), admin)
def update_product(admin, product_id): """ Update the product with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param product_id: Is the product id. :return: A message that the update was successful and a list of all updated fields. """ return generic_update(Product, product_id, json_body(), admin)
def update_stocktakingcollection(admin, id): """ Update the stocktakingcollection with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the stocktakingcollection id. :return: A message that the update was successful. :raises EntryNotFound: If the stocktakingcollection with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If no change occurred after the update. :raises CouldNotUpdateEntry: If any other error occurs. """ # Check StocktakingCollection collection = (StocktakingCollection.query.filter_by(id=id).first()) if not collection: raise exc.EntryNotFound() data = json_body() if data == {}: raise exc.NothingHasChanged() updateable = {'revoked': bool} check_forbidden(data, updateable, collection) check_fields_and_types(data, None, updateable) updated_fields = [] # Handle revoke if 'revoked' in data: if collection.revoked == data['revoked']: raise exc.NothingHasChanged() collection.toggle_revoke(revoked=data['revoked'], admin_id=admin.id) del data['revoked'] updated_fields.append('revoked') # Handle all other fields updated_fields = update_fields(data, collection, updated_fields) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated stocktakingcollection.', 'updated_fields': updated_fields }), 201
def login(): """ Registered users can log in on this route. :return: A temporary valid token, which users can use to identify themselves when making requests to the API. :raises DataIsMissing: If the id or password (or both) is not included in the request. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises InvalidCredentials: If no user can be found with the given data. :raises UserIsNotVerified: If the user has not yet been verified. """ data = json_body() # Check all items in the json body. required = {'id': int, 'password': str} check_fields_and_types(data, required) # Try to get the user with the id user = User.query.filter_by(id=data['id']).first() # If no user with this data exists cancel the authentication. if not user: raise exc.InvalidCredentials() # Check if the user has already been verified. if not user.is_verified: raise exc.UserIsNotVerified() # Check if the user is inactive if not user.active: raise exc.UserIsInactive() # Check if the user has set a password. if not user.password: raise exc.InvalidCredentials() # Check if the password matches the user's password. if not bcrypt.check_password_hash(user.password, str(data['password'])): raise exc.InvalidCredentials() # Create a dictionary object of the user. fields = ['id', 'firstname', 'lastname', 'credit', 'is_admin'] d_user = convert_minimal(user, fields)[0] # Create a token. exp = datetime.datetime.utcnow() + datetime.timedelta(minutes=60) token = jwt.encode({'user': d_user, 'exp': exp}, app.config['SECRET_KEY']) # Return the result. return jsonify({'result': True, 'token': token.decode('UTF-8')}), 200
def update_stocktaking(admin, id): """ Update the stocktaking with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the stocktaking id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the stocktaking with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If no change occurred after the update. :raises CouldNotUpdateEntry: If any other error occurs. """ # Check Stocktaking stocktaking = Stocktaking.query.filter_by(id=id).first() if not stocktaking: raise exc.EntryNotFound() # Data validation data = json_body() updateable = {'count': int} check_forbidden(data, updateable, stocktaking) check_fields_and_types(data, None, updateable) updated_fields = [] message = 'Updated stocktaking.' # Check count if 'count' in data: if data['count'] < 0: raise exc.InvalidAmount() if data['count'] == stocktaking.count: raise exc.NothingHasChanged() # Handle all other fields updated_fields = update_fields(data, stocktaking, updated_fields) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({'message': message, 'updated_fields': updated_fields}), 201
def set_maintenance_mode(admin): """ This route can be used by an administrator to switch the maintenance mode on or off. :param admin: Is the administrator user, determined by @adminRequired. :raises DataIsMissing: If the maintenance state is not included in the request. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If the maintenance mode is not changed by the request. :return: A message with the new maintenance mode. """ data = json_body() # Check all items in the json body. required = {'state': bool} check_fields_and_types(data, required) # Get the current maintenance state. current_state = app.config['MAINTENANCE'] # Get the new state. new_state = data['state'] # Handle the request. if current_state == new_state: raise exc.NothingHasChanged() # Change the config file persistently RE_MAINENTANCE_PATTERN = r"(.*)(MAINTENANCE)([^\w]*)(True|False)" with open(os.path.join(PATH, "configuration.py"), "r") as config_file: config_file_content = config_file.read() new_config_file = re.sub(RE_MAINENTANCE_PATTERN, r"\1\2\3{}".format(str(new_state)), config_file_content) with open(os.path.join(PATH, "configuration.py"), "w") as config_file: config_file.write(new_config_file) # Change the current app state app.config['MAINTENANCE'] = new_state message = 'Turned maintenance mode ' + ('on.' if new_state else 'off.') return jsonify({'message': message})
def update_refund(admin, id): """ Update the refund with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the refund id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the refund with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If no change occurred after the update. :raises CouldNotUpdateEntry: If any other error occurs. """ # Check refund refund = Refund.query.filter_by(id=id).first() if not refund: raise exc.EntryNotFound() data = json_body() if not data: raise exc.NothingHasChanged() updateable = {'revoked': bool} check_forbidden(data, updateable, refund) check_fields_and_types(data, None, updateable) # Handle refund revoke if 'revoked' in data: if refund.revoked == data['revoked']: raise exc.NothingHasChanged() refund.toggle_revoke(revoked=data['revoked'], admin_id=admin.id) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated refund.', }), 201
def register(): """ Registration of new users. :return: A message that the registration was successful. :raises CouldNotCreateEntry: If the new user cannot be created. """ insert_user(json_body()) try: db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created user.'}), 200
def update_user(admin, user_id): """ Update the user with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param user_id: Is the user id. :return: A message that the update was successful and a list of all updated fields. """ # Get the update data data = json_body() # Query the user. If he/she is not verified yet, there *must* be a # rank_id given in the update data. user = User.query.filter(User.id == user_id).first() if not user: raise exc.EntryNotFound() if not user.is_verified and 'rank_id' not in data: raise exc.UserIsNotVerified() # The password pre-check must be done here... if 'password' in data: # The repeat password must be there, too! if 'password_repeat' not in data: raise exc.DataIsMissing() # Both must be strings if not all([isinstance(x, str) for x in [data['password'], data['password_repeat']]]): raise exc.WrongType() # Passwords must match if data['password'] != data['password_repeat']: raise exc.PasswordsDoNotMatch() # Minimum password length if len(data['password']) < app.config['MINIMUM_PASSWORD_LENGTH']: raise exc.PasswordTooShort() # Convert the password into a salted hash # DONT YOU DARE TO REMOVE THIS LINE data['password'] = bcrypt.generate_password_hash(data['password']) # DONT YOU DARE TO REMOVE THIS LINE # All fine, delete repeat_password from the dict and do the rest of the update del data['password_repeat'] return generic_update(User, user_id, data, admin)
def update_tag(admin, id): """ Update the tag with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the tag id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the tag with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises CouldNotUpdateEntry: If any other error occurs. """ data = json_body() # Check, if the product exists. tag = Tag.query.filter_by(id=id).first() if not tag: raise exc.EntryNotFound() updateable = {'name': str} # Check forbidden fields check_forbidden(data, updateable, tag) # Check types check_fields_and_types(data, None, updateable) updated_fields = update_fields(data, tag) tag.created_by = admin.id # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated tag.', 'updated_fields': updated_fields }), 201
def create_refund(admin): """ Insert a new refund. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If the user with this ID does not exist. :raises UserIsNotVerified: If the user has not yet been verified. :raises InvalidAmount: If amount is equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() required = {'user_id': int, 'total_price': int, 'comment': str} check_fields_and_types(data, required) user = User.query.filter_by(id=data['user_id']).first() if not user: raise exc.EntryNotFound() # Check if the user has been verified. if not user.is_verified: raise exc.UserIsNotVerified() # Check if the user is inactive if not user.active: raise exc.UserIsInactive() # Check amount if data['total_price'] <= 0: raise exc.InvalidAmount() # Create and insert refund try: refund = Refund(**data) refund.admin_id = admin.id db.session.add(refund) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created refund.'}), 200
def create_tag(admin): """ Route to create a new tag. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If one or more fields are missing to create the tag. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises EntryAlreadyExists: If a tag with this name already exists. :raises CouldNotCreateEntry: If the new tag cannot be added to the database. """ data = json_body() required = {'name': str} # Check all required fields check_fields_and_types(data, required) # Check if a tag with this name already exists if Tag.query.filter_by(name=data['name']).first(): raise exc.EntryAlreadyExists() try: tag = Tag(**data) tag.created_by = admin.id db.session.add(tag) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created Tag.'}), 201
def verify_user(admin, id): """ Verify a user. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the user id. :return: A message that the verification was successful. :raises UserAlreadyVerified: If the user already has been verified. :raises DataIsMissing: If the rank_id is not included in the request. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises EntryNotFound: If the rank to be assigned to the user does not exist. """ user = User.query.filter_by(id=id).first() if not user: raise exc.EntryNotFound() if user.is_verified: raise exc.UserAlreadyVerified() data = json_body() # Check all items in the json body. required = {'rank_id': int} check_fields_and_types(data, required) rank_id = data['rank_id'] rank = Rank.query.filter_by(id=rank_id).first() if not rank: raise exc.EntryNotFound() user.verify(admin_id=admin.id, rank_id=rank_id) db.session.commit() return jsonify({'message': 'Verified user.'}), 201
def create_batch_deposit(admin): """ Insert a new batch deposit. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If any user cannot be found. :raises UserIsNotVerified: If any user is not verified. :raises InvalidAmount: If amount is equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() required = {'user_ids': list, 'amount': int, 'comment': str} check_fields_and_types(data, required) # Call the insert deposit helper function for each user. for user_id in data['user_ids']: data = { 'user_id': user_id, 'comment': data['comment'], 'amount': data['amount'] } insert_deposit(data, admin) # Try to commit the changes. try: db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created batch deposit.'}), 200
def create_product(admin): """ Route to create a new product. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If one or more fields are missing to create the product. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises EntryAlreadyExists: If a product with this name already exists. :raises EntryAlreadyExists: If the barcode already exists. :raises CouldNotCreateEntry: If the new product cannot be added to the database. """ data = json_body() required = {'name': str, 'price': int, 'tags': list} optional = { 'barcode': str, 'active': bool, 'countable': bool, 'revocable': bool, 'imagename': str } # Check all required fields check_fields_and_types(data, required, optional) # Check if a product with this name already exists if Product.query.filter_by(name=data['name']).first(): raise exc.EntryAlreadyExists() # Check if a product with this barcode already exists if 'barcode' in data: if Product.query.filter_by(barcode=data['barcode']).first(): raise exc.EntryAlreadyExists() # Check the product tags tags = data['tags'] for tag_id in tags: if not isinstance(tag_id, int): raise exc.WrongType tag = Tag.query.filter_by(id=tag_id).first() if not tag: raise exc.EntryNotFound del data['tags'] # Save the price and delete it from the data dictionary price = int(data['price']) del data['price'] try: product = Product(**data) product.created_by = admin.id db.session.add(product) db.session.flush() product.set_price(price=price, admin_id=admin.id) for tag_id in tags: tag = Tag.query.filter_by(id=tag_id).first() product.tags.append(tag) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created Product.'}), 201
def update_product(admin, id): """ Update the product with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the product id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the product with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises EntryNotFound: If the image is to be changed but no image with this name exists. :raises CouldNotUpdateEntry: If any other error occurs. """ data = json_body() # Check, if the product exists. product = Product.query.filter_by(id=id).first() if not product: raise exc.EntryNotFound() optional = { 'name': str, 'price': int, 'barcode': str, 'imagename': str, 'countable': bool, 'revocable': bool } # Check forbidden fields check_forbidden(data, optional, product) # Check types check_fields_and_types(data, None, optional) updated_fields = [] # Check for price change if 'price' in data: price = int(data['price']) del data['price'] if price != product.price: product.set_price(price=price, admin_id=admin.id) updated_fields.append('price') # Check for barcode change if 'barcode' in data: if Product.query.filter_by(barcode=data['barcode']).first(): raise exc.EntryAlreadyExists() # Check for image change. if 'imagename' in data: imagename = data['imagename'] del data['imagename'] if imagename != product.imagename: upload = Upload.query.filter_by(filename=imagename).first() if not upload: raise exc.EntryNotFound() product.image_upload_id = upload.id updated_fields.append('imagename') # Update all other fields updated_fields = update_fields(data, product, updated=updated_fields) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated product.', 'updated_fields': updated_fields }), 201
def create_replenishmentcollection(admin): """ Insert a new replenishmentcollection. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises ForbiddenField : If a forbidden field is in the data. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If the product with with the id of any replenishment does not exist. :raises InvalidAmount: If amount of any replenishment is less than or equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() required = {'replenishments': list, 'comment': str} required_repl = {'product_id': int, 'amount': int, 'total_price': int} # Check all required fields check_fields_and_types(data, required) replenishments = data['replenishments'] # Check for the replenishments in the collection if not replenishments: raise exc.DataIsMissing() for repl in replenishments: # Check all required fields check_fields_and_types(repl, required_repl) product_id = repl.get('product_id') amount = repl.get('amount') # Check amount if amount <= 0: raise exc.InvalidAmount() # Check product product = Product.query.filter_by(id=product_id).first() if not product: raise exc.EntryNotFound() # If the product has been marked as inactive, it will now be marked as # active again. if not product.active: product.active = True # Create and insert replenishmentcollection try: collection = ReplenishmentCollection(admin_id=admin.id, comment=data['comment'], revoked=False) db.session.add(collection) db.session.flush() for repl in replenishments: rep = Replenishment(replcoll_id=collection.id, **repl) db.session.add(rep) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created replenishmentcollection.'}), 201
def update_user(admin, id): """ Update the user with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the user id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the user with this ID does not exist. :raises UserIsNotVerified: If the user has not yet been verified. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises PasswordsDoNotMatch: If the password and its repetition do not match. :raises DataIsMissing: If the password is to be updated but no repetition of the password exists in the request. """ data = json_body() # Query user user = User.query.filter(User.id == id).first() if not user: raise exc.EntryNotFound() # Raise an exception if the user has not been verified yet. if not user.is_verified: raise exc.UserIsNotVerified() allowed = { 'firstname': str, 'lastname': str, 'password': str, 'password_repeat': str, 'is_admin': bool, 'rank_id': int } # Check the data for forbidden fields. check_forbidden(data, allowed, user) # Check all allowed fields and for their types. check_fields_and_types(data, None, allowed) updated_fields = [] # Update admin role if 'is_admin' in data: user.set_admin(is_admin=data['is_admin'], admin_id=admin.id) if not user.is_admin: users = User.query.all() admins = list(filter(lambda x: x.is_admin, users)) if not admins: raise exc.NoRemainingAdmin() updated_fields.append('is_admin') del data['is_admin'] # Update rank if 'rank_id' in data: user.set_rank_id(rank_id=data['rank_id'], admin_id=admin.id) updated_fields.append('rank_id') del data['rank_id'] # Check password if 'password' in data: if 'password_repeat' in data: password = data['password'].strip() password_repeat = data['password_repeat'].strip() if password != password_repeat: raise exc.PasswordsDoNotMatch() if len(password) < app.config['MINIMUM_PASSWORD_LENGTH']: raise exc.PasswordTooShort() user.password = bcrypt.generate_password_hash(password) updated_fields.append('password') del data['password_repeat'] else: raise exc.DataIsMissing() del data['password'] # All other fields updateable = ['firstname', 'lastname'] check_forbidden(data, updateable, user) updated_fields = update_fields(data, user, updated=updated_fields) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated user.', 'updated_fields': updated_fields }), 201
def update_replenishment(admin, id): """ Update the replenishment with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the replenishment id. :return: A message that the update was successful and a list of all updated fields. :raises EntryNotFound: If the replenishment with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If no change occurred after the update. :raises CouldNotUpdateEntry: If any other error occurs. """ # Check Replenishment repl = Replenishment.query.filter_by(id=id).first() if not repl: raise exc.EntryNotFound() # Get the corresponding ReplenishmentCollection replcoll = (ReplenishmentCollection.query.filter_by( id=repl.replcoll_id).first()) # Get all not revoked replenishments corresponding to the # replenishmentcollection before changes are made repls_nr = replcoll.replenishments.filter_by(revoked=False).all() # Data validation data = json_body() updateable = {'revoked': bool, 'amount': int, 'total_price': int} check_forbidden(data, updateable, repl) check_fields_and_types(data, None, updateable) updated_fields = [] message = 'Updated replenishment.' # Handle replenishment revoke if 'revoked' in data: if repl.revoked == data['revoked']: raise exc.NothingHasChanged() if not data['revoked'] and not repls_nr: replcoll.toggle_revoke(revoked=False, admin_id=admin.id) message = message + ( ' Rerevoked ReplenishmentCollection ID: {}'.format( replcoll.id)) repl.toggle_revoke(revoked=data['revoked'], admin_id=admin.id) del data['revoked'] updated_fields.append('revoked') # Handle all other fields updated_fields = update_fields(data, repl, updated_fields) # Check if ReplenishmentCollection still has unrevoked Replenishments repls = replcoll.replenishments.filter_by(revoked=False).all() if not repls and not replcoll.revoked: message = message + (' Revoked ReplenishmentCollection ID: {}'.format( replcoll.id)) replcoll.toggle_revoke(revoked=True, admin_id=admin.id) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({'message': message, 'updated_fields': updated_fields}), 201
def update_replenishmentcollection(admin, id): """ Update the replenishmentcollection with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param id: Is the replenishmentcollection id. :return: A message that the update was successful. :raises EntryNotFound: If the replenishmentcollection with this ID does not exist. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises NothingHasChanged: If no change occurred after the update. :raises CouldNotUpdateEntry: If any other error occurs. :raises EntryNotRevocable: If the replenishmentcollections was revoked by by replenishment_update, because all replenishments are revoked, the revoked field can not be set to true. """ # Check ReplenishmentCollection replcoll = (ReplenishmentCollection.query.filter_by(id=id).first()) if not replcoll: raise exc.EntryNotFound() # Which replenishments are not revoked? repls = replcoll.replenishments.filter_by(revoked=False).all() data = json_body() if data == {}: raise exc.NothingHasChanged() updateable = {'revoked': bool, 'comment': str, 'timestamp': int} check_forbidden(data, updateable, replcoll) check_fields_and_types(data, None, updateable) updated_fields = [] # Handle replenishmentcollection revoke if 'revoked' in data: if replcoll.revoked == data['revoked']: raise exc.NothingHasChanged() # Check if the revoke was caused through the replenishment_update and # therefor cant be changed if not data['revoked'] and not repls: raise exc.EntryNotRevocable() replcoll.toggle_revoke(revoked=data['revoked'], admin_id=admin.id) del data['revoked'] updated_fields.append('revoked') # Handle new timestamp if 'timestamp' in data: try: timestamp = datetime.datetime.fromtimestamp(data['timestamp']) assert timestamp <= datetime.datetime.now() replcoll.timestamp = timestamp updated_fields.append('revoked') except (AssertionError, TypeError, ValueError, OSError, OverflowError): """ AssertionError: The timestamp lies in the future. TypeError: Invalid type for conversion. ValueError: Timestamp is out of valid range. OSError: Value exceeds the data type. OverflowError: Timestamp out of range for platform time_t. """ raise exc.InvalidData() del data['timestamp'] # Handle all other fields updated_fields = update_fields(data, replcoll, updated_fields) # Apply changes try: db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({ 'message': 'Updated replenishmentcollection.', 'updated_fields': updated_fields }), 201
def create_purchase(admin): """ Insert a new purchase. :param admin: Is the administrator user, determined by @adminOptional. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If the user with this ID does not exist. :raises UserIsNotVerified: If the user has not yet been verified. :raises EntryNotFound: If the product with this ID does not exist. :raises EntryIsInactive: If the product is inactive. :raises InvalidAmount: If amount is less than or equal to zero. :raises InsufficientCredit: If the credit balance of the user is not sufficient. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() required = {'user_id': int, 'product_id': int, 'amount': int} check_fields_and_types(data, required) # Check user user = User.query.filter_by(id=data['user_id']).first() if not user: raise exc.EntryNotFound() # Check if the user has been verified. if not user.is_verified: raise exc.UserIsNotVerified() # Check if the user is inactive if not user.active: raise exc.UserIsInactive() # Check product product = Product.query.filter_by(id=data['product_id']).first() if not product: raise exc.EntryNotFound() if not product.active: raise exc.EntryIsInactive() # Check amount if data['amount'] <= 0: raise exc.InvalidAmount() # If the purchase is made by an administrator, the credit limit # may be exceeded. if not admin: limit = Rank.query.filter_by(id=user.rank_id).first().debt_limit current_credit = user.credit future_credit = current_credit - (product.price * data['amount']) if future_credit < limit: raise exc.InsufficientCredit() try: purchase = Purchase(**data) db.session.add(purchase) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Purchase created.'}), 200
def change_product_tag_assignment(admin, command): """ Under this route, a tag can be added to a product or removed. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the assignment has been added or removed. :raises ForbiddenField: If a forbidden field is in the request data. :raises UnknownField: If an unknown parameter exists in the request data. :raises InvalidType: If one or more parameters have an invalid type. :raises EntryNotFound If the product with the specified ID does not exist. :raises EntryNotFound: If the tag with the specified ID does not exist. :raises NothingHasChanged: If no change occurred after the update or removal. """ if command not in ['add', 'remove']: raise exc.UnauthorizedAccess() data = json_body() required = {'product_id': int, 'tag_id': int} # Check all required fields check_fields_and_types(data, required) # Check if the product exists. product = Product.query.filter_by(id=data['product_id']).first() if not product: raise exc.EntryNotFound() # Check if the tag exists. tag = Tag.query.filter_by(id=data['tag_id']).first() if not tag: raise exc.EntryNotFound() if command == 'add': if tag in product.tags: raise exc.NothingHasChanged() try: product.tags.append(tag) db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({'message': 'Tag assignment has been added.'}), 201 else: if tag not in product.tags: raise exc.NothingHasChanged() if len(product.tags) <= 1: raise exc.NoRemainingTag() try: product.tags.remove(tag) db.session.commit() except IntegrityError: raise exc.CouldNotUpdateEntry() return jsonify({'message': 'Tag assignment has been removed.'}), 201
def create_stocktakingcollections(admin): """ Insert a new stocktakingcollection. :param admin: Is the administrator user, determined by @adminRequired. :return: A message that the creation was successful. :raises DataIsMissing: If not all required data is available. :raises ForbiddenField : If a forbidden field is in the data. :raises WrongType: If one or more data is of the wrong type. :raises EntryNotFound: If the product with with the id of any replenishment does not exist. :raises InvalidAmount: If amount of any replenishment is less than or equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ data = json_body() required = {'stocktakings': list, 'timestamp': int} required_s = {'product_id': int, 'count': int} optional_s = {'keep_active': bool} # Check all required fields check_fields_and_types(data, required) stocktakings = data['stocktakings'] # Check for stocktakings in the collection if not stocktakings: raise exc.DataIsMissing() for stocktaking in stocktakings: product_id = stocktaking.get('product_id') product = Product.query.filter_by(id=product_id).first() if not product: raise exc.EntryNotFound() if not product.countable: raise exc.InvalidData() # Get all active product ids products = (Product.query.filter(Product.active.is_(True)).filter( Product.countable.is_(True)).all()) active_ids = list(map(lambda p: p.id, products)) data_product_ids = list(map(lambda d: d['product_id'], stocktakings)) # Compare function def compare(x, y): return collections.Counter(x) == collections.Counter(y) # We need an entry for all active products. If some data is missing, # raise an exception if not compare(active_ids, data_product_ids): raise exc.DataIsMissing() # Check the timestamp try: timestamp = datetime.datetime.fromtimestamp(data['timestamp']) assert timestamp <= datetime.datetime.now() except (AssertionError, TypeError, ValueError, OSError, OverflowError): # AssertionError: The timestamp is after the current time. # TypeError: Invalid type for conversion. # ValueError: Timestamp is out of valid range. # OSError: Value exceeds the data type. # OverflowError: Timestamp out of range for platform time_t. raise exc.InvalidData() # Create stocktakingcollection collection = StocktakingCollection(admin_id=admin.id, timestamp=timestamp) db.session.add(collection) db.session.flush() # Check for all required data and types for stocktaking in stocktakings: # Check all required fields check_fields_and_types(stocktaking, required_s, optional_s) # Get all fields product_id = stocktaking.get('product_id') count = stocktaking.get('count') keep_active = stocktaking.get('keep_active', False) # Check amount if count < 0: raise exc.InvalidAmount() # Does the product changes its active state? product = Product.query.filter_by(id=product_id).first() if count == 0 and keep_active is False: product.active = False # Create and insert stocktakingcollection try: for stocktaking in stocktakings: s = Stocktaking(collection_id=collection.id, product_id=stocktaking.get('product_id'), count=stocktaking.get('count')) db.session.add(s) db.session.commit() except IntegrityError: raise exc.CouldNotCreateEntry() return jsonify({'message': 'Created stocktakingcollection.'}), 201