import datetime import hashlib import os from PIL import Image, ImageSequence import concurrent.futures from flask import Flask, render_template, redirect, url_for, request, jsonify from flask_login import login_required, LoginManager, login_user, logout_user, current_user from models.Habit import Habit from models.HabitList import HabitList from models.HabitTracking import HabitTracking from models.User import User from utils import anonymous_required # Create a new Flask instance app = Flask(__name__) app.secret_key = 'PSSSSSHHHT!' UPLOAD_FOLDER = 'static/profile_images/' # Folder to store profile images app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # Initialize the Flask-Login extension login_manager = LoginManager() login_manager.login_view = 'login' login_manager.init_app(app) @login_manager.user_loader def load_user(user_id): return User.get(user_id) @app.context_processor def inject_user(): return dict(user=current_user) @app.context_processor def inject_notifications(): if current_user.is_authenticated: habit_lists = current_user.get_unaccepted_habitLists() lists = [] for habit_list in habit_lists: # Check if the user is the first user in the list if habit_list.get_users()[0].id != current_user.id: lists.append(habit_list) return dict(notifications=lists) return dict(notifications=[]) # Create a new route @app.route('/') def index(): if current_user.is_authenticated: habit_lists = current_user.get_habitLists() name = "Hallo " + current_user.name heatmap_values, day = current_user.get_heatmap() heatmap_color = current_user.heatmap_color else: habit_lists = [] name = "Bitte melde dich an." heatmap_values = [] day = None heatmap_color = None # Sort habits by whether they have been checked today and then by slot for habit_list in habit_lists: habit_list.habits = sorted(habit_list.get_habits(), key=lambda habit: (habit.checked, habit.slot)) for habit in habit_list.get_habits(): habit.load_statistics() return render_template( 'index.html', title=name, habit_lists=habit_lists, heatmap_values=heatmap_values, day=day, color=heatmap_color, errors={} ) ###################### Login & Signup ##################### @app.route('/login') @anonymous_required def login(): return render_template('auth/login.html', errors={}) @app.route('/signup') @anonymous_required def signup(): return render_template('auth/signup.html', errors={}) @app.route('/login', methods=['POST']) def login_post(): email = request.form.get('email') password = request.form.get('password') # Check for errors errors = {} if not email: errors['email'] = 'Die E-Mail Adresse ist erforderlich.' if not password: errors['password'] = 'Das Passwort ist erforderlich.' # Check if user exists user = User.get_by_email(email) if not user: errors['email'] = 'E-Mail Adresse nicht gefunden.' elif user.password is None or hashlib.sha256(password.encode()).hexdigest() != user.password: errors['password'] = 'Das Passwort ist falsch.' if errors: return render_template( 'auth/login.html', email=email, password=password, errors=errors ) login_user(user) # Redirect to login page return redirect(url_for('index')) @app.route('/signup', methods=['POST']) def signup_post(): email = request.form.get('email') name = request.form.get('name') password = request.form.get('password') # Check for errors errors = {} if not email: errors['email'] = 'Die E-Mail Adresse ist erforderlich.' if not name: errors['name'] = 'Der Name ist erforderlich.' if not password: errors['password'] = 'Das Passwort ist erforderlich.' # Check if email is already in use if User.get_by_email(email): errors['email'] = 'E-Mail Adresse bereits in Benutzung.' if errors: return render_template( 'auth/signup.html', email=email, name=name, password=password, errors=errors ) # Save user to database. Maybe log the user in directly. user = User.create(name, email, password) login_user(user) # Redirect to login page return redirect(url_for('index')) @app.route('/logout') @login_required def logout(): # Log out functionality logout_user() return redirect(url_for('index')) ########################################################### ########################## Habit ########################## @app.route('/habit') @login_required def habit_creation(): return render_template( 'habit.html', title='Erstelle ein Habit', unit="Woche", errors={}, ) @app.route('/habit', methods=['POST']) @login_required def habit_create(): name = request.form.get('name') note = request.form.get('note') times = request.form.get('times') unit = request.form.get('unit') list_id = request.form.get('list_query') # Check for errors errors = {} if not name: errors['name'] = 'Der Name ist erforderlich.' if not times: errors['times'] = 'Die Anzahl ist erforderlich.' if not note: note = '' if not unit: errors['unit'] = 'Die Einheit ist erforderlich.' if not list_id: errors['list_query'] = 'Die Habitliste ist erforderlich.' # Check if times is an integer try: times = int(times) # Check that times is greater than 0 if times <= 0: errors['times'] = 'Die Anzahl muss größer als 0 sein.' except ValueError: errors['times'] = 'Die Anzahl muss eine Zahl sein.' # Check that unit is valid if unit not in ['Tag', 'Woche', 'Monat', 'Jahr']: errors['unit'] = 'Die Einheit ist ungültig.' # check if list_id is an int try: list_id = int(list_id) except ValueError: errors['list_query'] = 'Die Anzahl muss eine Zahl sein.' # Check if times is possible to achieve if unit == 'Tag' and times != 1: errors['times'] = 'Die Anzahl muss 1 sein, wenn das Habit täglich ist.' if unit == 'Woche' and times > 7: errors['times'] = 'Die Anzahl darf höchstens 7 sein, wenn das Habit wöchentlich ist.' if unit == 'Monat' and times > 31: errors['times'] = 'Die Anzahl darf höchstens 31 sein, wenn das Habit monatlich ist.' if unit == 'Jahr' and times > 365: errors['times'] = 'Die Anzahl darf höchstens 365 sein, wenn das Habit jährlich ist.' if errors: return render_template( 'habit.html', title='Erstelle ein Habit', name=name, note=note, times=times, unit=unit, errors=errors, list_id=list_id ) # Map unit to integer if unit == 'Tag': unit = 0 elif unit == 'Woche': unit = 1 elif unit == 'Monat': unit = 2 elif unit == 'Jahr': unit = 3 else: unit = 1 # Save habit to database Habit.create(list_id, name, times, note, unit) # Back to index return redirect(url_for('index')) @app.route('/edit-habit') @login_required def edit_habit(): habit_id = int(request.args.get("habit")) habit = Habit.get(habit_id) units = ["Tag", "Woche", "Monat", "Jahr"] return render_template( "edit-habit.html", title=habit.name, habit=habit.id, name=habit.name, note=habit.note, times=habit.times, unit=units[habit.unit], errors={} ) @app.route('/edit-habit', methods=['POST']) @login_required def edit_habit_change(): units = ["Tag", "Woche", "Monat", "Jahr"] name = request.form.get('name') note = request.form.get('note') times = request.form.get('times') unit = request.form.get('unit') list_id = request.form.get('habit') habit = Habit.get(list_id) # Check for errors errors = {} if not name: errors['name'] = 'Der Name ist erforderlich.' if not times: errors['times'] = 'Die Anzahl ist erforderlich.' if not note: note = '' if not unit: errors['unit'] = 'Die Einheit ist erforderlich.' if not list_id: errors['habit'] = 'Das Habit ist erforderlich.' # Check if times is an integer try: print(times) times = int(times) # Check that times is greater than 0 if times <= 0: errors['times'] = 'Die Anzahl muss größer als 0 sein.' except ValueError: errors['times'] = 'Die Anzahl muss eine Zahl sein.' # Check that unit is valid if unit not in ['Tag', 'Woche', 'Monat', 'Jahr']: errors['unit'] = 'Die Einheit ist ungültig.' # check if list_id is an int try: int(list_id) except ValueError: errors['list_query'] = 'Die Anzahl muss eine Zahl sein.' # Check if times is possible to achieve if unit == 'Tag' and times != 1: errors['times'] = 'Die Anzahl muss 1 sein, wenn das Habit täglich ist.' if unit == 'Woche' and times > 7: errors['times'] = 'Die Anzahl darf höchstens 7 sein, wenn das Habit wöchentlich ist.' if unit == 'Monat' and times > 31: errors['times'] = 'Die Anzahl darf höchstens 31 sein, wenn das Habit monatlich ist.' if unit == 'Jahr' and times > 365: errors['times'] = 'Die Anzahl darf höchstens 365 sein, wenn das Habit jährlich ist.' if errors: return render_template( "edit-habit.html", title=habit.name, habit=habit.id, name=habit.name, note=habit.note, times=habit.times, unit=units[habit.unit], errors=errors ) # Map unit to integer if unit == 'Tag': unit = 0 elif unit == 'Woche': unit = 1 elif unit == 'Monat': unit = 2 elif unit == 'Jahr': unit = 3 else: unit = 1 # Save habit to database print(name, note, times, unit) habit.name, habit.note, habit.times, habit.unit = name, note, times, unit habit.update() # Back to index return redirect(url_for('index')) @app.route('/check', methods=['POST']) @login_required def check_habit(): habit_id = request.get_json()["habitId"] habit = Habit.get(habit_id) if habit is None: return {"error": "Habit not found"} # Check if habit belongs to user users = habit.habit_list().get_users() if current_user not in users: return {"error": "Habit does not belong to user"} trackings = habit.get_habitTrackings() # Check if habit has been tracked today delete_tracking = None for tracking in trackings: if tracking.created_at.date() == datetime.date.today(): delete_tracking = tracking if not delete_tracking: HabitTracking.create(habit_id) habit.fill_statistics() else: delete_tracking.delete() habit.reset_statistics() habit.load_statistics() heatmap_values, day = current_user.get_heatmap() return { "habitId": habit_id, "unchecked": not delete_tracking, "percentage": habit.percentage, "streak": habit.streak, "heatmap": heatmap_values, "day": day, } @app.route('/delete', methods=['POST']) @login_required def delete_habit(): habit_id = request.get_json()["habitId"] habit = Habit.get(habit_id) if habit is None: return {"error": "Habit not found"} # Check if habit belongs to user if current_user not in habit.habit_list().get_users(): return {"error": "Habit does not belong to user"} habit.delete() return {} @app.route('/reorder-habit', methods=['POST']) @login_required def reorder_habits(): new_index = request.get_json()["newIndex"] + 1 habit = Habit.get(request.get_json()["habitId"]) if habit is None: return {"error": "Habit not found"} # Check if habit belongs to user users = habit.habit_list().get_users() if current_user not in users: return {"error": "Habit does not belong to user"} habit.update_slot(new_index) return {} ########################################################### ######################## HabitList ######################## @app.route('/habit-list') @login_required def habit_list_creation(): return render_template( 'habit-list.html', title='Erstelle eine Habitliste', errors={}, ) @app.route('/habit-list', methods=['POST']) @login_required def habit_list_create(): name = request.form.get('name') description = request.form.get('description') # Check for errors errors = {} if not name: errors['name'] = 'Der Name ist erforderlich.' if not description: description = '' if errors: return render_template( 'habit-list.html', title='Erstelle eine Habitliste', name=name, description=description, errors=errors ) # Save habit to database HabitList.create(current_user.id, name, description) # Back to index return redirect(url_for('index')) @app.route('/delete-list', methods=['POST']) @login_required def delete_list(): list_id = request.get_json()["listId"] habit_list = HabitList.get(list_id) if habit_list is None: return {"error": "List not found"} # Check if habit belongs to user if current_user not in habit_list.get_users(): return {"error": "List does not belong to user"} habit_list.delete(current_user.id) return {} @app.route('/users') @login_required def users(): habit_list_id = request.args.get('habit_list', current_user.get_habitLists()[0].id) habit_list = HabitList.get(int(habit_list_id)) users = habit_list.get_users() # Remove the current user from the list users = [user for user in users if user.id != current_user.id] return render_template( 'users.html', title='Teilnehmer', habit_list=habit_list, users=users, errors={}, ) @app.route('/users', methods=['POST']) @login_required def add_user(): email = request.form.get('email') habit_list_id = request.form.get('habit_list_id') habit_list = HabitList.get(int(habit_list_id)) # Check for errors errors = {} if not email: errors['email'] = 'Die E-Mail Adresse ist erforderlich.' if not habit_list_id: errors['habit_list'] = 'Die Habitliste ist erforderlich.' if errors: return render_template( 'users.html', title='Teilnehmer', email=email, habit_list=habit_list, errors=errors, users=habit_list.get_users(), ) # Check if user exists user = User.get_by_email(email) if not user: errors['email'] = 'E-Mail Adresse nicht gefunden.' if user and user.id == current_user.id: errors['email'] = 'Du kannst dich nicht selbst hinzufügen.' # Check if user is already in the habit list already = False if user: for u in habit_list.get_users(): if u.id == user.id: already = True break if already: errors['email'] = 'Teilnehmer ist bereits in der Liste.' if errors: return render_template( 'users.html', title='Teilnehmer', email=email, habit_list=habit_list, errors=errors, users=habit_list.get_users(), ) # Add user to habit list habit_list = HabitList.get(int(habit_list_id)) habit_list.add_user(user) return redirect(url_for('index', habit_list=habit_list.id)) @app.route('/users-edit') @login_required def edit_users(): habit_list_id = request.args.get('habit_list', current_user.get_habitLists()[0].id) habit_list = HabitList.get(int(habit_list_id)) users = habit_list.get_users() # Remove the current user from the list users = [user for user in users if user.id != current_user.id] return render_template( 'users-edit.html', title='Teilnehmer bearbeiten', habit_list=habit_list, users=users, errors={}, ) @app.route('/user-delete', methods=['POST']) @login_required def delete_user_from_list(): habit_list_id = request.form.get('habit_list_id') habit_list = HabitList.get(int(habit_list_id)) habit_user_id = request.form.get('habit_user_id') users = habit_list.get_users() # Remove the current user from the list users = [user for user in users if user.id != current_user.id] # Check for errors errors = {} if not habit_list_id: errors['habit_list'] = 'Die Habitliste ist erforderlich.' if not habit_list_id: errors['habit_user'] = 'Ein User ist erforderlich.' if errors: return render_template( 'users-edit.html', title='Teilnehmer bearbeiten', habit_list=habit_list, users=users, errors={}, ) # delete user from habit list id = int(habit_user_id) habit_list.delete(id) return render_template( 'users-edit.html', title='Teilnehmer bearbeiten', habit_list=habit_list, users=users, errors={}, ) @app.route('/users-leave') @login_required def user_leave(): list_id = request.args.get('habit_list') habit_list = HabitList.get(list_id) if habit_list is None: return {"error": "List not found"} # Check if habit belongs to user if current_user not in habit_list.get_users(): return {"error": "List does not belong to user"} habit_list.remove_user(current_user.id) return redirect(url_for("index")) @app.route('/accept-list', methods=['POST']) @login_required def accept_list(): list_id = request.json.get('list_id') habit_list = HabitList.get(int(list_id)) users = habit_list.get_users() # Check if user is part of the list found = False for user in users: if user.id == current_user.id: found = True break if not found: return {} current_user.accept_List(habit_list.id) return {} @app.route('/deny-list', methods=['POST']) @login_required def deny_list(): list_id = request.json.get('list_id') habit_list = HabitList.get(int(list_id)) users = habit_list.get_users() # Check if user is part of the list found = False for user in users: if user.id == current_user.id: found = True break if not found: return {} habit_list.remove_user(current_user.id) return {} @app.route('/reorder-list', methods=['POST']) @login_required def reorder_habit_list(): new_index = request.get_json()["newIndex"] + 1 habitList = HabitList.get(request.get_json()["habitListId"]) if habitList is None: return {"error": "HabitList not found"} # Check if habit belongs to user users = habitList.get_users() if current_user not in users: return {"error": "HabitList does not belong to user"} habitList.update_slot(new_index) return {} ########################################################### ######################### Profile ######################### @app.route('/profile') @login_required def profile(): return render_template( "profile.html", name=current_user.name, email=current_user.email, profile_image_url=current_user.profile_image, color = current_user.heatmap_color, title="Profil", ) @app.route('/profile', methods=['POST']) @login_required def profile_change(): newName = request.form.get('newName') newEmail = request.form.get('newEmail') # Update user current_user.name = newName current_user.email = newEmail current_user.update() # Back to profile return render_template( "profile.html", name=current_user.name, email=current_user.email, profile_image_url=current_user.profile_image, color=current_user.heatmap_color, ) @app.route('/check_password', methods=['POST']) @login_required def check_password(): # Get the password sent from the client password = request.json.get('password') if hashlib.sha256(password.encode()).hexdigest() == current_user.password: return jsonify({"valid": True}) else: return jsonify({"valid": False}) @app.route('/password', methods=['POST']) @login_required def password_change(): newPassword = request.form.get('newPassword') # Update user if newPassword: current_user.password = hashlib.sha256(newPassword.encode()).hexdigest() current_user.update() # Back to profile return render_template( "profile.html", name=current_user.name, email=current_user.email, profile_image_url=current_user.profile_image, color=current_user.heatmap_color, ) def save_profile_image(image_file): filename = image_file.filename if '.' not in filename: # Ensure the filename has an extension raise ValueError("Invalid filename") # Check if the file extension is allowed allowed_extensions = {'jpg', 'jpeg', 'png', 'gif'} file_extension = filename.rsplit('.', 1)[1].lower() if file_extension not in allowed_extensions: raise ValueError("Invalid file extension") # Get the filename from the image path saved in the user filename = os.path.basename(current_user.profile_image) # Open the uploaded image image = Image.open(image_file) # Function to crop and resize frames def process_frame(frame, size): new_size = min(frame.size) left = (frame.width - new_size) // 2 top = (frame.height - new_size) // 2 right = left + new_size bottom = top + new_size cropped_frame = frame.crop((left, top, right, bottom)) return cropped_frame.resize(size) # Function to process frames in parallel def process_frames_parallel(frames, size): with concurrent.futures.ThreadPoolExecutor() as executor: resized_frames = list(executor.map(lambda f: process_frame(f, size), frames)) return resized_frames # Check if the image is an animated gif if file_extension == 'gif': # Process frames gif_frames = [frame.copy() for frame in ImageSequence.Iterator(image)] processed_frames = process_frames_parallel(gif_frames, size=(128, 128)) # Save the modified frames as a new GIF output_gif_path = os.path.join(app.config['UPLOAD_FOLDER'], filename.replace(".jpg", ".gif")) processed_frames[0].save(output_gif_path, save_all=True, append_images=processed_frames[1:], loop=0) current_user.profile_image = output_gif_path else: # Process single image processed_image = process_frame(image, size=(256, 256)) processed_image = processed_image.convert('RGB') # Save the processed image image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename.replace(".gif", ".jpg")) processed_image.save(image_path, 'JPEG', quality=100) current_user.profile_image = image_path # Update user profile current_user.update() @app.route('/upload', methods=['POST']) @login_required def upload_profile_image(): if 'file' not in request.files: return 'No file part' file = request.files['file'] save_profile_image(file) # Back to profile return redirect(url_for('profile')) @app.route('/save_color', methods=['POST']) @login_required def save_heatmap_color(): # Get the color value from the form new_color = request.form['color'] current_user.heatmap_color = new_color current_user.update() # Back to profile return render_template( "profile.html", name=current_user.name, email=current_user.email, profile_image_url=current_user.profile_image, color=current_user.heatmap_color, ) @app.route('/delete_account', methods=['POST']) @login_required def delete_account(): os.remove(current_user.profile_image) current_user.delete() return redirect(url_for('index')) ########################################################### # Run the application if __name__ == '__main__': app.run(host="0.0.0.0", port=5000, debug=True)