from flask import Flask, render_template, redirect, url_for, request, flash, session from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, login_user, login_required, logout_user, current_user, UserMixin, AnonymousUserMixin import os from werkzeug.security import generate_password_hash, check_password_hash from flask import send_from_directory from werkzeug.utils import secure_filename from flask_migrate import Migrate import uuid UPLOAD_FOLDER = 'static/uploads' ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} app = Flask(__name__) app.config['SECRET_KEY'] = 'your_secret_key_here' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///liturgie.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True) db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = 'login' migrate = Migrate(app, db) # Models class Church(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(150), unique=True, nullable=False) is_active = db.Column(db.Boolean, default=False, nullable=False) # New field for activation state contact_email = db.Column(db.String(150), default='') contact_phone = db.Column(db.String(50), default='') contact_address = db.Column(db.String(200), default='') logo_filename = db.Column(db.String(300), nullable=True) # New field for logo filename users = db.relationship('User', backref='church', lazy=True) boards = db.relationship('Liturgiebord', backref='church', lazy=True) class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(150), unique=True, nullable=False) password = db.Column(db.String(150), nullable=False) church_id = db.Column(db.Integer, db.ForeignKey('church.id'), nullable=False) is_admin = db.Column(db.Boolean, default=False) # Association table for many-to-many relationship board_schedule_association = db.Table('board_schedule', db.Column('board_id', db.Integer, db.ForeignKey('liturgiebord.id'), primary_key=True), db.Column('schedule_id', db.Integer, db.ForeignKey('schedule.id'), primary_key=True) ) class Liturgiebord(db.Model): id = db.Column(db.Integer, primary_key=True) unique_id = db.Column(db.String(32), unique=True, nullable=False, default=lambda: uuid.uuid4().hex) name = db.Column(db.String(150), nullable=False) church_id = db.Column(db.Integer, db.ForeignKey('church.id'), nullable=False) line1 = db.Column(db.String(200), default='') line2 = db.Column(db.String(200), default='') line3 = db.Column(db.String(200), default='') line4 = db.Column(db.String(200), default='') line5 = db.Column(db.String(200), default='') line6 = db.Column(db.String(200), default='') line7 = db.Column(db.String(200), default='') line8 = db.Column(db.String(200), default='') line9 = db.Column(db.String(200), default='') line10 = db.Column(db.String(200), default='') background_image = db.Column(db.String(300), default=None) # Updated relationship: many-to-many schedules = db.relationship('Schedule', secondary=board_schedule_association, back_populates='boards', lazy='dynamic') class Schedule(db.Model): id = db.Column(db.Integer, primary_key=True) # Removed board_id to make schedules global name = db.Column(db.String(150), nullable=False) start_time = db.Column(db.Time, nullable=False) end_time = db.Column(db.Time, nullable=False) date = db.Column(db.Date, nullable=False) content = db.Column(db.Text, nullable=False) # separate lines with \n # Many-to-many relationship back to boards boards = db.relationship('Liturgiebord', secondary=board_schedule_association, back_populates='schedules', lazy='dynamic') @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) from flask_login import current_user @app.route('/') def index(): if current_user.is_authenticated: return redirect(url_for('portal')) return render_template('index.html') # --- Church Settings Route --- from werkzeug.security import generate_password_hash from werkzeug.utils import secure_filename @app.route('/church/settings', methods=['GET', 'POST']) @login_required def church_settings(): church = current_user.church msg = None if request.method == 'POST' and request.form.get('form_type') == 'contact': church.name = request.form.get('name', '').strip() church.contact_email = request.form.get('contact_email', '').strip() church.contact_phone = request.form.get('contact_phone', '').strip() church.contact_address = request.form.get('contact_address', '').strip() # Handle logo upload if 'logo' in request.files: logo = request.files['logo'] if logo and allowed_file(logo.filename): # Remove old logo file if exists if church.logo_filename: old_filepath = os.path.join(app.config['UPLOAD_FOLDER'], church.logo_filename) if os.path.exists(old_filepath): os.remove(old_filepath) # Create a unique filename import time filename = secure_filename(f'church_{church.id}_logo_{int(time.time())}_{logo.filename}') filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) logo.save(filepath) church.logo_filename = filename db.session.commit() msg = 'Kerkgegevens opgeslagen!' elif request.method == 'POST' and request.form.get('form_type') == 'user': username = request.form.get('username', '').strip() password = request.form.get('password', '').strip() if not username or not password: msg = 'Gebruikersnaam en wachtwoord zijn verplicht.' elif User.query.filter_by(username=username).first(): msg = 'Gebruikersnaam bestaat al.' else: user = User(username=username, password=generate_password_hash(password), church_id=church.id) db.session.add(user) db.session.commit() msg = f'Gebruiker {username} toegevoegd aan deze kerk.' users = church.users return render_template('church_settings.html', church=church, users=users, msg=msg) # Placeholder routes for login, register, church portal, board management, and display @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('portal')) if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = User.query.filter_by(username=username).first() if user and check_password_hash(user.password, password): login_user(user) return redirect(url_for('portal')) else: flash('Ongeldige gebruikersnaam of wachtwoord') return render_template('login.html') @app.route('/church/delete_user/', methods=['POST']) @login_required def church_delete_user(user_id): user_to_delete = User.query.get_or_404(user_id) # Confirm user belongs to the same church if user_to_delete.church_id != current_user.church_id: flash('Geen toegang tot deze gebruiker') return redirect(url_for('church_settings')) # Disallow deleting self if user_to_delete.id == current_user.id: flash('Je kunt jezelf niet verwijderen') return redirect(url_for('church_settings')) # Disallow deleting admin users if user_to_delete.is_admin: flash('Kan admin-gebruiker niet verwijderen') return redirect(url_for('church_settings')) db.session.delete(user_to_delete) db.session.commit() flash(f'Gebruiker {user_to_delete.username} verwijderd!') return redirect(url_for('church_settings')) @app.route('/logout') @login_required def logout(): session.pop('impersonate_id', None) session.pop('admin_id', None) logout_user() return redirect(url_for('login')) @app.route('/register', methods=['GET', 'POST']) def register(): import re if current_user.is_authenticated: return redirect(url_for('portal')) if request.method == 'POST': email = request.form['username'].strip() password = request.form['password'] church_name = request.form['church'].strip() # Validate basic email format if not re.match(r"[^@]+@[^@]+\.[^@]+", email): flash('Voer een geldig e-mailadres in') return render_template('register.html') if User.query.filter_by(username=email).first(): flash('Emailadres bestaat al') return render_template('register.html') church = Church.query.filter_by(name=church_name).first() if church: # Prevent registering to an existing church to avoid security risk flash('Het church name bestaat al. Maak een unieke church aan.') return render_template('register.html') else: church = Church(name=church_name, is_active=False) # New churches start inactive db.session.add(church) db.session.commit() hashed_pw = generate_password_hash(password) user = User(username=email, password=hashed_pw, church_id=church.id) db.session.add(user) db.session.commit() flash('Registratie gelukt! Je kunt nu inloggen.') return redirect(url_for('login')) return render_template('register.html') @app.route('/portal') @login_required def portal(): boards = Liturgiebord.query.filter_by(church_id=current_user.church_id).all() return render_template('portal.html', boards=boards) @app.route('/board/add', methods=['GET', 'POST']) @login_required def add_board(): if request.method == 'POST': name = request.form.get('name') board = Liturgiebord(name=name, church_id=current_user.church_id) db.session.add(board) db.session.commit() return redirect(url_for('portal')) return render_template('add_board.html') @app.route('/board//edit', methods=['GET', 'POST']) @login_required def edit_board(board_id): board = Liturgiebord.query.get_or_404(board_id) if board.church_id != current_user.church_id: flash('Geen toegang tot dit bord') return redirect(url_for('portal')) if request.method == 'POST': # If changing the name if request.form.get('form_type') == 'name': new_name = request.form.get('name', '').strip() if new_name: board.name = new_name db.session.commit() flash('Naam van het bord gewijzigd!') else: flash('Naam mag niet leeg zijn.') return redirect(url_for('edit_board', board_id=board.id)) # Otherwise, update lines for i in range(1, 11): setattr(board, f'line{i}', request.form.get(f'line{i}', '')) db.session.commit() flash('Bord bijgewerkt!') return redirect(url_for('portal')) # Get schedules assigned to this board schedules = board.schedules.order_by(Schedule.date.desc(), Schedule.start_time).all() # Find the active schedule object active_schedule = None now = datetime.now() today = now.date() current_time = now.time() for schedule in schedules: if schedule.date == today and schedule.start_time <= current_time <= schedule.end_time: active_schedule = schedule break schedule_active = active_schedule is not None schedule_content = None if schedule_active: schedule_content = active_schedule.content return render_template('edit_board.html', board=board, schedules=schedules, schedule_active=schedule_active, active_schedule=active_schedule, schedule_content=schedule_content) from datetime import datetime # Helper to get active schedule for a board from sqlalchemy import and_, or_ def get_active_schedule(board): now = datetime.now() today = now.date() current_time = now.time() for schedule in board.schedules: if schedule.date != today: continue if not (schedule.start_time <= current_time <= schedule.end_time): continue return schedule.content return None @app.route('/board/') def display_board(board_id): board = Liturgiebord.query.get_or_404(board_id) schedule_content = get_active_schedule(board) return render_template('display_board.html', board=board, schedule_content=schedule_content) from flask import jsonify @app.route('/board//active_schedule_json') def active_schedule_json(board_id): board = Liturgiebord.query.get_or_404(board_id) schedule_content = get_active_schedule(board) # Return schedule lines if active, else board input lines if schedule_content: lines = schedule_content.split('\n') else: # Use board lines if no active schedule lines = [ board.line1 or ' ', board.line2 or ' ', board.line3 or ' ', board.line4 or ' ', board.line5 or ' ', board.line6 or ' ', board.line7 or ' ', board.line8 or ' ', board.line9 or ' ', board.line10 or ' ' ] # Also include background info for completeness bg_url = None if board.background_image: # Determine URL path, check if default or uploaded from flask import url_for if board.background_image.startswith('default_'): bg_url = url_for('static', filename=board.background_image) else: bg_url = url_for('uploaded_file', filename=board.background_image) return jsonify({ 'lines': lines, 'background': bg_url }) @app.route('/board//delete', methods=['POST']) @login_required def delete_board(board_id): board = Liturgiebord.query.get_or_404(board_id) if board.church_id != current_user.church_id and not current_user.is_admin: flash('Geen toegang tot dit bord') return redirect(url_for('portal')) # Remove schedule associations but keep schedules intact board.schedules = [] db.session.commit() # Then delete the board db.session.delete(board) db.session.commit() flash('Liturgiebord verwijderd!') return redirect(url_for('portal')) from flask import abort # Remove old per-board schedule management routes @app.route('/schedules') @login_required def global_schedules(): user_church_id = current_user.church_id if current_user.is_admin: # Admin sees all schedules schedules = Schedule.query.order_by(Schedule.date.desc(), Schedule.start_time).all() else: # Non-admin: show only schedules linked to boards of their church schedules = [] boards = Liturgiebord.query.filter_by(church_id=user_church_id).all() board_ids = [b.id for b in boards] all_schedules = Schedule.query.order_by(Schedule.date.desc(), Schedule.start_time).all() for schedule in all_schedules: # Check if schedule belongs to any board_id of current church for board in schedule.boards: if board.id in board_ids: schedules.append(schedule) break boards = Liturgiebord.query.filter_by(church_id=user_church_id).all() return render_template('global_schedules.html', schedules=schedules, boards=boards) @app.route('/schedules/add', methods=['GET', 'POST']) @login_required def add_global_schedule(): user_church_id = current_user.church_id boards = Liturgiebord.query.filter_by(church_id=user_church_id).all() if request.method == 'POST': from datetime import datetime sched_date = datetime.strptime(request.form['date'], '%Y-%m-%d').date() start_time = datetime.strptime(request.form['start_time'], '%H:%M').time() end_time = datetime.strptime(request.form['end_time'], '%H:%M').time() lines = [] for i in range(1, 11): line = request.form.get(f'line{i}', '').strip() if line == '': line = ' ' lines.append(line) content = '\n'.join(lines) name = request.form['name'] board_ids = request.form.getlist('boards') schedule_obj = Schedule( name=name, start_time=start_time, end_time=end_time, content=content, date=sched_date ) # Assign boards for board_id in board_ids: board = Liturgiebord.query.get(int(board_id)) if board and (board.church_id == user_church_id or current_user.is_admin): schedule_obj.boards.append(board) db.session.add(schedule_obj) db.session.commit() flash('Nieuwe planning toegevoegd.') return redirect(url_for('global_schedules')) return render_template('schedule_form.html', schedule=None, boards=boards) @app.route('/schedules//edit', methods=['GET', 'POST']) @login_required def edit_global_schedule(schedule_id): user_church_id = current_user.church_id schedule = Schedule.query.get_or_404(schedule_id) # Convert boards dynamic relationship to list for template schedule.boards_list = schedule.boards.all() # Check user has access via any board or admin accessible = False for board in schedule.boards_list: if board.church_id == user_church_id: accessible = True break if not (accessible or current_user.is_admin): abort(403) boards = Liturgiebord.query.filter_by(church_id=user_church_id).all() if request.method == 'POST': from datetime import datetime schedule.date = datetime.strptime(request.form['date'], '%Y-%m-%d').date() schedule.start_time = datetime.strptime(request.form['start_time'], '%H:%M').time() schedule.end_time = datetime.strptime(request.form['end_time'], '%H:%M').time() lines = [] for i in range(1, 11): line = request.form.get(f'line{i}', '').strip() if line == '': line = ' ' lines.append(line) schedule.content = '\n'.join(lines) schedule.name = request.form['name'] # Update board assignments board_ids = request.form.getlist('boards') schedule.boards = [] # Clear old assignments for board_id in board_ids: board = Liturgiebord.query.get(int(board_id)) if board and (board.church_id == user_church_id or current_user.is_admin): schedule.boards.append(board) db.session.commit() flash('Planning bijgewerkt.') return redirect(url_for('global_schedules')) return render_template('schedule_form.html', schedule=schedule, boards=boards) @app.route('/schedules//delete', methods=['POST']) @login_required def delete_global_schedule(schedule_id): schedule = Schedule.query.get_or_404(schedule_id) # Check user has access via any board or admin user_church_id = current_user.church_id accessible = False for board in schedule.boards: if board.church_id == user_church_id: accessible = True break if not (accessible or current_user.is_admin): abort(403) db.session.delete(schedule) db.session.commit() flash('Planning verwijderd.') return redirect(url_for('global_schedules')) def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/board//background', methods=['GET', 'POST']) @login_required def upload_background(board_id): board = Liturgiebord.query.get_or_404(board_id) if board.church_id != current_user.church_id: flash('Geen toegang tot dit bord') return redirect(url_for('portal')) if request.method == 'POST': # handle setting default wallpaper default_background = request.form.get('default_background') if default_background: # Set the default wallpaper filename directly # We assume the filename is safe and known board.background_image = default_background db.session.commit() flash('Achtergrondafbeelding ingesteld!') return redirect(url_for('edit_board', board_id=board.id)) if 'background' not in request.files: flash('Geen bestand geselecteerd') return redirect(request.url) file = request.files['background'] if file and allowed_file(file.filename): filename = secure_filename(f'board_{board.id}_bg_{file.filename}') filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) board.background_image = filename db.session.commit() flash('Achtergrondafbeelding bijgewerkt!') return redirect(url_for('edit_board', board_id=board.id)) else: flash('Ongeldig bestandstype') return render_template('upload_background.html', board=board) @app.route('/uploads/') def uploaded_file(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename) @app.route('/admin/login', methods=['GET', 'POST']) def admin_login(): if current_user.is_authenticated and current_user.is_admin: return redirect(url_for('admin_dashboard')) if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = User.query.filter_by(username=username, is_admin=True).first() if user and check_password_hash(user.password, password): login_user(user) return redirect(url_for('admin_dashboard')) else: flash('Ongeldige admin-inloggegevens') return render_template('admin_login.html') from flask import request @app.route('/admin/delete_church/', methods=['POST']) @login_required def admin_delete_church(church_id): if not current_user.is_admin: flash('Geen toegang') return redirect(url_for('portal')) church = Church.query.get_or_404(church_id) # Delete associated boards and users for board in church.boards: db.session.delete(board) for user in church.users: db.session.delete(user) db.session.delete(church) db.session.commit() flash(f'Kerk {church.name} en alle bijbehorende data verwijderd!') return redirect(url_for('admin_dashboard')) @app.route('/admin/dashboard', methods=['GET', 'POST']) @login_required def admin_dashboard(): if not current_user.is_admin: flash('Geen toegang') return redirect(url_for('portal')) if request.method == 'POST': # Update church activation states for church in Church.query.all(): checkbox_val = request.form.get(f'church_active_{church.id}') church.is_active = checkbox_val == 'on' db.session.commit() flash('Kerken activatiestatus bijgewerkt.') churches = Church.query.all() users = User.query.all() return render_template('admin_dashboard.html', churches=churches, users=users) @app.route('/admin/impersonate/') @login_required def admin_impersonate(user_id): if not current_user.is_admin: flash('Geen toegang') return redirect(url_for('portal')) user = User.query.get_or_404(user_id) session['impersonate_id'] = user.id session['admin_id'] = current_user.id flash(f'Je bent nu ingelogd als {user.username}') return redirect(url_for('portal')) @app.before_request def impersonate_user(): if 'impersonate_id' in session and 'admin_id' in session: # Only impersonate if the real admin is logged in admin = User.query.get(session['admin_id']) if admin and admin.is_authenticated and admin.is_admin: user = User.query.get(session['impersonate_id']) if user: # Use Flask-Login's login_user to switch context login_user(user) # Attach a proxy attribute to current_user to allow admin checks current_user._is_impersonated = True current_user._real_admin_id = admin.id else: # Not impersonating, ensure _is_impersonated is not set if hasattr(current_user, '_is_impersonated'): delattr(current_user, '_is_impersonated') if hasattr(current_user, '_real_admin_id'): delattr(current_user, '_real_admin_id') @app.route('/admin/users') @login_required def admin_users(): if not current_user.is_admin: flash('Geen toegang') return redirect(url_for('portal')) users = User.query.all() return render_template('admin_users.html', users=users) @app.route('/admin/delete_user/', methods=['POST']) @login_required def admin_delete_user(user_id): if not current_user.is_admin: flash('Geen toegang') return redirect(url_for('portal')) user = User.query.get_or_404(user_id) church = user.church if user.is_admin: flash('Kan geen admin-gebruiker verwijderen!') return redirect(url_for('admin_users')) if len(church.users) > 1: # Just delete the user db.session.delete(user) db.session.commit() flash(f'Gebruiker {user.username} verwijderd!') else: # Last user, delete user, church, boards for board in church.boards: db.session.delete(board) db.session.delete(user) db.session.delete(church) db.session.commit() flash(f'Laatste gebruiker verwijderd. Kerk en data ook verwijderd ({user.username}, {church.name})!') return redirect(url_for('admin_users')) @app.route('/display/') def display_board_unique(unique_id): board = Liturgiebord.query.filter_by(unique_id=unique_id).first_or_404() schedule_content = get_active_schedule(board) return render_template('display_board.html', board=board, schedule_content=schedule_content) from werkzeug.security import check_password_hash @app.route('/change_password', methods=['GET', 'POST']) @login_required def change_password(): msg = None if request.method == 'POST': current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') if not check_password_hash(current_user.password, current_password): msg = 'Huidig wachtwoord is onjuist.' elif not new_password or not confirm_password: msg = 'Voer een nieuw wachtwoord in.' elif new_password != confirm_password: msg = 'De nieuwe wachtwoorden komen niet overeen.' else: current_user.password = generate_password_hash(new_password) db.session.commit() msg = 'Wachtwoord succesvol gewijzigd.' return render_template('change_password.html', msg=msg) if __name__ == '__main__': if not os.path.exists('liturgie.db'): with app.app_context(): db.create_all() # Create initial admin user if not exists if not User.query.filter_by(username='admin').first(): admin_church = Church.query.filter_by(name='Admin').first() if not admin_church: admin_church = Church(name='Admin') db.session.add(admin_church) db.session.commit() admin_user = User(username='admin', password=generate_password_hash('admin'), church_id=admin_church.id, is_admin=True) db.session.add(admin_user) db.session.commit() app.run(host='0.0.0.0')