def login(): """Check user credentials and log in if authorized.""" if session.get("authorized"): return utils.success_response("User already authorized!", user=session.get("user")) else: username = request.form.get("login") password = request.form.get("password") try: user = get_user(username) current_app.logger.debug( f"User: {user.username}, {user.displayname}, {user.is_authenticated(password)}" ) except User.DoesNotExist: return utils.error_response("Invalid username or password!"), 401 if user.is_authenticated(password): session["authorized"] = True session["user"] = user.displayname session["uid"] = user.id session["admin"] = user.admin # For non-admin users make session expire when closing browser if not user.admin: session.permanent = False current_app.logger.debug("User %s logged in successfully" % username) return utils.success_response("User %s logged in successfully!" % username, user=user.displayname, admin=user.admin) return utils.error_response("Invalid username or password!"), 401
def parse_from_url(): """Extract recipe data from given url and return response with recipe data.""" url = request.args.get("url") if not url.startswith("http"): url = "http://" + url if not utils.valid_url(url): return utils.error_response(f"Invalid URL: {url}."), 400 import_parsers() # Collect all parser classes all_parsers = [parser for parser in GeneralParser.__subclasses__()] p = find_parser(all_parsers, url) if p: recipe = {} parser = p(url) recipe["title"] = parser.title recipe["contents"] = parser.contents recipe["ingredients"] = parser.ingredients recipe["source"] = parser.url recipe["portions_text"] = parser.portions image_path = download_image(parser.image) recipe["image"] = image_path return utils.success_response("Successfully extracted recipe.", data=recipe) else: return utils.error_response(f"No parser found for URL {url}."), 400
def add_recpie(): """Add new recipe to the data base.""" recipe_id = None filename = None try: data = request.form.to_dict() data = utils.deserialize(data) data["user"] = session.get("uid") data["published"] = False if data.get("published", True).lower() == "false" else True image_file = request.files.get("image") recipe_id = recipemodel.add_recipe(data) url = utils.make_url(data["title"], recipe_id) recipemodel.set_url(recipe_id, url) tagmodel.add_tags(data, recipe_id) storedmodel.add_recipe(recipe_id) save_image(data, recipe_id, image_file) return utils.success_response(msg="Recipe saved", url=url) except pw.IntegrityError: return utils.error_response("Recipe title already exists!"), 409 except Exception as e: # Delete recipe data and image if recipe_id is not None: storedmodel.delete_recipe(recipe_id) recipemodel.delete_recipe(recipe_id) if filename is not None: img_path = os.path.join(current_app.instance_path, current_app.config.get("IMAGE_PATH")) filepath = os.path.join(img_path, filename) try: utils.remove_file(filepath) except Exception: current_app.logger.warning(f"Could not delete file: {filepath}") current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to save data: {e}"), 400
def clean_tmp_data(): """Clean temporary data like uploaded images.""" password = request.args.get("password") if password != current_app.config.get("ADMIN_PASSWORD"): return utils.error_response("Failed to confirm password."), 500 try: tmp_path = os.path.join(current_app.instance_path, current_app.config.get("TMP_DIR")) data = utils.clean_tmp_folder(tmp_path) return utils.success_response(f"Successfully cleaned temporary data!", removed_files=data) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Cleanup failed: {e}"), 400
def get_random_recipe(): """Return one recipe at random from randomizer categories in config.""" tags = current_app.config.get("RANDOM_TAGS", []) or_expressions = reduce(pw.operator.or_, [ pw.fn.FIND_IN_SET(tag, pw.fn.group_concat(tagmodel.Tag.tagname)) for tag in tags ]) try: recipes = recipemodel.Recipe.select( recipemodel.Recipe, storedmodel.Stored, pw.fn.group_concat(tagmodel.Tag.tagname).alias("taglist") ).where( recipemodel.Recipe.published == True ).join( storedmodel.Stored, pw.JOIN.LEFT_OUTER, on=(storedmodel.Stored.recipeID == recipemodel.Recipe.id) ).join( tagmodel.RecipeTags, pw.JOIN.LEFT_OUTER, on=(tagmodel.RecipeTags.recipeID == recipemodel.Recipe.id) ).join( tagmodel.Tag, pw.JOIN.LEFT_OUTER, on=(tagmodel.Tag.id == tagmodel.RecipeTags.tagID) ).group_by( recipemodel.Recipe.id ).having( or_expressions ) recipe = [random.choice(recipemodel.get_recipes(recipes))] return utils.success_response(msg="Got random recipe", data=recipe, hits=len(recipe)) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to load data: {e}")
def edit_recpie(): """Edit a recipe that already exists in the data base.""" try: data = request.form.to_dict() data = utils.deserialize(data) data["user"] = session.get("uid") # Store info about which user edited last data["published"] = False if data.get("published", True).lower() == "false" else True url = utils.make_url(data["title"], data["id"]) data["url"] = url image_file = request.files.get("image") if not image_file and not data["image"]: recipe = recipemodel.Recipe.get(recipemodel.Recipe.id == data["id"]) if recipe.image: try: utils.remove_file(utils.remove_file(os.path.join(current_app.config.get("IMAGE_PATH"), recipe.image))) except OSError: current_app.logger.warning(traceback.format_exc()) else: save_image(data, data["id"], image_file) recipemodel.edit_recipe(data["id"], data) tagmodel.add_tags(data, data["id"]) return utils.success_response(msg="Recipe saved", url=url) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to save data: {e}"), 400
def suggest_recipe(): """Save a recipe suggestion in the data base (published=False).""" recipe_id = None filename = None try: data = request.form.to_dict() data = utils.deserialize(data) data["user"] = session.get("uid") data["published"] = False image_file = request.files.get("image") recipe_id = recipemodel.add_recipe(data) url = utils.make_url(data["title"], recipe_id) recipemodel.set_url(recipe_id, url) tagmodel.add_tags(data, recipe_id) storedmodel.add_recipe(recipe_id) save_image(data, recipe_id, image_file) # Attempt to send email to admins try: msg = ("Hej kalufs-admin!\n\nEtt nytt receptförslag med titel \"{}\" har lämnats in av {}.\n" "Logga in på https://kalufs.lol/recept för att granska och publicera receptet.\n\n" "Vänliga hälsningar,\nkalufs.lol" ).format(data.get("title"), data.get("suggester")) utils.send_mail(current_app.config.get("EMAIL_TO"), "Nytt receptförslag!", msg) except Exception: current_app.logger.error(traceback.format_exc()) return utils.success_response(msg="Recipe saved", url=url) except pw.IntegrityError: return utils.error_response("Recipe title already exists!"), 409 except Exception as e: # Delete recipe data and image if recipe_id is not None: storedmodel.delete_recipe(recipe_id) recipemodel.delete_recipe(recipe_id) if filename is not None: img_path = os.path.join(current_app.instance_path, current_app.config.get("IMAGE_PATH")) filepath = os.path.join(img_path, filename) try: utils.remove_file(filepath) except Exception: current_app.logger.warning(f"Could not delete file: {filepath}") current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to save data: {e}"), 400
def get_recipe_from_db(convert=False): """Get data for one recipe. Convert to html if convert=True.""" recipe_id = request.args.get("id") title = request.args.get("title") try: Changed = User.alias() recipes = recipemodel.Recipe.select( recipemodel.Recipe, User, Changed, storedmodel.Stored, pw.fn.group_concat(tagmodel.Tag.tagname).alias("taglist") ).where( recipemodel.Recipe.id == recipe_id if recipe_id else recipemodel.Recipe.title == title ).join( storedmodel.Stored, pw.JOIN.LEFT_OUTER, on=(storedmodel.Stored.recipeID == recipemodel.Recipe.id) ).switch( recipemodel.Recipe ).join( User, pw.JOIN.LEFT_OUTER, on=(User.id == recipemodel.Recipe.created_by).alias("a") ).switch( recipemodel.Recipe ).join( Changed, pw.JOIN.LEFT_OUTER, on=(Changed.id == recipemodel.Recipe.changed_by).alias("b") ).switch( recipemodel.Recipe ).join( tagmodel.RecipeTags, pw.JOIN.LEFT_OUTER, on=(tagmodel.RecipeTags.recipeID == recipemodel.Recipe.id) ).join( tagmodel.Tag, pw.JOIN.LEFT_OUTER, on=(tagmodel.Tag.id == tagmodel.RecipeTags.tagID) ).group_by(recipemodel.Recipe.id) recipe = recipemodel.get_recipe(recipes[0]) if convert: recipe = utils.recipe2html(recipe) if not recipe: return utils.error_response(f"Could not find recipe '{title}'."), 404 return utils.success_response(msg="Data loaded", data=recipe) except IndexError: return utils.error_response(f"Could not find recipe with ID '{recipe_id}'"), 404 except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to load recipe: {e}"), 400
def toggle_stored(): """Toggle the 'stored' value of a recipe.""" try: data = request.get_json() stored = data.get("stored", False) storedmodel.toggle_stored(data["id"], stored) if stored: return utils.success_response(msg="Recipe stored") else: return utils.success_response(msg="Recipe unstored") except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to save data: {e}"), 400
def get_parsers(): """Get a list of recipe pages for which there is a parser available.""" import_parsers() try: pages_list = [{ "domain": p.domain, "name": p.name, "address": p.address } for p in GeneralParser.__subclasses__()] return utils.success_response( "Successfully retrieved list of parsable pages.", data=pages_list) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response( f"Could not retrieve list of parsable pages: {e}"), 500
def toggle_needs_fix(): """Toggle the 'needs_fix' value of a recipe.""" try: data = request.form.to_dict() data = utils.deserialize(data) needs_fix = data.get("needs_fix", False) recipemodel.toggle_needs_fix(data["id"], needs_fix) if needs_fix: return utils.success_response(msg="Recipe marked as 'needs_fix'") else: return utils.success_response(msg="Recipe unmarked") except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to save data: {e}"), 400
def preview_data(): """Generate recipe preview. Convert markdown data to html.""" try: data = utils.recipe2html(request.form.to_dict()) data = utils.deserialize(data) image_file = request.files.get("image") if image_file: filename = utils.make_random_filename(image_file, file_extension=".jpg") directory = os.path.join(current_app.instance_path, current_app.config.get("TMP_DIR")) utils.save_upload_image(image_file, filename, directory) data["image"] = "tmp/" + filename return utils.success_response(msg="Data converted", data=data) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to convert data: {e}")
def delete_recpie(): """Remove recipe from data base.""" try: recipe_id = request.args.get("id") recipe = recipemodel.Recipe.get(recipemodel.Recipe.id == recipe_id) if recipe.image: utils.remove_file(os.path.join(current_app.config.get("IMAGE_PATH"), recipe.image)) utils.remove_file(os.path.join(current_app.config.get("THUMBNAIL_PATH"), recipe.image)) utils.remove_file(os.path.join(current_app.config.get("MEDIUM_IMAGE_PATH"), recipe.image)) tagmodel.delete_recipe(recipe_id) storedmodel.delete_recipe(recipe_id) recipemodel.delete_recipe(recipe_id) return utils.success_response(msg="Recipe removed") except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to remove recipe: {e}"), 400
def needs_fix_recipes(): """Return data for all recipes that need fixes.""" try: recipes = recipemodel.Recipe.select( recipemodel.Recipe, storedmodel.Stored, pw.fn.group_concat(tagmodel.Tag.tagname).alias("taglist") ).where( recipemodel.Recipe.needs_fix == True ).join( storedmodel.Stored, pw.JOIN.LEFT_OUTER, on=(storedmodel.Stored.recipeID == recipemodel.Recipe.id) ).join( tagmodel.RecipeTags, pw.JOIN.LEFT_OUTER, on=(tagmodel.RecipeTags.recipeID == recipemodel.Recipe.id) ).join( tagmodel.Tag, pw.JOIN.LEFT_OUTER, on=(tagmodel.Tag.id == tagmodel.RecipeTags.tagID) ).group_by(recipemodel.Recipe.id) data = recipemodel.get_recipes(recipes) return utils.success_response(msg="Data loaded", data=data, hits=len(data)) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to load data: {e}")
def get_recipe_data(published=False, complete_data=False): """Return published or unpublished recipe data.""" try: Changed = User.alias() recipes = recipemodel.Recipe.select( recipemodel.Recipe, storedmodel.Stored, pw.fn.group_concat(tagmodel.Tag.tagname).alias("taglist") ).where( recipemodel.Recipe.published == published ).join( storedmodel.Stored, pw.JOIN.LEFT_OUTER, on=(storedmodel.Stored.recipeID == recipemodel.Recipe.id) ).join( tagmodel.RecipeTags, pw.JOIN.LEFT_OUTER, on=(tagmodel.RecipeTags.recipeID == recipemodel.Recipe.id) ).join( tagmodel.Tag, pw.JOIN.LEFT_OUTER, on=(tagmodel.Tag.id == tagmodel.RecipeTags.tagID) ).group_by( recipemodel.Recipe.id) if complete_data: # Load in User table recipes = recipes.select( User, Changed, recipemodel.Recipe, storedmodel.Stored, pw.fn.group_concat(tagmodel.Tag.tagname).alias("taglist") ).switch( recipemodel.Recipe ).join( User, pw.JOIN.LEFT_OUTER, on=(User.id == recipemodel.Recipe.created_by).alias("a") ).switch( recipemodel.Recipe ).join( Changed, pw.JOIN.LEFT_OUTER, on=(Changed.id == recipemodel.Recipe.changed_by).alias("b")) data = recipemodel.get_recipes(recipes, complete_data=complete_data) return utils.success_response(msg="Data loaded", data=data, hits=len(data)) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Failed to load data: {e}")
def search(): """Search recipe data base.""" try: tag = request.args.get("tag") user = request.args.get("user") q = request.args.get("q") if tag: # Tag Search querytype = "tag" tagset = set(tag.split(",")) tagstructure = tagmodel.get_tag_structure(simple=True) taggroups = [] for cat in tagstructure: selected_tags = list(set(cat.get("tags")).intersection(tagset)) if selected_tags: taggroups.append(selected_tags) # Chain tags with OR within a category and with AND between categories and_expressions = [] for taggroup in taggroups: or_expressions = [ pw.fn.FIND_IN_SET(tag, pw.fn.group_concat(tagmodel.Tag.tagname)) for tag in taggroup ] and_expressions.append(reduce(pw.operator.or_, or_expressions)) expr = reduce(pw.operator.and_, and_expressions) elif user: # User search querytype = "user" expr = ((User.displayname == user) | recipemodel.Recipe.suggester.contains(user)) else: # String search: seperate by whitespace and search in all relevant fields querytype = "q" if len(q) > 1 and q.startswith('"') and q.endswith('"'): searchitems = [q[1:-1]] else: searchitems = q.split(" ") searchitems = [i.rstrip(",") for i in searchitems] expr_list = [ ( recipemodel.Recipe.title.contains(s) | recipemodel.Recipe.contents.contains(s) | recipemodel.Recipe.ingredients.contains(s) | recipemodel.Recipe.source.contains(s) | User.username.contains(s) | pw.fn.FIND_IN_SET(s, pw.fn.group_concat(tagmodel.Tag.tagname)) ) for s in searchitems ] expr = reduce(pw.operator.and_, expr_list) # Build query Changed = User.alias() query = recipemodel.Recipe.select( recipemodel.Recipe, User, Changed, storedmodel.Stored, pw.fn.group_concat(tagmodel.Tag.tagname).alias("taglist") ).join( storedmodel.Stored, pw.JOIN.LEFT_OUTER, on=(storedmodel.Stored.recipeID == recipemodel.Recipe.id) ).join( User, pw.JOIN.LEFT_OUTER, on=(User.id == recipemodel.Recipe.created_by).alias("a") ).switch( recipemodel.Recipe ).join( Changed, pw.JOIN.LEFT_OUTER, on=(Changed.id == recipemodel.Recipe.changed_by).alias("b") ).switch( recipemodel.Recipe ).join( tagmodel.RecipeTags, pw.JOIN.LEFT_OUTER, on=(tagmodel.RecipeTags.recipeID == recipemodel.Recipe.id) ).join( tagmodel.Tag, pw.JOIN.LEFT_OUTER, on=(tagmodel.Tag.id == tagmodel.RecipeTags.tagID) ).group_by( recipemodel.Recipe.id ).where( (recipemodel.Recipe.published == True) ).having(expr) data = recipemodel.get_recipes(query) message = f"Query: {querytype}={q}" return utils.success_response(msg=message, data=data, hits=len(data)) except Exception as e: current_app.logger.error(traceback.format_exc()) return utils.error_response(f"Query failed: {e}"), 400
def handle_unauthorized(e): """Handle 401.""" return utils.error_response("Unauthorized.")
def page_not_found(e): """Handle 404.""" return utils.error_response("Page not found.")