def parse_timestamp(data: dict, required: bool) -> dict: """ Parses a timestamp in a input dictionary. If there is no timestamp and it's not required, nothing happens. Otherwise an exception gets raised. If a timestamp exists, it gets parsed. :param data: The input dictionary :param required: Flag whether the timestamp is required. :return: The parsed input dictionary. """ # If the timestamp is missing but it is required, raise an exception. # Otherwise return the (non-modified) input data. if 'timestamp' not in data: if required: raise exc.DataIsMissing() else: return data # Get the timestamp timestamp = data.get('timestamp') # If the timestamp is not a string, raise an exception if not isinstance(timestamp, str): raise exc.WrongType() # Catch empty string timestamp which is caused by some JS date pickers # inputs when they get cleared. If the timestamp is required, raise an exception. if timestamp == '': if required: raise exc.DataIsMissing() else: del data['timestamp'] return data else: try: timestamp = dateutil.parser.parse(data['timestamp']) assert isinstance(timestamp, datetime.datetime) assert timestamp < datetime.datetime.now(datetime.timezone.utc) data['timestamp'] = timestamp.replace(microsecond=0) except (TypeError, ValueError, AssertionError): raise exc.InvalidData() return data
def insert_user(data): """ This help function creates a new user with the given data. :param data: Is the dictionary containing the data for the new user. :return: None :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. :raises PasswordsDoNotMatch: If the passwords do not match. :raises CouldNotCreateEntry: If the new user cannot be created. """ required = {'lastname': str} optional = {'firstname': str, 'password': str, 'password_repeat': str} check_fields_and_types(data, required, optional) password = None if 'password' in data: if 'password_repeat' not in data: raise exc.DataIsMissing() password = data['password'].strip() repeat_password = data['password_repeat'].strip() # Check if the passwords match. if password != repeat_password: raise exc.PasswordsDoNotMatch() # Check the password length if len(password) < app.config['MINIMUM_PASSWORD_LENGTH']: raise exc.PasswordTooShort() password = bcrypt.generate_password_hash(data['password']) # Try to create the user. if 'firstname' in data: firstname = data['firstname'] else: firstname = None try: user = User( firstname=firstname, lastname=data['lastname'], password=password) db.session.add(user) except IntegrityError: raise exc.CouldNotCreateEntry()
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 check_fields_and_types(data, required, optional=None): """ This function checks the given data for its types and existence. Required fields must exist, optional fields must not. :param data: The data sent to the API. :param required: A dictionary with all required entries and their types. :param optional: A dictionary with all optional entries and their types. :return: None :raises DataIsMissing: If a required field is not in the data. :raises WrongType: If a field is of the wrong type. """ if required and optional: allowed = dict(**required, **optional) elif required: allowed = required else: allowed = optional # Check if there is an unknown field in the data if not all(x in allowed for x in data): raise exc.UnknownField() # Check whether all required data is available if required and any(item not in data for item in required): raise exc.DataIsMissing() # Check all data (including optional data) for their types for key, value in data.items(): if not isinstance(value, allowed.get(key)): raise exc.WrongType()
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 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
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