Initial commit
14
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Python File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
__pycache__/app.cpython-311.pyc
Normal file
7
ad.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from app import db, Liturgiebord
|
||||
import uuid
|
||||
|
||||
with db.app.app_context():
|
||||
for board in Liturgiebord.query.filter((Liturgiebord.unique_id == None) | (Liturgiebord.unique_id == '')).all():
|
||||
board.unique_id = uuid.uuid4().hex
|
||||
db.session.commit()
|
||||
692
app.py
Normal file
@@ -0,0 +1,692 @@
|
||||
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/<int:user_id>', 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/<int:board_id>/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/<int:board_id>')
|
||||
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/<int:board_id>/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/<int:board_id>/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/<int:schedule_id>/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/<int:schedule_id>/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/<int:board_id>/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/<filename>')
|
||||
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/<int:church_id>', 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/<int:user_id>')
|
||||
@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/<int:user_id>', 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/<unique_id>')
|
||||
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')
|
||||
17
cgi-bin/flask_cgi.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
import cgitb; cgitb.enable()
|
||||
import os
|
||||
import sys
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
# Path to your app.py
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
# Import your Flask app
|
||||
import app
|
||||
|
||||
# Run the WSGI server
|
||||
if __name__ == '__main__':
|
||||
# Cottage-style server for CGI
|
||||
from wsgiref.handlers import CGIHandler
|
||||
CGIHandler().run(app.app)
|
||||
BIN
instance/liturgie.db
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
||||
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
50
migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
113
migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions['migrate'].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions['migrate'].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||
'%', '%%')
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace('%', '%%')
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||
target_db = current_app.extensions['migrate'].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, 'metadatas'):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
conf_args = current_app.extensions['migrate'].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
**conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
88
migrations/versions/168fdd980dba_initial_migration.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: 168fdd980dba
|
||||
Revises:
|
||||
Create Date: 2025-07-31 10:56:01.296387
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '168fdd980dba'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('church',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('contact_email', sa.String(length=150), nullable=True),
|
||||
sa.Column('contact_phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('contact_address', sa.String(length=200), nullable=True),
|
||||
sa.Column('logo_filename', sa.String(length=300), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('schedule',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('start_time', sa.Time(), nullable=False),
|
||||
sa.Column('end_time', sa.Time(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('liturgiebord',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('unique_id', sa.String(length=32), nullable=False),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('church_id', sa.Integer(), nullable=False),
|
||||
sa.Column('line1', sa.String(length=200), nullable=True),
|
||||
sa.Column('line2', sa.String(length=200), nullable=True),
|
||||
sa.Column('line3', sa.String(length=200), nullable=True),
|
||||
sa.Column('line4', sa.String(length=200), nullable=True),
|
||||
sa.Column('line5', sa.String(length=200), nullable=True),
|
||||
sa.Column('line6', sa.String(length=200), nullable=True),
|
||||
sa.Column('line7', sa.String(length=200), nullable=True),
|
||||
sa.Column('line8', sa.String(length=200), nullable=True),
|
||||
sa.Column('line9', sa.String(length=200), nullable=True),
|
||||
sa.Column('line10', sa.String(length=200), nullable=True),
|
||||
sa.Column('background_image', sa.String(length=300), nullable=True),
|
||||
sa.ForeignKeyConstraint(['church_id'], ['church.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('unique_id')
|
||||
)
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=150), nullable=False),
|
||||
sa.Column('password', sa.String(length=150), nullable=False),
|
||||
sa.Column('church_id', sa.Integer(), nullable=False),
|
||||
sa.Column('is_admin', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['church_id'], ['church.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_table('board_schedule',
|
||||
sa.Column('board_id', sa.Integer(), nullable=False),
|
||||
sa.Column('schedule_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['board_id'], ['liturgiebord.id'], ),
|
||||
sa.ForeignKeyConstraint(['schedule_id'], ['schedule.id'], ),
|
||||
sa.PrimaryKeyConstraint('board_id', 'schedule_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('board_schedule')
|
||||
op.drop_table('user')
|
||||
op.drop_table('liturgiebord')
|
||||
op.drop_table('schedule')
|
||||
op.drop_table('church')
|
||||
# ### end Alembic commands ###
|
||||
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Flask
|
||||
Flask_SQLAlchemy
|
||||
Flask_Login
|
||||
flask_migrate
|
||||
Werkzeug
|
||||
SQLAlchemy
|
||||
itsdangerous
|
||||
python-dotenv
|
||||
BIN
static/Hero.jpg
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
static/cropped-cropped-CGKZwolle_Logo-verticaal-rood.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/default_logo.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
static/default_wall_1.jpg
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
static/default_wall_2.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
static/default_wall_3.jpg
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
static/default_wall_4.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/favicon.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/icons/board.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/icons/church.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/icons/cog.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
static/icons/group.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/icons/task.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/uploads/board_10_bg_Bubinga-QC.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
static/uploads/board_1_bg_Bubinga-QC.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
static/uploads/board_1_bg_Philips-Keterlmeer.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
static/uploads/board_1_bg_decarbonizing-hrt.JPG
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
static/uploads/board_1_bg_default_wall_1.jpg.jpg
Normal file
|
After Width: | Height: | Size: 472 KiB |
BIN
static/uploads/board_1_bg_default_wall_4.jpg.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
static/uploads/board_2_bg_Bubinga-QC.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
static/uploads/board_2_bg_DSCF8453.JPG
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
static/uploads/board_3_bg_yealink-meetingboard-pro-1.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
static/uploads/board_5_bg_Bubinga-QC.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
static/uploads/board_9_bg_Bubinga-QC.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
static/uploads/board_9_bg_DSCF9911_1.jpg
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
static/uploads/board_9_bg_yealink-meetingboard-pro-1.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
static/uploads/church_1_logo_1754068453_default_logo.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
static/uploads/church_1_logo_Green_Modern_Tree_Logo_Design_1.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
static/uploads/church_1_logo_defaut_logo.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/uploads/church_1_logo_images_2.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/uploads/church_1_logo_logo-rood.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/uploads/church_1_logo_path2.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/uploads/church_2_logo_unnamed.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
27
templates/add_board.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Nieuw Liturgiebord Toevoegen - Digitale Liturgie{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
|
||||
<h2 class="text-3xl font-semibold mb-6">Nieuw Liturgiebord Toevoegen</h2>
|
||||
|
||||
<form method="post" class="space-y-6 max-w-xl">
|
||||
|
||||
<div>
|
||||
<label for="name" class="block mb-3 font-semibold text-gray-700">Naam van het bord</label>
|
||||
<input type="text" id="name" name="name" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" class="bg-[#f7d91a] text-black px-8 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Toevoegen</button>
|
||||
<a href="{{ url_for('portal') }}" class="bg-white text-black border border-black px-8 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition">Annuleren</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
107
templates/admin_dashboard.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Admin Dashboard - Digitale Liturgie{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
|
||||
<section>
|
||||
|
||||
<h2 class="text-3xl font-semibold mb-6">Admin Dashboard</h2>
|
||||
|
||||
<form method="POST" class="space-y-6">
|
||||
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold mb-4">Kerken</h3>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Kerknaam</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Actief</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">URL</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for church in churches %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ church.name }}</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
<input type="checkbox" name="church_active_{{ church.id }}" {% if church.is_active %}checked{% endif %} class="h-5 w-5 text-blue-600">
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-blue-600">
|
||||
{% if church.is_active %}
|
||||
{% for board in church.boards %}
|
||||
<div class="mb-1 break-all"><a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" target="_blank" class="underline hover:text-blue-800 transition">{{ url_for('display_board_unique', unique_id=board.unique_id, _external=True) }}</a></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
<a href="#" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Weergeven</a>
|
||||
<form action="{{ url_for('admin_delete_church', church_id=church.id) }}" method="post" style="display:inline;margin-left:8px;" onsubmit="return confirm('Weet je zeker dat je deze kerk en alle bijbehorende data wilt verwijderen?');">
|
||||
<button type="submit" class="bg-red-600 text-white px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
<h3 class="text-2xl font-semibold mb-4">Gebruikers</h3>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
|
||||
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Kerk</th>
|
||||
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Admin</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Impersonalisatie</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ user.username }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">{{ user.church.name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">{% if user.is_admin %}Ja{% else %}Nee{% endif %}</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
{% if not user.is_admin %}
|
||||
<a href="{{ url_for('admin_impersonate', user_id=user.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Impersonate</a>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
{% if not user.is_admin %}
|
||||
<form action="{{ url_for('admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze gebruiker en alle bijbehorende kerkdata wilt verwijderen?');">
|
||||
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
|
||||
</form>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
18
templates/admin_login.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Admin Login - Digitale Liturgie{% endblock %}
|
||||
{% block content %}
|
||||
<div class="space-y-8 max-w-md mx-auto">
|
||||
<h2 class="text-3xl font-semibold tracking-tight">Admin Login</h2>
|
||||
<form method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block mb-2 font-semibold text-gray-700">Gebruikersnaam</label>
|
||||
<input type="text" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block mb-2 font-semibold text-gray-700">Wachtwoord</label>
|
||||
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
<button type="submit" class="bg-blue-600 text-white w-full rounded-md py-3 font-semibold shadow hover:bg-blue-700 transition">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
50
templates/admin_users.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Gebruikersbeheer - Digitale Liturgie{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
|
||||
<h2 class="text-3xl font-semibold mb-6">Gebruikersbeheer</h2>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
|
||||
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Kerk</th>
|
||||
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Admin</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Impersonalisatie</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ user.username }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">{{ user.church.name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">{% if user.is_admin %}Ja{% else %}Nee{% endif %}</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
{% if not user.is_admin %}
|
||||
<a href="{{ url_for('admin_impersonate', user_id=user.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Impersonate</a>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
{% if not user.is_admin %}
|
||||
<form action="{{ url_for('admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze gebruiker en alle bijbehorende kerkdata wilt verwijderen?');">
|
||||
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
|
||||
</form>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block mt-6">Terug naar dashboard</a>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
151
templates/base.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}PsalmbordOnline{% endblock %}</title>
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lora&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Quicksand:wght@300..700&display=swap" rel="stylesheet">
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Lora', serif;
|
||||
font-weight: 600;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Quicksand';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Floating scrollbar style for WebKit browsers */
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Webkit Floating Scrollbar */
|
||||
body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: transparent;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Firefox Floating scrollbar */
|
||||
@-moz-document url-prefix() {
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-white text-gray-900 font-sans min-h-screen flex flex-col">
|
||||
<nav class="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-20">
|
||||
<!-- Logo -->
|
||||
<div class="flex-shrink-0 flex items-center w-24">
|
||||
{% if current_user.is_authenticated and current_user.church and current_user.church.logo_filename %}
|
||||
<a href="{{ url_for('index') }}"><img src="{{ url_for('uploaded_file', filename=current_user.church.logo_filename) }}" alt="Logo" class="h-10 w-auto object-contain"></a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('index') }}"><img src="{{ url_for('static', filename='default_logo.png') }}" alt="Default Logo" class="h-10 w-auto object-contain"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Desktop Nav -->
|
||||
<div class="hidden sm:flex flex-1 justify-center">
|
||||
<div id="nav-links" class="flex flex-row gap-2 items-center">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/portal" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Mijn schermen</a>
|
||||
<a href="{{ url_for('global_schedules') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Geplande diensten</a>
|
||||
<a href="{{ url_for('church_settings') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Instellingen</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Admin</a>
|
||||
{% endif %}
|
||||
<a href="/logout" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Uitloggen</a>
|
||||
{% else %}
|
||||
<a href="/login" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Inloggen</a>
|
||||
<a href="/register" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Registreren</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Current Time -->
|
||||
<div id="current-time" class="text-gray-700 font-semibold text-right text-sm w-24" title="Huidige datum en tijd">
|
||||
<div id="current-date"></div>
|
||||
<div id="current-clock" class="mt-0"></div>
|
||||
</div>
|
||||
<!-- Mobile hamburger -->
|
||||
<div class="flex sm:hidden">
|
||||
<button id="menu-button" class="p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-600" aria-label="Open menu">
|
||||
<svg class="h-7 w-7 text-gray-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile nav menu -->
|
||||
<div id="nav-mobile" class="sm:hidden hidden">
|
||||
<div class="flex flex-col gap-2 py-3 items-center border-t border-gray-100">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/portal" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Mijn schermen</a>
|
||||
<a href="{{ url_for('global_schedules') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Geplande diensten</a>
|
||||
<a href="{{ url_for('church_settings') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Kerk instellingen</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Admin instellingen</a>
|
||||
{% endif %}
|
||||
<a href="/logout" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Logout</a>
|
||||
{% else %}
|
||||
<a href="/login" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Login</a>
|
||||
<a href="/register" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script>
|
||||
const menuButton = document.getElementById('menu-button');
|
||||
const navMobile = document.getElementById('nav-mobile');
|
||||
|
||||
menuButton.addEventListener('click', () => {
|
||||
navMobile.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const dateOptions = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||
const timeOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
|
||||
const dateStr = now.toLocaleDateString('nl-NL', dateOptions);
|
||||
const timeStr = now.toLocaleTimeString('nl-NL', timeOptions);
|
||||
document.getElementById('current-date').textContent = dateStr;
|
||||
document.getElementById('current-clock').textContent = timeStr;
|
||||
}
|
||||
|
||||
setInterval(updateTime, 1000);
|
||||
updateTime();
|
||||
</script>
|
||||
<main class="flex-grow max-w-7xl mx-auto w-full px-6 py-8 sm:py-12">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">
|
||||
{% for message in messages %}
|
||||
<div>{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer class="bg-gray-100 text-gray-600 text-center py-4 text-sm">
|
||||
© {{ current_year }} PsalmbordOnline | Contact: <a href="mailto:info@psalmbord.online" class="text-blue-600 hover:underline">info@psalmbord.online</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
48
templates/board_details.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
|
||||
<h1 class="text-3xl font-semibold mb-6">Details voor {{ board.name }}</h1>
|
||||
|
||||
<!-- You can add more board settings above here as needed -->
|
||||
|
||||
<h2 class="text-2xl font-semibold mb-4">Schedules for this display</h2>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Days</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Start Time</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">End Time</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Content</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for s in schedules %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ s.days_of_week }}</td>
|
||||
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
|
||||
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">{{ s.content[:50] }}{% if s.content|length > 50 %}...{% endif %}</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
<a href="{{ url_for('edit_schedule', board_id=board.id, schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Edit</a>
|
||||
<form method="post" action="{{ url_for('delete_schedule', board_id=board.id, schedule_id=s.id) }}" style="display:inline;">
|
||||
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('add_schedule', board_id=board.id) }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition inline-block">Add Schedule</a>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
45
templates/board_schedules.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
|
||||
<h1 class="text-3xl font-semibold mb-6">Manage Schedules for {{ board.name }}</h1>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700">Datum</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Starttijd</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Eindtijd</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for s in schedules %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ s.name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">{{ s.date.strftime('%Y-%m-%d') }}</td>
|
||||
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
|
||||
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
<a href="{{ url_for('edit_schedule', board_id=board.id, schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Edit</a>
|
||||
<form action="{{ url_for('delete_schedule', board_id=board.id, schedule_id=s.id) }}" method="POST" style="display:inline;">
|
||||
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('add_schedule', board_id=board.id) }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition inline-block">Add Schedule</a>
|
||||
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block ml-4">Terug naar bord bewerken</a>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
36
templates/change_password.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Wachtwoord wijzigen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
<div class="max-w-md mx-auto">
|
||||
<h2 class="text-3xl font-semibold mb-6">Wijzig Wachtwoord</h2>
|
||||
|
||||
{% if msg %}
|
||||
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">{{ msg }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="current_password" class="block mb-3 font-semibold text-gray-700">Huidig wachtwoord</label>
|
||||
<input type="password" id="current_password" name="current_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="new_password" class="block mb-3 font-semibold text-gray-700">Nieuw wachtwoord</label>
|
||||
<input type="password" id="new_password" name="new_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password" class="block mb-3 font-semibold text-gray-700">Bevestig nieuw wachtwoord</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
104
templates/church_settings.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Kerk Instellingen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
|
||||
|
||||
{% if msg %}
|
||||
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">{{ msg }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-wrap gap-10">
|
||||
|
||||
<form method="post" class="flex-1 min-w-[300px] max-w-lg space-y-6" enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" name="form_type" value="contact">
|
||||
|
||||
<h3 class="text-2xl font-semibold">Kerkgegevens</h3>
|
||||
|
||||
<div>
|
||||
<label for="name" class="block mb-3 font-semibold text-gray-700">Kerknaam</label>
|
||||
<input type="text" id="name" name="name" value="{{ church.name }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="contact_email" class="block mb-3 font-semibold text-gray-700">Contact Email</label>
|
||||
<input type="email" id="contact_email" name="contact_email" value="{{ church.contact_email }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="contact_phone" class="block mb-3 font-semibold text-gray-700">Telefoon</label>
|
||||
<input type="text" id="contact_phone" name="contact_phone" value="{{ church.contact_phone }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="contact_address" class="block mb-3 font-semibold text-gray-700">Adres</label>
|
||||
<input type="text" id="contact_address" name="contact_address" value="{{ church.contact_address }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="logo" class="block mb-3 font-semibold text-gray-700">Logo uploaden</label>
|
||||
<input type="file" id="logo" name="logo" accept="image/png, image/jpeg, image/jpg, image/gif" class="w-full border border-gray-300 rounded-md p-4">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="flex-1 min-w-[300px] max-w-md">
|
||||
|
||||
<h3 class="text-2xl font-semibold mb-4">Gebruikers van deze kerk</h3>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300 mb-6">
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900 flex items-center justify-between">
|
||||
{{ user.username }}
|
||||
{% if user.id != current_user.id %}
|
||||
<form method="post" action="{{ url_for('church_delete_user', user_id=user.id) }}" onsubmit="return confirm('Weet je zeker dat je deze gebruiker wilt verwijderen?');">
|
||||
<button type="submit" class="bg-red-600 text-white px-6 py-3 rounded font-semibold hover:bg-red-700 ml-4 transition">Verwijderen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-semibold mb-4">Nieuwe gebruiker toevoegen aan deze kerk</h3>
|
||||
|
||||
<form method="post" class="space-y-6" >
|
||||
<input type="hidden" name="form_type" value="user">
|
||||
<div>
|
||||
<label for="username" class="block mb-3 font-semibold text-gray-700">Emailadres</label>
|
||||
<input type="email" id="username" name="username" placeholder="email@voorbeeld.com" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
|
||||
<input type="password" id="password" name="password" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Toevoegen</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ url_for('change_password') }}" class="bg-white text-black border border-black px-4 py-2 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block mt-6 inline-block text-base">Wachtwoord wijzigen</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('portal') }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block">Terug naar portal</a>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
228
templates/display_board.html
Normal file
@@ -0,0 +1,228 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Liturgiebord</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
font-family: 'Lora', serif;
|
||||
height: 1920px;
|
||||
width: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.liturgie-board {
|
||||
width: 1080px;
|
||||
height: 1920px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start; /* Align all rows from the top */
|
||||
align-items: flex-start;
|
||||
|
||||
gap: 0vh;
|
||||
padding-left: 0;
|
||||
position: relative;
|
||||
}
|
||||
.liturgie-line {
|
||||
margin: auto;
|
||||
width: 1080px;
|
||||
justify-items: center;
|
||||
|
||||
text-align: left;
|
||||
font-size: 130px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
height: 190px; /* Fixed height ensures every row occupies space, even when empty */
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
z-index: 2;
|
||||
text-shadow: 2px 2px 8px #000, 0 0 4px #000;
|
||||
/* Optional: Uncomment for visible empty row borders */
|
||||
/* border-bottom: 1px solid rgba(255,255,255,0.15); */
|
||||
}
|
||||
.liturgie-bg-overlay {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="transform-origin: top left;">
|
||||
<div class="liturgie-board" {% if board.background_image %}
|
||||
style="background-image: url('{% if board.background_image.startswith('default_') %}{{ url_for('static', filename=board.background_image) }}{% else %}{{ url_for('uploaded_file', filename=board.background_image) }}{% endif %}'); background-size: cover; background-position: center; background-repeat: no-repeat;"
|
||||
{% endif %}>
|
||||
{% if board.background_image %}<div class="liturgie-bg-overlay"></div>{% endif %}
|
||||
{% if schedule_content %}
|
||||
{% set lines = schedule_content.split('\n') %}
|
||||
{% for i in range(10) %}
|
||||
<div class="liturgie-line">{{ lines[i] if lines|length > i else ' ' }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="liturgie-line">{{ board.line1 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line2 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line3 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line4 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line5 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line6 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line7 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line8 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line9 or ' ' }}</div>
|
||||
<div class="liturgie-line">{{ board.line10 or ' ' }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// LIVE PREVIEW FEATURE: Accept messages with new data and update the lines/background
|
||||
window.addEventListener('message', function(event) {
|
||||
// You may want to restrict this for security:
|
||||
// if (event.origin !== 'YOUR_DOMAIN') return;
|
||||
var data = event.data;
|
||||
if (!data || typeof data !== 'object' || !data.type || data.type !== 'updateBoard') return;
|
||||
|
||||
// Update all lines
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
var el = document.querySelector('.liturgie-line:nth-child(' + (data.bg ? i+1 : i) + ')');
|
||||
if (el) el.textContent = (data['line'+i] || ' ');
|
||||
}
|
||||
// Update background if sent
|
||||
if ('bg' in data) {
|
||||
document.querySelector('.liturgie-board').style.backgroundImage = data.bg ? ("url('" + data.bg + "')") : 'none';
|
||||
// Show/hide overlay
|
||||
var overlay = document.querySelector('.liturgie-bg-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = data.bg ? '' : 'none';
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
</script>
|
||||
<script>
|
||||
// Poll every 5 seconds to check for active schedule update
|
||||
(function() {
|
||||
let currentLines = [];
|
||||
let currentBackground = null;
|
||||
{% if schedule_content %}
|
||||
currentLines = {{ schedule_content.split('\n')|tojson }};
|
||||
{% else %}
|
||||
currentLines = Array(10).fill(' ');
|
||||
{% endif %}
|
||||
|
||||
const boardId = {{ board.id }};
|
||||
|
||||
function updateBoardFromData(data) {
|
||||
// Use postMessage event format to update lines & background
|
||||
const msg = { type: 'updateBoard' };
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
msg['line'+i] = data.lines[i-1] || ' ';
|
||||
}
|
||||
if (data.background) {
|
||||
msg.bg = data.background;
|
||||
} else {
|
||||
msg.bg = null;
|
||||
}
|
||||
window.postMessage(msg, '*');
|
||||
currentLines = data.lines;
|
||||
currentBackground = data.background || null;
|
||||
}
|
||||
|
||||
async function pollActiveSchedule() {
|
||||
try {
|
||||
const response = await fetch(`/board/${boardId}/active_schedule_json`);
|
||||
if (!response.ok) throw new Error('Network response not ok');
|
||||
const data = await response.json();
|
||||
|
||||
// Compare new lines with current lines
|
||||
let changed = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if ((data.lines[i] || ' ') !== (currentLines[i] || ' ')) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for background change
|
||||
if ((data.background || null) !== currentBackground) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
updateBoardFromData(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error polling active schedule:', e);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(pollActiveSchedule, 5000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Poll every 5 seconds to check for active schedule update
|
||||
(function() {
|
||||
let currentLines = [];
|
||||
{% if schedule_content %}
|
||||
currentLines = {{ schedule_content.split('\n')|tojson }};
|
||||
{% else %}
|
||||
currentLines = Array(10).fill(' ');
|
||||
{% endif %}
|
||||
|
||||
const boardId = {{ board.id }};
|
||||
|
||||
function updateBoardFromData(data) {
|
||||
// Use postMessage event format to update lines & background
|
||||
const msg = { type: 'updateBoard' };
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
msg['line'+i] = data.lines[i-1] || ' ';
|
||||
}
|
||||
if (data.background) {
|
||||
msg.bg = data.background;
|
||||
}
|
||||
window.postMessage(msg, '*');
|
||||
currentLines = data.lines;
|
||||
}
|
||||
|
||||
async function pollActiveSchedule() {
|
||||
try {
|
||||
const response = await fetch(`/board/${boardId}/active_schedule_json`);
|
||||
if (!response.ok) throw new Error('Network response not ok');
|
||||
const data = await response.json();
|
||||
|
||||
// Compare new lines with current lines
|
||||
let changed = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if ((data.lines[i] || ' ') !== (currentLines[i] || ' ')) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Also consider background change
|
||||
// We can extend this if needed, skipping for now
|
||||
|
||||
if (changed) {
|
||||
updateBoardFromData(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error polling active schedule:', e);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(pollActiveSchedule, 5000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
391
templates/edit_board.html
Normal file
@@ -0,0 +1,391 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Bewerk Liturgiebord - Digitale Liturgie{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.edit-board-container {
|
||||
padding: 1rem 1rem;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
|
||||
.board-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: #000000;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.lines-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.line-input-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.line-input-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.line-input-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1.125rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
|
||||
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.line-input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 5px #93c5fd;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
padding: 0.75rem 1.75rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 700;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.schedule-section {
|
||||
max-width: 100%;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.schedule-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.schedule-section table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
font-family: 'Lora', serif;
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
width: 250px;
|
||||
height: 444px; /* 9:16 aspect ratio */
|
||||
margin: 0 auto;
|
||||
background: #222;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
iframe#live-preview-iframe {
|
||||
width: 1080px;
|
||||
height: 1920px;
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
transform-origin: top left;
|
||||
transform: scale(0.23);
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.edit-board-container {
|
||||
padding: 15px;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.board-title {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
width: 180px;
|
||||
height: 320px;
|
||||
margin: 0 auto;
|
||||
border-radius: 0.5rem;
|
||||
background: #222;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
iframe#live-preview-iframe {
|
||||
transform: scale(0.23);
|
||||
position: static;
|
||||
width: 1080px;
|
||||
height: 1920px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlight for drag-over between lines in preview */
|
||||
.liturgie-line.line-over {
|
||||
background: rgba(255,255,255,0.22);
|
||||
border: 2px dashed #fff;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: background 0.12s, border 0.12s;
|
||||
}
|
||||
|
||||
.liturgie-line.swap-animate {
|
||||
animation: swap-pulse 0.35s;
|
||||
}
|
||||
@keyframes swap-pulse {
|
||||
0% { background: #ffe066; }
|
||||
60% { background: #fff3cd; }
|
||||
100% { background: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="edit-board-container">
|
||||
<div style="display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap;">
|
||||
<!-- Left: Live (editable) preview -->
|
||||
<form method="post" id="boardForm" style="flex: 1 1 300px; min-width: 250px; max-width:250px;">
|
||||
<div class="preview-section">
|
||||
<div class="preview-wrapper"
|
||||
style="width:250px; height:444px; background: #222; border-radius: 0.5rem; overflow: hidden; position:relative;
|
||||
background-image: {% if board.background_image %}url('{% if board.background_image.startswith('default_') %}{{ url_for('static', filename=board.background_image) }}{% else %}{{ url_for('uploaded_file', filename=board.background_image) }}{% endif %}'){% else %}none{% endif %}; background-size:cover; background-position:center;">
|
||||
{% if board.background_image %}
|
||||
<div class="liturgie-bg-overlay" style="
|
||||
content:''; position:absolute; top:0; left:0; right:0; bottom:0;
|
||||
background:rgba(0,0,0,0.3); z-index:1; pointer-events:none;">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if schedule_active %}
|
||||
{% set matched_schedules = schedules|selectattr('content', 'equalto', schedule_content)|list %}
|
||||
<div style="position:absolute; top:0; left:0; right:0; bottom:0; z-index:10; display:flex; justify-content:center; align-items:center; background: rgba(0,0,0,0.7); color:white; font-weight:bold; font-size:1rem; text-align:center; padding: 1rem;">
|
||||
Geplande dienst ({{ matched_schedules[0].name if matched_schedules|length > 0 else 'Onbekend' }}) is nu actief
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="liturgie-board"
|
||||
style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:flex-start; gap:0.6vh; padding-left:5%; position:relative; z-index:2;">
|
||||
{% if schedule_active %}
|
||||
{% set lines = schedule_content.split('\\n') %}
|
||||
{% for i in range(10) %}
|
||||
<div class="liturgie-line" style="width:90%; font-size:28px; line-height:1.2; text-shadow:2px 2px 8px #000, 0 0 4px #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin:0; padding:0; color:#fff; text-align:left;">
|
||||
{{ lines[i] if lines|length > i else ' ' }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for i in range(1, 11) %}
|
||||
<div class="liturgie-line"
|
||||
contenteditable="true"
|
||||
data-line="line{{ i }}"
|
||||
draggable="true"
|
||||
style="width:90%; font-size:28px; line-height:1.2; text-shadow:2px 2px 8px #000, 0 0 4px #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin:0; padding:0; color:#fff; text-align:left;"
|
||||
>{{ board['line' ~ i] or ' ' }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if schedule_active %}
|
||||
<p style="color: red;">Er is een dienst actief! Pas deze aan via de planning.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<!-- Right: Schedule section -->
|
||||
<div class="schedule-section" style="flex: 1 1 380px; min-width:330px;">
|
||||
<form method="post" style="margin-bottom: 1rem;">
|
||||
<input type="hidden" name="form_type" value="name">
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<input id="boardNameInput" name="name" type="text" value="{{ board.name }}" style="font-size:1.1rem; padding:0.3em 0.6em; border-radius:0.3em; border:1px solid #aaa; width:60%;">
|
||||
<button type="submit" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold transition" style="margin-left:5px;">Opslaan</button>
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="text-xl font-semibold mb-3">Geplande diensten</h3>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
{% if schedules|length == 0 %}
|
||||
<p class="p-4">Geen geplande inhoud.</p>
|
||||
{% else %}
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Datum</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Starttijd</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Eindtijd</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for s in schedules %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-gray-900 flex items-center gap-2">
|
||||
<span style="display:inline-block; width:0.75rem; height:0.75rem; border-radius:9999px;"
|
||||
title="{% if schedule_active and active_schedule and s.id == active_schedule.id %}Actief{% else %}Niet actief{% endif %}"
|
||||
class="{% if schedule_active and active_schedule and s.id == active_schedule.id %}bg-green-500{% else %}bg-red-500{% endif %}">
|
||||
</span>
|
||||
{{ s.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">{{ s.date.strftime('%Y-%m-%d') }}</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<!-- Removed delete button here: schedules can't be deleted from board edit page -->
|
||||
<a href="{{ url_for('edit_global_schedule', schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold transition">Bewerken</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ url_for('add_global_schedule') }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold inline-block mt-3 transition">Dienst toevoegen</a>
|
||||
<div class="button-group mt-3">
|
||||
<button type="button" class="bg-white text-black border border-black px-6 py-3 rounded font-semibold hover:bg-gray-100 transition"
|
||||
id="save-and-upload-bg"
|
||||
data-edit-url="{{ url_for('edit_board', board_id=board.id) }}"
|
||||
data-upload-url="{{ url_for('upload_background', board_id=board.id) }}">
|
||||
Achtergrondafbeelding wijzigen
|
||||
</button>
|
||||
<!-- Removed Opslaan button as live save is implemented -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% if not schedule_active %}
|
||||
<script>
|
||||
// --- Line drag-and-swap logic ---
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let dragSrc = null;
|
||||
const lines = document.querySelectorAll('.liturgie-line[data-line]');
|
||||
lines.forEach(line => {
|
||||
line.addEventListener('dragstart', function (e) {
|
||||
dragSrc = this;
|
||||
this.classList.add('dragging');
|
||||
});
|
||||
line.addEventListener('dragend', function (e) {
|
||||
this.classList.remove('dragging');
|
||||
dragSrc = null;
|
||||
lines.forEach(l => l.classList.remove('line-over'));
|
||||
});
|
||||
line.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
if (this !== dragSrc) this.classList.add('line-over');
|
||||
});
|
||||
line.addEventListener('dragleave', function (e) {
|
||||
this.classList.remove('line-over');
|
||||
});
|
||||
line.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('line-over');
|
||||
if (dragSrc && dragSrc !== this) {
|
||||
// Only swap textContent, not the whole element
|
||||
const tmp = this.textContent;
|
||||
this.textContent = dragSrc.textContent;
|
||||
dragSrc.textContent = tmp;
|
||||
// animation for both lines
|
||||
this.classList.add('swap-animate');
|
||||
dragSrc.classList.add('swap-animate');
|
||||
setTimeout(() => {
|
||||
this.classList.remove('swap-animate');
|
||||
dragSrc.classList.remove('swap-animate');
|
||||
}, 400);
|
||||
// trigger input event for autosave
|
||||
this.dispatchEvent(new Event('input'));
|
||||
dragSrc.dispatchEvent(new Event('input'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// --- End drag-and-swap logic ---
|
||||
|
||||
// On submit, copy preview lines to hidden fields
|
||||
const boardForm = document.getElementById('boardForm');
|
||||
if (boardForm) {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
// create hidden inputs for each line
|
||||
let hidden = document.createElement('input');
|
||||
hidden.type = 'hidden';
|
||||
hidden.name = 'line'+i;
|
||||
hidden.id = 'hidden-line'+i;
|
||||
boardForm.appendChild(hidden);
|
||||
}
|
||||
|
||||
async function saveLines() {
|
||||
const formData = new FormData();
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const text = document.querySelector('.liturgie-line[data-line="line'+i+'"]').textContent;
|
||||
formData.append('line'+i, text);
|
||||
}
|
||||
|
||||
const response = await fetch(boardForm.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to save lines.');
|
||||
}
|
||||
}
|
||||
|
||||
// Add auto save event listeners to editable lines with debounce
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const editableLine = document.querySelector('.liturgie-line[data-line="line'+i+'"]');
|
||||
if (editableLine) {
|
||||
editableLine.addEventListener('input', () => {
|
||||
if (window.autoSaveTimeout) clearTimeout(window.autoSaveTimeout);
|
||||
window.autoSaveTimeout = setTimeout(saveLines, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent form's default submit to avoid page reload on autosave
|
||||
boardForm.addEventListener('submit', function(e) {
|
||||
if (document.getElementById('redirect_to_upload').value === '0') {
|
||||
e.preventDefault();
|
||||
saveLines();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Added script for wallpaper button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btn = document.getElementById('save-and-upload-bg');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const uploadUrl = this.getAttribute('data-upload-url');
|
||||
if (uploadUrl) {
|
||||
window.location.href = uploadUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<script>
|
||||
// When schedule is active, there's nothing to submit for the lines and nothing to sync.
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
43
templates/global_schedules.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Globale Planningen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
<h1 class="text-3xl font-semibold mb-6">Globale planningen</h1>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Datum</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Starttijd</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Eindtijd</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Toegewezen borden</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for schedule in schedules %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-gray-900">{{ schedule.name }}</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.date.strftime('%Y-%m-%d') }}</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.start_time.strftime('%H:%M') }}</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.end_time.strftime('%H:%M') }}</td>
|
||||
<td class="px-6 py-4 text-center text-gray-900">
|
||||
{% for board in schedule.boards %}{{ board.name }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<a href="{{ url_for('edit_global_schedule', schedule_id=schedule.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Bewerken</a>
|
||||
<form action="{{ url_for('delete_global_schedule', schedule_id=schedule.id) }}" method="POST" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze planning wilt verwijderen?');">
|
||||
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="{{ url_for('add_global_schedule') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 inline-block mb-6 transition">Nieuwe planning toevoegen</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
337
templates/index.html
Normal file
@@ -0,0 +1,337 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Welkom bij Digitale Liturgie{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* Modern Google Fonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
|
||||
.hero-section {
|
||||
position: relative;
|
||||
background: url("{{ url_for('static', filename='Hero.jpg') }}") no-repeat center center/cover;
|
||||
min-height: 30rem;
|
||||
border-radius: 1.15rem;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
animation: fadeInHero 1.2s cubic-bezier(.77,0,.18,1) both;
|
||||
}
|
||||
@keyframes fadeInHero {
|
||||
from { opacity: 0; transform: translateY(-30px);}
|
||||
to { opacity: 1; transform: translateY(0);}
|
||||
}
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(40,40,50,0.75) 0%, rgba(10,0,40,0.40) 70%);
|
||||
z-index: 1;
|
||||
border-radius: 1.15rem;
|
||||
}
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 42rem;
|
||||
text-align: center;
|
||||
padding: 2rem 2rem;
|
||||
animation: fadeUp 1.2s .3s cubic-bezier(.77,0,.18,1) both;
|
||||
}
|
||||
.hero-content h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 5px 32px rgba(0,0,0,0.32);
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -1.2px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.hero-content p {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
.modern-btn {
|
||||
background: linear-gradient(90deg, #f7d91a 0%, #ffe486 100%);
|
||||
color: #181818;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
padding: 1rem 2.3rem;
|
||||
border-radius: 0.66rem;
|
||||
font-size: 1.14rem;
|
||||
box-shadow: 0 4px 24px rgba(247,217,26,0.13);
|
||||
transition: box-shadow 0.23s, background 0.23s, transform 0.18s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modern-btn:hover {
|
||||
background: linear-gradient(90deg,#ffe486 0%, #f7d91a 100%);
|
||||
box-shadow: 0 8px 28px rgba(247,217,26,0.19);
|
||||
transform: translateY(-2px) scale(1.045);
|
||||
color: #25201b;
|
||||
}
|
||||
|
||||
/* Contact section styling */
|
||||
.contact-section {
|
||||
background: white;
|
||||
border-radius: 1.15rem;
|
||||
margin-top: 1rem;
|
||||
padding: 2.2rem 1.5rem;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.09);
|
||||
max-width: 98vw;
|
||||
min-width: 280px;
|
||||
animation: fadeUp 1.1s .6s cubic-bezier(.77,0,.18,1) both;
|
||||
}
|
||||
.contact-section h2 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.contact-section input,
|
||||
.contact-section textarea {
|
||||
width: 100%;
|
||||
border: 1.3px solid #e6e6e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.9rem 1.14rem;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #f8f9fa;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.contact-section input:focus,
|
||||
.contact-section textarea:focus {
|
||||
border-color: #f7d91a;
|
||||
outline: none;
|
||||
background: #fffde6;
|
||||
}
|
||||
.contact-section button {
|
||||
margin-top: 0.3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Cards Area */
|
||||
.features-area {
|
||||
display: grid;
|
||||
gap: 2.2rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
margin-top: 2.2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
animation: fadeInStagger 1s cubic-bezier(.77,0,.18,1) both;
|
||||
}
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(50px);}
|
||||
to { opacity: 1; transform: translateY(0);}
|
||||
}
|
||||
@keyframes fadeInStagger {
|
||||
from { opacity: 0;}
|
||||
to { opacity: 1;}
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 1.09rem;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
|
||||
padding: 2rem 1.2rem 1.4rem 1.2rem;
|
||||
text-align: center;
|
||||
min-height: 19.5rem;
|
||||
animation: fadeUp 0.8s cubic-bezier(.77,0,.18,1) both;
|
||||
transition: transform 0.22s, box-shadow 0.22s;
|
||||
will-change: transform;
|
||||
/* Animation delay stagger per card (manual in HTML) */
|
||||
}
|
||||
.feature-card:hover {
|
||||
transform: translateY(-7px) scale(1.02);
|
||||
box-shadow: 0 16px 40px rgba(247,217,26,0.13);
|
||||
}
|
||||
.feature-card img {
|
||||
max-width: 55px;
|
||||
margin-bottom: 1.1rem;
|
||||
margin-top: -0.8rem;
|
||||
filter: drop-shadow(0 0 2px rgba(40,40,40,0.06));
|
||||
transition: transform 0.17s;
|
||||
}
|
||||
.feature-card:hover img {
|
||||
transform: scale(1.095) rotate(2deg);
|
||||
}
|
||||
.feature-card h3 {
|
||||
font-size: 1.23rem;
|
||||
margin-bottom: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #181a1d;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.feature-card ul {
|
||||
text-align: left;
|
||||
color: #434243;
|
||||
font-size: 1.02rem;
|
||||
list-style: none;
|
||||
padding-left: 0.7em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.feature-card ul li::before {
|
||||
content: '• ';
|
||||
color: #f7d91a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Responsive hero/contact split */
|
||||
@media (min-width: 1000px) {
|
||||
.hero-container {
|
||||
display: flex;
|
||||
gap: 2.4rem;
|
||||
max-width: 1330px;
|
||||
margin: 0 auto 3.3rem auto;
|
||||
}
|
||||
.hero-section, .contact-section {
|
||||
flex: 1.2;
|
||||
min-height: 32rem;
|
||||
}
|
||||
.contact-section {
|
||||
min-width: 350px;
|
||||
margin-top: 0;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 780px) {
|
||||
.hero-section {
|
||||
min-height: 22rem;
|
||||
}
|
||||
.hero-content h1 { font-size: 2rem;}
|
||||
.features-area { gap: 1rem;}
|
||||
.feature-card { padding: 1.3rem 0.5rem; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="hero-section" style="margin-bottom:2.5rem;">
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content">
|
||||
<h1>Psalmbord Online</h1>
|
||||
<p>
|
||||
Welkom bij Psalmbord Online.<br>
|
||||
Beheer, plan en presenteer moeiteloos je kerkdiensten.<br>
|
||||
Start direct met een eigen account!
|
||||
</p>
|
||||
<a href="/login" class="modern-btn">Log in</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-intro" style="margin-top: 3.7rem; margin-bottom:1.5rem; animation: fadeUp 1.1s .3s cubic-bezier(.77,0,.18,1) both;">
|
||||
<h2 class="text-3xl font-bold text-center mb-2" style="font-size:2.1rem;">Wat kan Digitale Liturgie voor u doen?</h2>
|
||||
<p class="text-center text-md mb-8" style="color:#62666b;">Beheer en presenteer moeiteloos uw kerkdiensten, gebruikers en schermen.</p>
|
||||
</div>
|
||||
|
||||
<div class="features-area">
|
||||
<div class="feature-card" style="animation-delay:.09s;">
|
||||
<img src="{{ url_for('static', filename='icons/group.png') }}" alt="Gebruikersbeheer">
|
||||
<h3>Gebruikersbeheer</h3>
|
||||
<ul>
|
||||
<li>Aanmelden & Registreren</li>
|
||||
<li>Wachtwoordbeheer</li>
|
||||
<li>Lid & Admin-rollen</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-card" style="animation-delay:.20s;">
|
||||
<img src="{{ url_for('static', filename='icons/church.png') }}" alt="Kerkenbeheer">
|
||||
<h3>Kerkenbeheer</h3>
|
||||
<ul>
|
||||
<li>Kerk aanmaken & aanpassen</li>
|
||||
<li>Logo uploaden</li>
|
||||
<li>Activatie beheren</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-card" style="animation-delay:.35s;">
|
||||
<img src="{{ url_for('static', filename='icons/board.png') }}" alt="Bordenbeheer">
|
||||
<h3>Bordenbeheer</h3>
|
||||
<ul>
|
||||
<li>Borden toevoegen, bewerken, verwijderen</li>
|
||||
<li>Max. 10 tekstregels per bord</li>
|
||||
<li>Achtergrond instellen of uploaden</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-card" style="animation-delay:.50s;">
|
||||
<img src="{{ url_for('static', filename='icons/task.png') }}" alt="Planningen">
|
||||
<h3>Planningen</h3>
|
||||
<ul>
|
||||
<li>Diensten & liturgie-invulling per datum</li>
|
||||
<li>Meerdere borden per planning</li>
|
||||
<li>Automatisch tonen volgens tijd</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact form at BOTTOM -->
|
||||
<div class="contact-section" style="max-width:none; margin:3.8rem 0 0 0; width:100%;">
|
||||
<h2>Contacteer ons</h2>
|
||||
<form method="POST" action="/contact" autocomplete="off">
|
||||
<div class="contact-form-fields">
|
||||
<div class="inputs-left">
|
||||
<input type="text" id="name" name="name" placeholder="Naam" required>
|
||||
<input type="text" id="church" name="church" placeholder="Kerknaam" required>
|
||||
<input type="tel" id="phone" name="phone" placeholder="Telefoonnummer" required>
|
||||
<input type="email" id="email" name="email" placeholder="E-mail" required>
|
||||
</div>
|
||||
<div class="textarea-right">
|
||||
<textarea id="message" name="message" rows="7" placeholder="Uw bericht..." required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="modern-btn">Verstuur</button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.contact-form-fields {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
.inputs-left {
|
||||
flex: 1 1 210px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 190px;
|
||||
}
|
||||
.inputs-left input {
|
||||
opacity: 0;
|
||||
transform: translateY(32px);
|
||||
animation: fadeFormInput 0.8s cubic-bezier(.77,0,.18,1) forwards;
|
||||
}
|
||||
.inputs-left input:nth-child(1) { animation-delay: 0.18s; }
|
||||
.inputs-left input:nth-child(2) { animation-delay: 0.32s; }
|
||||
.inputs-left input:nth-child(3) { animation-delay: 0.46s; }
|
||||
.inputs-left input:nth-child(4) { animation-delay: 0.6s; }
|
||||
.textarea-right textarea {
|
||||
opacity: 0;
|
||||
transform: translateY(32px);
|
||||
animation: fadeFormInput 0.8s 0.74s cubic-bezier(.77,0,.18,1) forwards;
|
||||
height: 100%;
|
||||
min-height: 150px;
|
||||
resize: vertical;
|
||||
}
|
||||
.contact-section button.modern-btn {
|
||||
opacity: 0;
|
||||
transform: translateY(32px);
|
||||
animation: fadeFormInput 0.8s 1s cubic-bezier(.77,0,.18,1) forwards;
|
||||
}
|
||||
@keyframes fadeFormInput {
|
||||
from { opacity: 0; transform: translateY(32px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.contact-form-fields {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.textarea-right, .inputs-left {
|
||||
min-width: 0;
|
||||
}
|
||||
.textarea-right textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
templates/login.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Login - Digitale Liturgie{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
<div class="max-w-md mx-auto">
|
||||
<h2 class="text-3xl font-semibold mb-6">Login</h2>
|
||||
|
||||
<form method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block mb-3 font-semibold text-gray-700">Gebruikersnaam</label>
|
||||
<input type="text" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
|
||||
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
106
templates/portal.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
<h2 class="text-3xl font-semibold mb-6">Welkom, {{ current_user.username }}!</h2>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-300">
|
||||
<table class="min-w-full divide-y divide-gray-200 bg-white">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
|
||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700">URL</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Bewerken</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Weergeven</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Verwijderen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for board in boards %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-gray-900">{{ board.name }}</td>
|
||||
<td class="px-6 py-4 text-blue-600 break-words">
|
||||
{% if board.church.is_active %}
|
||||
<a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" target="_blank" class="underline hover:text-blue-800 transition">display/{{ board.unique_id }}</a>
|
||||
{% else %}
|
||||
<em>Geen actieve licentie voor dit scherm</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="bg-[#f7d91a] text-black text-xs px-2 py-1 rounded hover:bg-yellow-300 transition">Bewerken</a>
|
||||
</td>
|
||||
<td class="relative text-center px-6 py-4">
|
||||
{% if board.church.is_active %}
|
||||
<a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" class="bg-black text-white text-xs px-2 py-1 rounded border border-black hover:bg-gray-800 transition show-iframe-preview" target="_blank" data-url="{{ url_for('display_board_unique', unique_id=board.unique_id) }}">Weergeven</a>
|
||||
<div class="iframe-preview-popup" style="display:none; position:absolute; top:0; left:100%; margin-left:8px; width:400px; height:225px; border:1px solid #ccc; box-shadow:0 2px 8px rgba(0,0,0,0.2); z-index:50;">
|
||||
<iframe src="" frameborder="0" style="width:100%; height:100%; transform-origin: top left; transform: scale(0.2);"></iframe>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center px-6 py-4">
|
||||
<form method="post" action="{{ url_for('delete_board', board_id=board.id) }}" style="display:inline;">
|
||||
<button type="submit" class="bg-white text-black text-xs px-2 py-1 rounded border border-black hover:bg-gray-100 transition" onclick="return confirm('Weet je zeker dat je dit bord wilt verwijderen?');">Verwijderen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="{{ url_for('add_board') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 inline-block mb-6 transition">Nieuw liturgiebord toevoegen</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const showIframeLinks = document.querySelectorAll('.show-iframe-preview');
|
||||
|
||||
// Create a single popup container to be reused
|
||||
const popup = document.createElement('div');
|
||||
popup.style.position = 'absolute';
|
||||
popup.style.width = '1080px';
|
||||
popup.style.height = '1920px';
|
||||
popup.style.border = '1px solid #ccc';
|
||||
popup.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
||||
popup.style.zIndex = '9999';
|
||||
popup.style.display = 'none';
|
||||
popup.style.background = 'white';
|
||||
popup.style.transformOrigin = 'top left';
|
||||
popup.style.transform = 'scale(0.2)';
|
||||
|
||||
// iframe inside popup
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.frameBorder = 0;
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.transform = '';
|
||||
iframe.style.transformOrigin = 'top left';
|
||||
popup.appendChild(iframe);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
showIframeLinks.forEach(link => {
|
||||
link.addEventListener('mouseenter', (event) => {
|
||||
const url = link.getAttribute('data-url');
|
||||
iframe.src = url;
|
||||
const rect = link.getBoundingClientRect();
|
||||
|
||||
// Position popup to the right and vertically aligned with the link
|
||||
popup.style.left = (window.scrollX + rect.right + 8) + 'px';
|
||||
popup.style.top = (window.scrollY + rect.top) + 'px';
|
||||
popup.style.display = 'block';
|
||||
});
|
||||
|
||||
link.addEventListener('mouseleave', () => {
|
||||
popup.style.display = 'none';
|
||||
iframe.src = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Also hide popup if mouse moves out of popup itself
|
||||
popup.addEventListener('mouseleave', () => {
|
||||
popup.style.display = 'none';
|
||||
iframe.src = '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
32
templates/register.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Register - Digitale Liturgie{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="space-y-8 max-w-6xl mx-auto px-4">
|
||||
<div class="max-w-md mx-auto">
|
||||
<h2 class="text-3xl font-semibold mb-6">Register</h2>
|
||||
|
||||
<form method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block mb-3 font-semibold text-gray-700">Emailadres</label>
|
||||
<input type="email" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
|
||||
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="church" class="block mb-3 font-semibold text-gray-700">Kerk</label>
|
||||
<input type="text" id="church" name="church" placeholder="Kies of maak een kerk" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
274
templates/schedule_form.html
Normal file
@@ -0,0 +1,274 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{% if schedule %}Schema Bijwerken{% else %}Schema Toevoegen{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.preview-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
max-width: 900px;
|
||||
margin: 0 auto 2rem auto;
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.preview-wrapper {
|
||||
width: 250px;
|
||||
height: 444px;
|
||||
background: #222;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.liturgie-board {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.6vh;
|
||||
padding-left: 5%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.liturgie-line {
|
||||
width: 90%;
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
text-shadow: 2px 2px 8px #000, 0 0 4px #000;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* Highlight for drag-over between lines in preview */
|
||||
.liturgie-line.line-over {
|
||||
background: rgba(255,255,255,0.22);
|
||||
border: 2px dashed #fff;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: background 0.12s, border 0.12s;
|
||||
}
|
||||
|
||||
.liturgie-line.swap-animate {
|
||||
animation: swap-pulse 0.35s;
|
||||
}
|
||||
@keyframes swap-pulse {
|
||||
0% { background: #ffe066; }
|
||||
60% { background: #fff3cd; }
|
||||
100% { background: none; }
|
||||
}
|
||||
.input-section {
|
||||
flex: 1 1 260px;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.input-section label {
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.input-section input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
|
||||
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
.input-section input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 5px #93c5fd;
|
||||
}
|
||||
.save-button {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background-color: #16a34a;
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
margin-top: auto;
|
||||
}
|
||||
.save-button:hover {
|
||||
background-color: #15803d;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.preview-container {
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.preview-wrapper {
|
||||
width: 200px;
|
||||
height: 355px;
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
.input-section {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">{% if schedule %}Schema Bijwerken{% else %}Schema Toevoegen{% endif %}</h1>
|
||||
<form method="post" id="scheduleForm">
|
||||
<div class="preview-container">
|
||||
<!-- Left: Live preview with editable lines -->
|
||||
<div class="preview-wrapper" style="font-family: 'Lora', serif; position: relative; background-image: {% if schedule and schedule.boards_list|length > 0 %}
|
||||
{% set board_bg = schedule.boards_list[0].background_image if schedule.boards_list[0].background_image else None %}
|
||||
{% if board_bg %}
|
||||
url('{% if board_bg.startswith('default_') %}{{ url_for('static', filename=board_bg) }}{% else %}{{ url_for('uploaded_file', filename=board_bg) }}{% endif %}')
|
||||
{% else %}none
|
||||
{% endif %}
|
||||
{% else %}none
|
||||
{% endif %}; background-color:#222; background-size: cover; background-position: center;">
|
||||
<div class="liturgie-bg-overlay" style="
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
"></div>
|
||||
<div class="liturgie-board" contenteditable="false" style="position: relative; z-index: 2; font-family: 'Lora', serif;">
|
||||
{% set lines = schedule.content.split('\n') if schedule and schedule.content else [' ']*10 %}
|
||||
{% for i in range(10) %}
|
||||
<div class="liturgie-line" contenteditable="true" data-line="line{{ i+1 }}" spellcheck="false">{{ lines[i] if i < lines|length else ' ' }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Input fields for schedule name, date, time, board assignment, and save button -->
|
||||
<div class="input-section">
|
||||
<label for="name">Naam:</label>
|
||||
<input type="text" id="name" name="name" required value="{{ schedule.name if schedule else '' }}">
|
||||
|
||||
<label for="date">Datum:</label>
|
||||
<input type="date" id="date" name="date" required value="{{ schedule.date.strftime('%Y-%m-%d') if schedule and schedule.date else '' }}">
|
||||
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<div style="flex: 1;">
|
||||
<label for="start_time">Starttijd:</label>
|
||||
<input type="time" id="start_time" name="start_time" required value="{{ schedule.start_time.strftime('%H:%M') if schedule and schedule.start_time else '' }}">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label for="end_time">Eindtijd:</label>
|
||||
<input type="time" id="end_time" name="end_time" required value="{{ schedule.end_time.strftime('%H:%M') if schedule and schedule.end_time else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Toegewezen borden:</label>
|
||||
<div class="overflow-hidden rounded-lg max-h-56 overflow-y-auto mb-6 border border-gray-200">
|
||||
<table class="min-w-full divide-y divide-gray-200 border-collapse">
|
||||
<thead class="bg-gray-50">
|
||||
<tr class="divide-x divide-gray-200">
|
||||
<th scope="col" class="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pl-6">Naam</th>
|
||||
<th scope="col" class="px-4 py-3.5 text-center text-sm font-semibold text-gray-900">Toewijzen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{% for board in boards %}
|
||||
<tr class="divide-x divide-gray-200 hover:bg-gray-50 cursor-pointer">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-4 text-sm font-semibold text-gray-900 sm:pl-6">{{ board.name }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-center text-sm font-semibold text-gray-900 flex justify-center items-center">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" value="{{ board.id }}" name="boards" class="sr-only peer" {% if schedule and board in schedule.boards_list %}checked{% endif %}>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:bg-blue-600 transition-all duration-300"></div>
|
||||
<div class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300 peer-checked:translate-x-5"></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-[#f7d91a] text-black px-8 py-3 rounded font-semibold hover:bg-yellow-300 transition">Opslaan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs to store lines for form submission -->
|
||||
{% for i in range(1, 11) %}
|
||||
<input type="hidden" name="line{{ i }}" id="hidden-line{{ i }}" value="">
|
||||
{% endfor %}
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// --- Line drag-and-swap logic ---
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let dragSrc = null;
|
||||
const lines = document.querySelectorAll('.liturgie-line[data-line]');
|
||||
lines.forEach(line => {
|
||||
line.addEventListener('dragstart', function (e) {
|
||||
dragSrc = this;
|
||||
this.classList.add('dragging');
|
||||
});
|
||||
line.addEventListener('dragend', function (e) {
|
||||
this.classList.remove('dragging');
|
||||
dragSrc = null;
|
||||
lines.forEach(l => l.classList.remove('line-over'));
|
||||
});
|
||||
line.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
if (this !== dragSrc) this.classList.add('line-over');
|
||||
});
|
||||
line.addEventListener('dragleave', function (e) {
|
||||
this.classList.remove('line-over');
|
||||
});
|
||||
line.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('line-over');
|
||||
if (dragSrc && dragSrc !== this) {
|
||||
// Only swap textContent, not the whole element
|
||||
const tmp = this.textContent;
|
||||
this.textContent = dragSrc.textContent;
|
||||
dragSrc.textContent = tmp;
|
||||
// animation for both lines
|
||||
this.classList.add('swap-animate');
|
||||
dragSrc.classList.add('swap-animate');
|
||||
setTimeout(() => {
|
||||
this.classList.remove('swap-animate');
|
||||
dragSrc.classList.remove('swap-animate');
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
// Make lines draggable
|
||||
line.setAttribute('draggable', 'true');
|
||||
});
|
||||
});
|
||||
// --- End drag-and-swap logic ---
|
||||
|
||||
document.getElementById('scheduleForm').addEventListener('submit', function(e) {
|
||||
// Copy content from editable divs to hidden inputs
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
let div = document.querySelector('.liturgie-line[data-line="line' + i + '"]');
|
||||
let hiddenInput = document.getElementById('hidden-line' + i);
|
||||
if (div && hiddenInput) {
|
||||
hiddenInput.value = div.textContent.trim() || ' ';
|
||||
}
|
||||
}
|
||||
});
|
||||
// Optional: keyboard navigation and basic shortcuts could be added here
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
44
templates/upload_background.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Achtergrondafbeelding wijzigen - Digitale Liturgie{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto">
|
||||
<h2 class="text-3xl font-semibold mb-6">Achtergrondafbeelding wijzigen voor: {{ board.name }}</h2>
|
||||
<div class="flex space-x-8">
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-6 flex-1">
|
||||
<div>
|
||||
<label for="background" class="block mb-2 font-semibold text-gray-700">Kies een afbeelding</label>
|
||||
<input type="file" id="background" name="background" accept="image/*" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
|
||||
</div>
|
||||
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-3 font-semibold shadow hover:bg-yellow-300 transition">Uploaden</button>
|
||||
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="block text-center bg-white text-black border border-black rounded-md py-3 mt-2 font-semibold shadow hover:bg-gray-100 transition w-full">Annuleren</a>
|
||||
</form>
|
||||
{% if board.background_image %}
|
||||
<div class="flex-1">
|
||||
<p class="mb-2">Huidige achtergrond:</p>
|
||||
<img src="{{ url_for('uploaded_file', filename=board.background_image) }}" alt="Achtergrond" class="rounded-lg max-w-full max-h-72 object-contain">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<h3 class="text-xl font-semibold mb-4">Kies een standaard achtergrond:</h3>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<form method="post" class="inline-block">
|
||||
<input type="hidden" name="default_background" value="default_wall_1.jpg">
|
||||
<img src="/static/default_wall_1.jpg" alt="Standaard achtergrond 1" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
|
||||
</form>
|
||||
<form method="post" class="inline-block">
|
||||
<input type="hidden" name="default_background" value="default_wall_2.jpg">
|
||||
<img src="/static/default_wall_2.jpg" alt="Standaard achtergrond 2" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
|
||||
</form>
|
||||
<form method="post" class="inline-block">
|
||||
<input type="hidden" name="default_background" value="default_wall_3.jpg">
|
||||
<img src="/static/default_wall_3.jpg" alt="Standaard achtergrond 3" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
|
||||
</form>
|
||||
<form method="post" class="inline-block">
|
||||
<input type="hidden" name="default_background" value="default_wall_4.jpg">
|
||||
<img src="/static/default_wall_4.jpg" alt="Standaard achtergrond 4" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
164
venv/Include/site/python3.11/greenlet/greenlet.h
Normal file
@@ -0,0 +1,164 @@
|
||||
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||
|
||||
/* Greenlet object interface */
|
||||
|
||||
#ifndef Py_GREENLETOBJECT_H
|
||||
#define Py_GREENLETOBJECT_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is deprecated and undocumented. It does not change. */
|
||||
#define GREENLET_VERSION "1.0.0"
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
#define implementation_ptr_t void*
|
||||
#endif
|
||||
|
||||
typedef struct _greenlet {
|
||||
PyObject_HEAD
|
||||
PyObject* weakreflist;
|
||||
PyObject* dict;
|
||||
implementation_ptr_t pimpl;
|
||||
} PyGreenlet;
|
||||
|
||||
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||
|
||||
|
||||
/* C API functions */
|
||||
|
||||
/* Total number of symbols that are exported */
|
||||
#define PyGreenlet_API_pointers 12
|
||||
|
||||
#define PyGreenlet_Type_NUM 0
|
||||
#define PyExc_GreenletError_NUM 1
|
||||
#define PyExc_GreenletExit_NUM 2
|
||||
|
||||
#define PyGreenlet_New_NUM 3
|
||||
#define PyGreenlet_GetCurrent_NUM 4
|
||||
#define PyGreenlet_Throw_NUM 5
|
||||
#define PyGreenlet_Switch_NUM 6
|
||||
#define PyGreenlet_SetParent_NUM 7
|
||||
|
||||
#define PyGreenlet_MAIN_NUM 8
|
||||
#define PyGreenlet_STARTED_NUM 9
|
||||
#define PyGreenlet_ACTIVE_NUM 10
|
||||
#define PyGreenlet_GET_PARENT_NUM 11
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
/* This section is used by modules that uses the greenlet C API */
|
||||
static void** _PyGreenlet_API = NULL;
|
||||
|
||||
# define PyGreenlet_Type \
|
||||
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||
|
||||
# define PyExc_GreenletError \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||
|
||||
# define PyExc_GreenletExit \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_New(PyObject *args)
|
||||
*
|
||||
* greenlet.greenlet(run, parent=None)
|
||||
*/
|
||||
# define PyGreenlet_New \
|
||||
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetCurrent(void)
|
||||
*
|
||||
* greenlet.getcurrent()
|
||||
*/
|
||||
# define PyGreenlet_GetCurrent \
|
||||
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Throw(
|
||||
* PyGreenlet *greenlet,
|
||||
* PyObject *typ,
|
||||
* PyObject *val,
|
||||
* PyObject *tb)
|
||||
*
|
||||
* g.throw(...)
|
||||
*/
|
||||
# define PyGreenlet_Throw \
|
||||
(*(PyObject * (*)(PyGreenlet * self, \
|
||||
PyObject * typ, \
|
||||
PyObject * val, \
|
||||
PyObject * tb)) \
|
||||
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||
*
|
||||
* g.switch(*args, **kwargs)
|
||||
*/
|
||||
# define PyGreenlet_Switch \
|
||||
(*(PyObject * \
|
||||
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||
*
|
||||
* g.parent = new_parent
|
||||
*/
|
||||
# define PyGreenlet_SetParent \
|
||||
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||
*
|
||||
* return greenlet.parent;
|
||||
*
|
||||
* This could return NULL even if there is no exception active.
|
||||
* If it does not return NULL, you are responsible for decrementing the
|
||||
* reference count.
|
||||
*/
|
||||
# define PyGreenlet_GetParent \
|
||||
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||
|
||||
/*
|
||||
* deprecated, undocumented alias.
|
||||
*/
|
||||
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||
|
||||
# define PyGreenlet_MAIN \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||
|
||||
# define PyGreenlet_STARTED \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||
|
||||
# define PyGreenlet_ACTIVE \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||
|
||||
|
||||
|
||||
|
||||
/* Macro that imports greenlet and initializes C API */
|
||||
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||
keep the older definition to be sure older code that might have a copy of
|
||||
the header still works. */
|
||||
# define PyGreenlet_Import() \
|
||||
{ \
|
||||
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||
}
|
||||
|
||||
#endif /* GREENLET_MODULE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_GREENLETOBJECT_H */
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
22
venv/Lib/site-packages/Flask_Login-0.6.3.dist-info/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2011 Matthew Frazier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
183
venv/Lib/site-packages/Flask_Login-0.6.3.dist-info/METADATA
Normal file
@@ -0,0 +1,183 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Flask-Login
|
||||
Version: 0.6.3
|
||||
Summary: User authentication and session management for Flask.
|
||||
Home-page: https://github.com/maxcountryman/flask-login
|
||||
Author: Matthew Frazier
|
||||
Author-email: leafstormrush@gmail.com
|
||||
Maintainer: Max Countryman
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://flask-login.readthedocs.io/
|
||||
Project-URL: Changes, https://github.com/maxcountryman/flask-login/blob/main/CHANGES.md
|
||||
Project-URL: Source Code, https://github.com/maxcountryman/flask-login
|
||||
Project-URL: Issue Tracker, https://github.com/maxcountryman/flask-login/issues
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Framework :: Flask
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: Flask >=1.0.4
|
||||
Requires-Dist: Werkzeug >=1.0.1
|
||||
|
||||
# Flask-Login
|
||||
|
||||

|
||||
[](https://coveralls.io/github/maxcountryman/flask-login?branch=main)
|
||||
[](LICENSE)
|
||||
|
||||
Flask-Login provides user session management for Flask. It handles the common
|
||||
tasks of logging in, logging out, and remembering your users' sessions over
|
||||
extended periods of time.
|
||||
|
||||
Flask-Login is not bound to any particular database system or permissions
|
||||
model. The only requirement is that your user objects implement a few methods,
|
||||
and that you provide a callback to the extension capable of loading users from
|
||||
their ID.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the extension with pip:
|
||||
|
||||
```sh
|
||||
$ pip install flask-login
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, the Flask-Login is easy to use. Let's walk through setting up
|
||||
a basic application. Also please note that this is a very basic guide: we will
|
||||
be taking shortcuts here that you should never take in a real application.
|
||||
|
||||
To begin we'll set up a Flask app:
|
||||
|
||||
```python
|
||||
import flask
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.secret_key = 'super secret string' # Change this!
|
||||
```
|
||||
|
||||
Flask-Login works via a login manager. To kick things off, we'll set up the
|
||||
login manager by instantiating it and telling it about our Flask app:
|
||||
|
||||
```python
|
||||
import flask_login
|
||||
|
||||
login_manager = flask_login.LoginManager()
|
||||
|
||||
login_manager.init_app(app)
|
||||
```
|
||||
|
||||
To keep things simple we're going to use a dictionary to represent a database
|
||||
of users. In a real application, this would be an actual persistence layer.
|
||||
However it's important to point out this is a feature of Flask-Login: it
|
||||
doesn't care how your data is stored so long as you tell it how to retrieve it!
|
||||
|
||||
```python
|
||||
# Our mock database.
|
||||
users = {'foo@bar.tld': {'password': 'secret'}}
|
||||
```
|
||||
|
||||
We also need to tell Flask-Login how to load a user from a Flask request and
|
||||
from its session. To do this we need to define our user object, a
|
||||
`user_loader` callback, and a `request_loader` callback.
|
||||
|
||||
```python
|
||||
class User(flask_login.UserMixin):
|
||||
pass
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def user_loader(email):
|
||||
if email not in users:
|
||||
return
|
||||
|
||||
user = User()
|
||||
user.id = email
|
||||
return user
|
||||
|
||||
|
||||
@login_manager.request_loader
|
||||
def request_loader(request):
|
||||
email = request.form.get('email')
|
||||
if email not in users:
|
||||
return
|
||||
|
||||
user = User()
|
||||
user.id = email
|
||||
return user
|
||||
```
|
||||
|
||||
Now we're ready to define our views. We can start with a login view, which will
|
||||
populate the session with authentication bits. After that we can define a view
|
||||
that requires authentication.
|
||||
|
||||
```python
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if flask.request.method == 'GET':
|
||||
return '''
|
||||
<form action='login' method='POST'>
|
||||
<input type='text' name='email' id='email' placeholder='email'/>
|
||||
<input type='password' name='password' id='password' placeholder='password'/>
|
||||
<input type='submit' name='submit'/>
|
||||
</form>
|
||||
'''
|
||||
|
||||
email = flask.request.form['email']
|
||||
if email in users and flask.request.form['password'] == users[email]['password']:
|
||||
user = User()
|
||||
user.id = email
|
||||
flask_login.login_user(user)
|
||||
return flask.redirect(flask.url_for('protected'))
|
||||
|
||||
return 'Bad login'
|
||||
|
||||
|
||||
@app.route('/protected')
|
||||
@flask_login.login_required
|
||||
def protected():
|
||||
return 'Logged in as: ' + flask_login.current_user.id
|
||||
```
|
||||
|
||||
Finally we can define a view to clear the session and log users out:
|
||||
|
||||
```python
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
return 'Logged out'
|
||||
```
|
||||
|
||||
We now have a basic working application that makes use of session-based
|
||||
authentication. To round things off, we should provide a callback for login
|
||||
failures:
|
||||
|
||||
```python
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized_handler():
|
||||
return 'Unauthorized', 401
|
||||
```
|
||||
|
||||
Documentation for Flask-Login is available on [ReadTheDocs](https://flask-login.readthedocs.io/en/latest/).
|
||||
For complete understanding of available configuration, please refer to the [source code](https://github.com/maxcountryman/flask-login).
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! If you would like to hack on Flask-Login, please
|
||||
follow these steps:
|
||||
|
||||
1. Fork this repository
|
||||
2. Make your changes
|
||||
3. Install the dev requirements with `pip install -r requirements/dev.txt`
|
||||
4. Submit a pull request after running `tox` (ensure it does not error!)
|
||||
|
||||
Please give us adequate time to review your submission. Thanks!
|
||||
23
venv/Lib/site-packages/Flask_Login-0.6.3.dist-info/RECORD
Normal file
@@ -0,0 +1,23 @@
|
||||
Flask_Login-0.6.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
Flask_Login-0.6.3.dist-info/LICENSE,sha256=ep37nF2iBO0TcPO2LBPimSoS2h2nB_R-FWiX7rQ0Tls,1059
|
||||
Flask_Login-0.6.3.dist-info/METADATA,sha256=AUSHR5Po6-Cwmz1KBrAZbTzR-iVVFvtb2NQKYl7UuAU,5799
|
||||
Flask_Login-0.6.3.dist-info/RECORD,,
|
||||
Flask_Login-0.6.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
Flask_Login-0.6.3.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
||||
Flask_Login-0.6.3.dist-info/top_level.txt,sha256=OuXmIpiFnXLvW-iBbW2km7ZIy5EZvwSBnYaOC3Kt7j8,12
|
||||
flask_login/__about__.py,sha256=Kkp5e9mV9G7vK_FqZof-g9RFmyyBzq1gge5aKXgvilE,389
|
||||
flask_login/__init__.py,sha256=wYQiQCikT_Ndp3PhOD-1gRTGCrUPIE-FrjQUrT9aVAg,2681
|
||||
flask_login/__pycache__/__about__.cpython-311.pyc,,
|
||||
flask_login/__pycache__/__init__.cpython-311.pyc,,
|
||||
flask_login/__pycache__/config.cpython-311.pyc,,
|
||||
flask_login/__pycache__/login_manager.cpython-311.pyc,,
|
||||
flask_login/__pycache__/mixins.cpython-311.pyc,,
|
||||
flask_login/__pycache__/signals.cpython-311.pyc,,
|
||||
flask_login/__pycache__/test_client.cpython-311.pyc,,
|
||||
flask_login/__pycache__/utils.cpython-311.pyc,,
|
||||
flask_login/config.py,sha256=YAocv18La7YGQyNY5aT7rU1GQIZnX6pvchwqx3kA9p8,1813
|
||||
flask_login/login_manager.py,sha256=h20F_iv3mqc6rIJ4-V6_XookzOUl8Rcpasua-dCByQY,20073
|
||||
flask_login/mixins.py,sha256=gPd7otMRljxw0eUhUMbHsnEBc_jK2cYdxg5KFLuJcoI,1528
|
||||
flask_login/signals.py,sha256=xCMoFHKU1RTVt1NY-Gfl0OiVKpiyNt6YJw_PsgkjY3w,2464
|
||||
flask_login/test_client.py,sha256=6mrjiBRLGJpgvvFlLypXPTBLiMp0BAN-Ft-uogqC81g,517
|
||||
flask_login/utils.py,sha256=Y1wxjCVxpYohBaQJ0ADLypQ-VvBNycwG-gVXFF7k99I,14021
|
||||
5
venv/Lib/site-packages/Flask_Login-0.6.3.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.41.3)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
flask_login
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
20
venv/Lib/site-packages/Flask_Migrate-4.1.0.dist-info/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Miguel Grinberg
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,91 @@
|
||||
Metadata-Version: 2.2
|
||||
Name: Flask-Migrate
|
||||
Version: 4.1.0
|
||||
Summary: SQLAlchemy database migrations for Flask applications using Alembic.
|
||||
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
||||
License: MIT
|
||||
Project-URL: Homepage, https://github.com/miguelgrinberg/flask-migrate
|
||||
Project-URL: Bug Tracker, https://github.com/miguelgrinberg/flask-migrate/issues
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Requires-Python: >=3.6
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: Flask>=0.9
|
||||
Requires-Dist: Flask-SQLAlchemy>=1.0
|
||||
Requires-Dist: alembic>=1.9.0
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: tox; extra == "dev"
|
||||
Requires-Dist: flake8; extra == "dev"
|
||||
Requires-Dist: pytest; extra == "dev"
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx; extra == "docs"
|
||||
|
||||
Flask-Migrate
|
||||
=============
|
||||
|
||||
[](https://github.com/miguelgrinberg/flask-migrate/actions)
|
||||
|
||||
Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic. The database operations are provided as command-line arguments under the `flask db` command.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install Flask-Migrate with `pip`:
|
||||
|
||||
pip install Flask-Migrate
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
This is an example application that handles database migrations through Flask-Migrate:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128))
|
||||
```
|
||||
|
||||
With the above application you can create the database or enable migrations if the database already exists with the following command:
|
||||
|
||||
$ flask db init
|
||||
|
||||
Note that the `FLASK_APP` environment variable must be set according to the Flask documentation for this command to work. This will add a `migrations` folder to your application. The contents of this folder need to be added to version control along with your other source files.
|
||||
|
||||
You can then generate an initial migration:
|
||||
|
||||
$ flask db migrate
|
||||
|
||||
The migration script needs to be reviewed and edited, as Alembic currently does not detect every change you make to your models. In particular, Alembic is currently unable to detect indexes. Once finalized, the migration script also needs to be added to version control.
|
||||
|
||||
Then you can apply the migration to the database:
|
||||
|
||||
$ flask db upgrade
|
||||
|
||||
Then each time the database models change repeat the `migrate` and `upgrade` commands.
|
||||
|
||||
To sync the database in another system just refresh the `migrations` folder from source control and run the `upgrade` command.
|
||||
|
||||
To see all the commands that are available run this command:
|
||||
|
||||
$ flask db --help
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
- [Documentation](http://flask-migrate.readthedocs.io/en/latest/)
|
||||
- [pypi](https://pypi.python.org/pypi/Flask-Migrate)
|
||||
- [Change Log](https://github.com/miguelgrinberg/Flask-Migrate/blob/master/CHANGES.md)
|
||||
31
venv/Lib/site-packages/Flask_Migrate-4.1.0.dist-info/RECORD
Normal file
@@ -0,0 +1,31 @@
|
||||
Flask_Migrate-4.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
Flask_Migrate-4.1.0.dist-info/LICENSE,sha256=kfkXGlJQvKy3Y__6tAJ8ynIp1HQfeROXhL8jZU1d-DI,1082
|
||||
Flask_Migrate-4.1.0.dist-info/METADATA,sha256=jifIy8PzfDzjuCEeKLDKRJA8O56KOgLfj3s2lmzZA-8,3289
|
||||
Flask_Migrate-4.1.0.dist-info/RECORD,,
|
||||
Flask_Migrate-4.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
Flask_Migrate-4.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
||||
Flask_Migrate-4.1.0.dist-info/top_level.txt,sha256=jLoPgiMG6oR4ugNteXn3IHskVVIyIXVStZOVq-AWLdU,14
|
||||
flask_migrate/__init__.py,sha256=JMySGA55Y8Gxy3HviWu7qq5rPUNQBWc2NID2OicpDyw,10082
|
||||
flask_migrate/__pycache__/__init__.cpython-311.pyc,,
|
||||
flask_migrate/__pycache__/cli.cpython-311.pyc,,
|
||||
flask_migrate/cli.py,sha256=IxrxBSC82S5sPfWac8Qg83_FVsRvqTYtCG7HRyMW8RU,11097
|
||||
flask_migrate/templates/aioflask-multidb/README,sha256=Ek4cJqTaxneVjtkue--BXMlfpfp3MmJRjqoZvnSizww,43
|
||||
flask_migrate/templates/aioflask-multidb/__pycache__/env.cpython-311.pyc,,
|
||||
flask_migrate/templates/aioflask-multidb/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857
|
||||
flask_migrate/templates/aioflask-multidb/env.py,sha256=UcjeqkAbyUjTkuQFmCFPG7QOvqhco8-uGp8QEbto0T8,6573
|
||||
flask_migrate/templates/aioflask-multidb/script.py.mako,sha256=198VPxVEN3NZ3vHcRuCxSoI4XnOYirGWt01qkbPKoJw,1246
|
||||
flask_migrate/templates/aioflask/README,sha256=KKqWGl4YC2RqdOdq-y6quTDW0b7D_UZNHuM8glM1L-c,44
|
||||
flask_migrate/templates/aioflask/__pycache__/env.cpython-311.pyc,,
|
||||
flask_migrate/templates/aioflask/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857
|
||||
flask_migrate/templates/aioflask/env.py,sha256=m6ZtBhdpwuq89vVeLTWmNT-1NfJZqarC_hsquCdR9bw,3478
|
||||
flask_migrate/templates/aioflask/script.py.mako,sha256=8_xgA-gm_OhehnO7CiIijWgnm00ZlszEHtIHrAYFJl0,494
|
||||
flask_migrate/templates/flask-multidb/README,sha256=AfiP5foaV2odZxXxuUuSIS6YhkIpR7CsOo2mpuxwHdc,40
|
||||
flask_migrate/templates/flask-multidb/__pycache__/env.cpython-311.pyc,,
|
||||
flask_migrate/templates/flask-multidb/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857
|
||||
flask_migrate/templates/flask-multidb/env.py,sha256=F44iqsAxLTVBN_zD8CMUkdE7Aub4niHMmo5wl9mY4Uw,6190
|
||||
flask_migrate/templates/flask-multidb/script.py.mako,sha256=198VPxVEN3NZ3vHcRuCxSoI4XnOYirGWt01qkbPKoJw,1246
|
||||
flask_migrate/templates/flask/README,sha256=JL0NrjOrscPcKgRmQh1R3hlv1_rohDot0TvpmdM27Jk,41
|
||||
flask_migrate/templates/flask/__pycache__/env.cpython-311.pyc,,
|
||||
flask_migrate/templates/flask/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857
|
||||
flask_migrate/templates/flask/env.py,sha256=ibK1hsdOsOBzXNU2yQoAIza7f_EFzaVSWwON_NSpNzQ,3344
|
||||
flask_migrate/templates/flask/script.py.mako,sha256=8_xgA-gm_OhehnO7CiIijWgnm00ZlszEHtIHrAYFJl0,494
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.8.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
flask_migrate
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
92
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/METADATA
Normal file
@@ -0,0 +1,92 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: MarkupSafe
|
||||
Version: 3.0.2
|
||||
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||
Maintainer-email: Pallets <contact@palletsprojects.com>
|
||||
License: Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||
Project-URL: Source, https://github.com/pallets/markupsafe/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE.txt
|
||||
|
||||
# MarkupSafe
|
||||
|
||||
MarkupSafe implements a text object that escapes characters so it is
|
||||
safe to use in HTML and XML. Characters that have special meanings are
|
||||
replaced so that they display as the actual characters. This mitigates
|
||||
injection attacks, meaning untrusted user input can safely be displayed
|
||||
on a page.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```pycon
|
||||
>>> from markupsafe import Markup, escape
|
||||
|
||||
>>> # escape replaces special characters and wraps in Markup
|
||||
>>> escape("<script>alert(document.cookie);</script>")
|
||||
Markup('<script>alert(document.cookie);</script>')
|
||||
|
||||
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||
>>> Markup("<strong>Hello</strong>")
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> escape(Markup("<strong>Hello</strong>"))
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> # Markup is a str subclass
|
||||
>>> # methods and operators escape their arguments
|
||||
>>> template = Markup("Hello <em>{name}</em>")
|
||||
>>> template.format(name='"World"')
|
||||
Markup('Hello <em>"World"</em>')
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports MarkupSafe and other
|
||||
popular packages. In order to grow the community of contributors and
|
||||
users, and allow the maintainers to devote more time to the projects,
|
||||
[please donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
14
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/RECORD
Normal file
@@ -0,0 +1,14 @@
|
||||
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=RjHsDbX9kKVH4zaBcmTGeYIUM4FG-KyUtKV_lu6MnsQ,1503
|
||||
MarkupSafe-3.0.2.dist-info/METADATA,sha256=nhoabjupBG41j_JxPCJ3ylgrZ6Fx8oMCFbiLF9Kafqc,4067
|
||||
MarkupSafe-3.0.2.dist-info/RECORD,,
|
||||
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=tE2EWZPEv-G0fjAlUUz7IGM64246YKD9fpv4HcsDMkk,101
|
||||
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||
markupsafe/__init__.py,sha256=pREerPwvinB62tNCMOwqxBS2YHV6R52Wcq1d-rB4Z5o,13609
|
||||
markupsafe/__pycache__/__init__.cpython-311.pyc,,
|
||||
markupsafe/__pycache__/_native.cpython-311.pyc,,
|
||||
markupsafe/_native.py,sha256=2ptkJ40yCcp9kq3L1NqpgjfpZB-obniYKFFKUOkHh4Q,218
|
||||
markupsafe/_speedups.c,sha256=SglUjn40ti9YgQAO--OgkSyv9tXq9vvaHyVhQows4Ok,4353
|
||||
markupsafe/_speedups.cp311-win_amd64.pyd,sha256=-5qfBr0xMpiTRlH9hFg_7Go9PHi7z5guMzmbbmZI3Xw,13312
|
||||
markupsafe/_speedups.pyi,sha256=LSDmXYOefH4HVpAXuL8sl7AttLw0oXh1njVoVZp2wqQ,42
|
||||
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
5
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.2.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp311-cp311-win_amd64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
markupsafe
|
||||
222
venv/Lib/site-packages/_distutils_hack/__init__.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# don't import any costly modules
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
is_pypy = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
if is_pypy and sys.version_info < (3, 7):
|
||||
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
|
||||
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils."
|
||||
)
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn("Setuptools is replacing distutils.")
|
||||
mods = [
|
||||
name
|
||||
for name in sys.modules
|
||||
if name == "distutils" or name.startswith("distutils.")
|
||||
]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
import importlib
|
||||
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
with shim():
|
||||
importlib.import_module('distutils')
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
assert 'setuptools._distutils.log' not in sys.modules
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class _TrivialRe:
|
||||
def __init__(self, *patterns):
|
||||
self._patterns = patterns
|
||||
|
||||
def match(self, string):
|
||||
return all(pat in string for pat in self._patterns)
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# optimization: only consider top level modules and those
|
||||
# found in the CPython test suite.
|
||||
if path is not None and not fullname.startswith('test.'):
|
||||
return
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
if self.is_cpython():
|
||||
return
|
||||
|
||||
import importlib
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
mod = importlib.import_module('setuptools._distutils')
|
||||
except Exception:
|
||||
# There are a couple of cases where setuptools._distutils
|
||||
# may not be present:
|
||||
# - An older Setuptools without a local distutils is
|
||||
# taking precedence. Ref #2957.
|
||||
# - Path manipulation during sitecustomize removes
|
||||
# setuptools from the path but only after the hook
|
||||
# has been loaded. Ref #2980.
|
||||
# In either case, fall back to stdlib behavior.
|
||||
return
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mod.__name__ = 'distutils'
|
||||
return mod
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader(
|
||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_cpython():
|
||||
"""
|
||||
Suppress supplying distutils for CPython (build and tests).
|
||||
Ref #2965 and #3007.
|
||||
"""
|
||||
return os.path.isfile('pybuilddir.txt')
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@classmethod
|
||||
def pip_imported_during_build(cls):
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
return any(
|
||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def frame_file_is_setup(frame):
|
||||
"""
|
||||
Return True if the indicated frame suggests a setup.py file.
|
||||
"""
|
||||
# some frames may not have __file__ (#2940)
|
||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||
|
||||
def spec_for_sensitive_tests(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running select tests under CPython.
|
||||
|
||||
python/cpython#91169
|
||||
"""
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
sensitive_tests = (
|
||||
[
|
||||
'test.test_distutils',
|
||||
'test.test_peg_generator',
|
||||
'test.test_importlib',
|
||||
]
|
||||
if sys.version_info < (3, 10)
|
||||
else [
|
||||
'test.test_distutils',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
for name in DistutilsMetaFinder.sensitive_tests:
|
||||
setattr(
|
||||
DistutilsMetaFinder,
|
||||
f'spec_for_{name}',
|
||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||
|
||||
|
||||
class shim:
|
||||
def __enter__(self):
|
||||
insert_shim()
|
||||
|
||||
def __exit__(self, exc, value, tb):
|
||||
remove_shim()
|
||||
|
||||
|
||||
def insert_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
1
venv/Lib/site-packages/_distutils_hack/override.py
Normal file
@@ -0,0 +1 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
140
venv/Lib/site-packages/alembic-1.16.4.dist-info/METADATA
Normal file
@@ -0,0 +1,140 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: alembic
|
||||
Version: 1.16.4
|
||||
Summary: A database migration tool for SQLAlchemy.
|
||||
Author-email: Mike Bayer <mike_mp@zzzcomputing.com>
|
||||
License-Expression: MIT
|
||||
Project-URL: Homepage, https://alembic.sqlalchemy.org
|
||||
Project-URL: Documentation, https://alembic.sqlalchemy.org/en/latest/
|
||||
Project-URL: Changelog, https://alembic.sqlalchemy.org/en/latest/changelog.html
|
||||
Project-URL: Source, https://github.com/sqlalchemy/alembic/
|
||||
Project-URL: Issue Tracker, https://github.com/sqlalchemy/alembic/issues/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Database :: Front-Ends
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: SQLAlchemy>=1.4.0
|
||||
Requires-Dist: Mako
|
||||
Requires-Dist: typing-extensions>=4.12
|
||||
Requires-Dist: tomli; python_version < "3.11"
|
||||
Provides-Extra: tz
|
||||
Requires-Dist: tzdata; extra == "tz"
|
||||
Dynamic: license-file
|
||||
|
||||
Alembic is a database migrations tool written by the author
|
||||
of `SQLAlchemy <http://www.sqlalchemy.org>`_. A migrations tool
|
||||
offers the following functionality:
|
||||
|
||||
* Can emit ALTER statements to a database in order to change
|
||||
the structure of tables and other constructs
|
||||
* Provides a system whereby "migration scripts" may be constructed;
|
||||
each script indicates a particular series of steps that can "upgrade" a
|
||||
target database to a new version, and optionally a series of steps that can
|
||||
"downgrade" similarly, doing the same steps in reverse.
|
||||
* Allows the scripts to execute in some sequential manner.
|
||||
|
||||
The goals of Alembic are:
|
||||
|
||||
* Very open ended and transparent configuration and operation. A new
|
||||
Alembic environment is generated from a set of templates which is selected
|
||||
among a set of options when setup first occurs. The templates then deposit a
|
||||
series of scripts that define fully how database connectivity is established
|
||||
and how migration scripts are invoked; the migration scripts themselves are
|
||||
generated from a template within that series of scripts. The scripts can
|
||||
then be further customized to define exactly how databases will be
|
||||
interacted with and what structure new migration files should take.
|
||||
* Full support for transactional DDL. The default scripts ensure that all
|
||||
migrations occur within a transaction - for those databases which support
|
||||
this (Postgresql, Microsoft SQL Server), migrations can be tested with no
|
||||
need to manually undo changes upon failure.
|
||||
* Minimalist script construction. Basic operations like renaming
|
||||
tables/columns, adding/removing columns, changing column attributes can be
|
||||
performed through one line commands like alter_column(), rename_table(),
|
||||
add_constraint(). There is no need to recreate full SQLAlchemy Table
|
||||
structures for simple operations like these - the functions themselves
|
||||
generate minimalist schema structures behind the scenes to achieve the given
|
||||
DDL sequence.
|
||||
* "auto generation" of migrations. While real world migrations are far more
|
||||
complex than what can be automatically determined, Alembic can still
|
||||
eliminate the initial grunt work in generating new migration directives
|
||||
from an altered schema. The ``--autogenerate`` feature will inspect the
|
||||
current status of a database using SQLAlchemy's schema inspection
|
||||
capabilities, compare it to the current state of the database model as
|
||||
specified in Python, and generate a series of "candidate" migrations,
|
||||
rendering them into a new migration script as Python directives. The
|
||||
developer then edits the new file, adding additional directives and data
|
||||
migrations as needed, to produce a finished migration. Table and column
|
||||
level changes can be detected, with constraints and indexes to follow as
|
||||
well.
|
||||
* Full support for migrations generated as SQL scripts. Those of us who
|
||||
work in corporate environments know that direct access to DDL commands on a
|
||||
production database is a rare privilege, and DBAs want textual SQL scripts.
|
||||
Alembic's usage model and commands are oriented towards being able to run a
|
||||
series of migrations into a textual output file as easily as it runs them
|
||||
directly to a database. Care must be taken in this mode to not invoke other
|
||||
operations that rely upon in-memory SELECTs of rows - Alembic tries to
|
||||
provide helper constructs like bulk_insert() to help with data-oriented
|
||||
operations that are compatible with script-based DDL.
|
||||
* Non-linear, dependency-graph versioning. Scripts are given UUID
|
||||
identifiers similarly to a DVCS, and the linkage of one script to the next
|
||||
is achieved via human-editable markers within the scripts themselves.
|
||||
The structure of a set of migration files is considered as a
|
||||
directed-acyclic graph, meaning any migration file can be dependent
|
||||
on any other arbitrary set of migration files, or none at
|
||||
all. Through this open-ended system, migration files can be organized
|
||||
into branches, multiple roots, and mergepoints, without restriction.
|
||||
Commands are provided to produce new branches, roots, and merges of
|
||||
branches automatically.
|
||||
* Provide a library of ALTER constructs that can be used by any SQLAlchemy
|
||||
application. The DDL constructs build upon SQLAlchemy's own DDLElement base
|
||||
and can be used standalone by any application or script.
|
||||
* At long last, bring SQLite and its inability to ALTER things into the fold,
|
||||
but in such a way that SQLite's very special workflow needs are accommodated
|
||||
in an explicit way that makes the most of a bad situation, through the
|
||||
concept of a "batch" migration, where multiple changes to a table can
|
||||
be batched together to form a series of instructions for a single, subsequent
|
||||
"move-and-copy" workflow. You can even use "move-and-copy" workflow for
|
||||
other databases, if you want to recreate a table in the background
|
||||
on a busy system.
|
||||
|
||||
Documentation and status of Alembic is at https://alembic.sqlalchemy.org/
|
||||
|
||||
The SQLAlchemy Project
|
||||
======================
|
||||
|
||||
Alembic is part of the `SQLAlchemy Project <https://www.sqlalchemy.org>`_ and
|
||||
adheres to the same standards and conventions as the core project.
|
||||
|
||||
Development / Bug reporting / Pull requests
|
||||
___________________________________________
|
||||
|
||||
Please refer to the
|
||||
`SQLAlchemy Community Guide <https://www.sqlalchemy.org/develop.html>`_ for
|
||||
guidelines on coding and participating in this project.
|
||||
|
||||
Code of Conduct
|
||||
_______________
|
||||
|
||||
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
|
||||
constructive communication between users and developers.
|
||||
Please see our current Code of Conduct at
|
||||
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Alembic is distributed under the `MIT license
|
||||
<https://opensource.org/licenses/MIT>`_.
|
||||
162
venv/Lib/site-packages/alembic-1.16.4.dist-info/RECORD
Normal file
@@ -0,0 +1,162 @@
|
||||
../../Scripts/alembic.exe,sha256=kg4OoHQLGwbKYDijEitu59R9DHwOxBU8iH-3haB5TFM,108431
|
||||
alembic-1.16.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
alembic-1.16.4.dist-info/METADATA,sha256=FOov3ImluFXi-Y-Zod8XhsXQ9xlB-lbP1V6WKMjhJ6c,7265
|
||||
alembic-1.16.4.dist-info/RECORD,,
|
||||
alembic-1.16.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
||||
alembic-1.16.4.dist-info/entry_points.txt,sha256=aykM30soxwGN0pB7etLc1q0cHJbL9dy46RnK9VX4LLw,48
|
||||
alembic-1.16.4.dist-info/licenses/LICENSE,sha256=NeqcNBmyYfrxvkSMT0fZJVKBv2s2tf_qVQUiJ9S6VN4,1059
|
||||
alembic-1.16.4.dist-info/top_level.txt,sha256=FwKWd5VsPFC8iQjpu1u9Cn-JnK3-V1RhUCmWqz1cl-s,8
|
||||
alembic/__init__.py,sha256=LBJqQA9P3cs5iDeqKVCK3DsoFQfdhd1cvtctowCUd0g,63
|
||||
alembic/__main__.py,sha256=373m7-TBh72JqrSMYviGrxCHZo-cnweM8AGF8A22PmY,78
|
||||
alembic/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/__pycache__/__main__.cpython-311.pyc,,
|
||||
alembic/__pycache__/command.cpython-311.pyc,,
|
||||
alembic/__pycache__/config.cpython-311.pyc,,
|
||||
alembic/__pycache__/context.cpython-311.pyc,,
|
||||
alembic/__pycache__/environment.cpython-311.pyc,,
|
||||
alembic/__pycache__/migration.cpython-311.pyc,,
|
||||
alembic/__pycache__/op.cpython-311.pyc,,
|
||||
alembic/autogenerate/__init__.py,sha256=ntmUTXhjLm4_zmqIwyVaECdpPDn6_u1yM9vYk6-553E,543
|
||||
alembic/autogenerate/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/autogenerate/__pycache__/api.cpython-311.pyc,,
|
||||
alembic/autogenerate/__pycache__/compare.cpython-311.pyc,,
|
||||
alembic/autogenerate/__pycache__/render.cpython-311.pyc,,
|
||||
alembic/autogenerate/__pycache__/rewriter.cpython-311.pyc,,
|
||||
alembic/autogenerate/api.py,sha256=L4qkapSJO1Ypymx8HsjLl0vFFt202agwMYsQbIe6ZtI,22219
|
||||
alembic/autogenerate/compare.py,sha256=LRTxNijEBvcTauuUXuJjC6Sg_gUn33FCYBTF0neZFwE,45979
|
||||
alembic/autogenerate/render.py,sha256=ceQL8nk8m2kBtQq5gtxtDLR9iR0Sck8xG_61Oez-Sqs,37270
|
||||
alembic/autogenerate/rewriter.py,sha256=NIASSS-KaNKPmbm1k4pE45aawwjSh1Acf6eZrOwnUGM,7814
|
||||
alembic/command.py,sha256=pZPQUGSxCjFu7qy0HMe02HJmByM0LOqoiK2AXKfRO3A,24855
|
||||
alembic/config.py,sha256=SbOhoGuXlh_vVpK3SD8LFUG_BNH5HDLv6Q9949HCiXA,34124
|
||||
alembic/context.py,sha256=hK1AJOQXJ29Bhn276GYcosxeG7pC5aZRT5E8c4bMJ4Q,195
|
||||
alembic/context.pyi,sha256=fdeFNTRc0bUgi7n2eZWVFh6NG-TzIv_0gAcapbfHnKY,31773
|
||||
alembic/ddl/__init__.py,sha256=Df8fy4Vn_abP8B7q3x8gyFwEwnLw6hs2Ljt_bV3EZWE,152
|
||||
alembic/ddl/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/_autogen.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/base.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/impl.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/mssql.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/mysql.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/oracle.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/postgresql.cpython-311.pyc,,
|
||||
alembic/ddl/__pycache__/sqlite.cpython-311.pyc,,
|
||||
alembic/ddl/_autogen.py,sha256=Blv2RrHNyF4cE6znCQXNXG5T9aO-YmiwD4Fz-qfoaWA,9275
|
||||
alembic/ddl/base.py,sha256=A1f89-rCZvqw-hgWmBbIszRqx94lL6gKLFXE9kHettA,10478
|
||||
alembic/ddl/impl.py,sha256=UL8-iza7CJk_T73lr5fjDLdhxEL56uD-AEjtmESAbLk,30439
|
||||
alembic/ddl/mssql.py,sha256=NzORSIDHUll_g6iH4IyMTXZU1qjKzXrpespKrjWnfLY,14216
|
||||
alembic/ddl/mysql.py,sha256=ujY4xDh13KgiFNRe3vUcLquie0ih80MYBUcogCBPdSc,17358
|
||||
alembic/ddl/oracle.py,sha256=669YlkcZihlXFbnXhH2krdrvDry8q5pcUGfoqkg_R6Y,6243
|
||||
alembic/ddl/postgresql.py,sha256=S7uye2NDSHLwV3w8SJ2Q9DLbcvQIxQfJ3EEK6JqyNag,29950
|
||||
alembic/ddl/sqlite.py,sha256=u5tJgRUiY6bzVltl_NWlI6cy23v8XNagk_9gPI6Lnns,8006
|
||||
alembic/environment.py,sha256=MM5lPayGT04H3aeng1H7GQ8HEAs3VGX5yy6mDLCPLT4,43
|
||||
alembic/migration.py,sha256=MV6Fju6rZtn2fTREKzXrCZM6aIBGII4OMZFix0X-GLs,41
|
||||
alembic/op.py,sha256=flHtcsVqOD-ZgZKK2pv-CJ5Cwh-KJ7puMUNXzishxLw,167
|
||||
alembic/op.pyi,sha256=PQ4mKNp7EXrjVdIWQRoGiBSVke4PPxTc9I6qF8ZGGZE,50711
|
||||
alembic/operations/__init__.py,sha256=e0KQSZAgLpTWvyvreB7DWg7RJV_MWSOPVDgCqsd2FzY,318
|
||||
alembic/operations/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/operations/__pycache__/base.cpython-311.pyc,,
|
||||
alembic/operations/__pycache__/batch.cpython-311.pyc,,
|
||||
alembic/operations/__pycache__/ops.cpython-311.pyc,,
|
||||
alembic/operations/__pycache__/schemaobj.cpython-311.pyc,,
|
||||
alembic/operations/__pycache__/toimpl.cpython-311.pyc,,
|
||||
alembic/operations/base.py,sha256=npw1iFboTlEsaQS0b7mb2SEHsRDV4GLQqnjhcfma6Nk,75157
|
||||
alembic/operations/batch.py,sha256=1UmCFcsFWObinQWFRWoGZkjynl54HKpldbPs67aR4wg,26923
|
||||
alembic/operations/ops.py,sha256=ftsFgcZIctxRDiuGgkQsaFHsMlRP7cLq7Dj_seKVBnQ,96276
|
||||
alembic/operations/schemaobj.py,sha256=Wp-bBe4a8lXPTvIHJttBY0ejtpVR5Jvtb2kI-U2PztQ,9468
|
||||
alembic/operations/toimpl.py,sha256=rgufuSUNwpgrOYzzY3Q3ELW1rQv2fQbQVokXgnIYIrs,7503
|
||||
alembic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic/runtime/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/runtime/__pycache__/environment.cpython-311.pyc,,
|
||||
alembic/runtime/__pycache__/migration.cpython-311.pyc,,
|
||||
alembic/runtime/environment.py,sha256=L6bDW1dvw8L4zwxlTG8KnT0xcCgLXxUfdRpzqlJoFjo,41479
|
||||
alembic/runtime/migration.py,sha256=lu9_z_qyWmNzSM52_FgdXP_G52PTmTTeOeMBQAkQTFg,49997
|
||||
alembic/script/__init__.py,sha256=lSj06O391Iy5avWAiq8SPs6N8RBgxkSPjP8wpXcNDGg,100
|
||||
alembic/script/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/script/__pycache__/base.cpython-311.pyc,,
|
||||
alembic/script/__pycache__/revision.cpython-311.pyc,,
|
||||
alembic/script/__pycache__/write_hooks.cpython-311.pyc,,
|
||||
alembic/script/base.py,sha256=4jINClsNNwQIvnf4Kwp9JPAMrANLXdLItylXmcMqAkI,36896
|
||||
alembic/script/revision.py,sha256=BQcJoMCIXtSJRLCvdasgLOtCx9O7A8wsSym1FsqLW4s,62307
|
||||
alembic/script/write_hooks.py,sha256=uQWAtguSCrxU_k9d87NX19y6EzyjJRRQ5HS9cyPnK9o,5092
|
||||
alembic/templates/async/README,sha256=ISVtAOvqvKk_5ThM5ioJE-lMkvf9IbknFUFVU_vPma4,58
|
||||
alembic/templates/async/__pycache__/env.cpython-311.pyc,,
|
||||
alembic/templates/async/alembic.ini.mako,sha256=Bgi4WkaHYsT7xvsX-4WOGkcXKFroNoQLaUvZA23ZwGs,4864
|
||||
alembic/templates/async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389
|
||||
alembic/templates/async/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/templates/generic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
|
||||
alembic/templates/generic/__pycache__/env.cpython-311.pyc,,
|
||||
alembic/templates/generic/alembic.ini.mako,sha256=LCpLL02bi9Qr3KRTEj9NbQqAu0ckUmYBwPtrMtQkv-Y,4864
|
||||
alembic/templates/generic/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103
|
||||
alembic/templates/generic/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/templates/multidb/README,sha256=dWLDhnBgphA4Nzb7sNlMfCS3_06YqVbHhz-9O5JNqyI,606
|
||||
alembic/templates/multidb/__pycache__/env.cpython-311.pyc,,
|
||||
alembic/templates/multidb/alembic.ini.mako,sha256=rIp1LTdE1xcoFT2G7X72KshzYjUTRrHTvnkvFL___-8,5190
|
||||
alembic/templates/multidb/env.py,sha256=6zNjnW8mXGUk7erTsAvrfhvqoczJ-gagjVq1Ypg2YIQ,4230
|
||||
alembic/templates/multidb/script.py.mako,sha256=ZbCXMkI5Wj2dwNKcxuVGkKZ7Iav93BNx_bM4zbGi3c8,1235
|
||||
alembic/templates/pyproject/README,sha256=dMhIiFoeM7EdeaOXBs3mVQ6zXACMyGXDb_UBB6sGRA0,60
|
||||
alembic/templates/pyproject/__pycache__/env.cpython-311.pyc,,
|
||||
alembic/templates/pyproject/alembic.ini.mako,sha256=bQnEoydnLOUgg9vNbTOys4r5MaW8lmwYFXSrlfdEEkw,782
|
||||
alembic/templates/pyproject/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103
|
||||
alembic/templates/pyproject/pyproject.toml.mako,sha256=Gf16ZR9OMG9zDlFO5PVQlfiL1DTKwSA--sTNzK7Lba0,2852
|
||||
alembic/templates/pyproject/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/templates/pyproject_async/README,sha256=2Q5XcEouiqQ-TJssO9805LROkVUd0F6d74rTnuLrifA,45
|
||||
alembic/templates/pyproject_async/__pycache__/env.cpython-311.pyc,,
|
||||
alembic/templates/pyproject_async/alembic.ini.mako,sha256=bQnEoydnLOUgg9vNbTOys4r5MaW8lmwYFXSrlfdEEkw,782
|
||||
alembic/templates/pyproject_async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389
|
||||
alembic/templates/pyproject_async/pyproject.toml.mako,sha256=Gf16ZR9OMG9zDlFO5PVQlfiL1DTKwSA--sTNzK7Lba0,2852
|
||||
alembic/templates/pyproject_async/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/testing/__init__.py,sha256=PTMhi_2PZ1T_3atQS2CIr0V4YRZzx_doKI-DxKdQS44,1297
|
||||
alembic/testing/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/assertions.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/env.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/fixtures.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/requirements.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/schemacompare.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/util.cpython-311.pyc,,
|
||||
alembic/testing/__pycache__/warnings.cpython-311.pyc,,
|
||||
alembic/testing/assertions.py,sha256=qcqf3tRAUe-A12NzuK_yxlksuX9OZKRC5E8pKIdBnPg,5302
|
||||
alembic/testing/env.py,sha256=pka7fjwOC8hYL6X0XE4oPkJpy_1WX01bL7iP7gpO_4I,11551
|
||||
alembic/testing/fixtures.py,sha256=fOzsRF8SW6CWpAH0sZpUHcgsJjun9EHnp4k2S3Lq5eU,9920
|
||||
alembic/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic/testing/plugin/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/testing/plugin/__pycache__/bootstrap.cpython-311.pyc,,
|
||||
alembic/testing/plugin/bootstrap.py,sha256=9C6wtjGrIVztZ928w27hsQE0KcjDLIUtUN3dvZKsMVk,50
|
||||
alembic/testing/requirements.py,sha256=gNnnvgPCuiqKeHmiNymdQuYIjQ0BrxiPxu_in4eHEsc,4180
|
||||
alembic/testing/schemacompare.py,sha256=N5UqSNCOJetIKC4vKhpYzQEpj08XkdgIoqBmEPQ3tlc,4838
|
||||
alembic/testing/suite/__init__.py,sha256=MvE7-hwbaVN1q3NM-ztGxORU9dnIelUCINKqNxewn7Y,288
|
||||
alembic/testing/suite/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/_autogen_fixtures.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_comments.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_computed.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_diffs.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_fks.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_identity.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_environment.cpython-311.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_op.cpython-311.pyc,,
|
||||
alembic/testing/suite/_autogen_fixtures.py,sha256=Drrz_FKb9KDjq8hkwxtPkJVY1sCY7Biw-Muzb8kANp8,13480
|
||||
alembic/testing/suite/test_autogen_comments.py,sha256=aEGqKUDw4kHjnDk298aoGcQvXJWmZXcIX_2FxH4cJK8,6283
|
||||
alembic/testing/suite/test_autogen_computed.py,sha256=-5wran56qXo3afAbSk8cuSDDpbQweyJ61RF-GaVuZbA,4126
|
||||
alembic/testing/suite/test_autogen_diffs.py,sha256=T4SR1n_kmcOKYhR4W1-dA0e5sddJ69DSVL2HW96kAkE,8394
|
||||
alembic/testing/suite/test_autogen_fks.py,sha256=AqFmb26Buex167HYa9dZWOk8x-JlB1OK3bwcvvjDFaU,32927
|
||||
alembic/testing/suite/test_autogen_identity.py,sha256=kcuqngG7qXAKPJDX4U8sRzPKHEJECHuZ0DtuaS6tVkk,5824
|
||||
alembic/testing/suite/test_environment.py,sha256=OwD-kpESdLoc4byBrGrXbZHvqtPbzhFCG4W9hJOJXPQ,11877
|
||||
alembic/testing/suite/test_op.py,sha256=2XQCdm_NmnPxHGuGj7hmxMzIhKxXNotUsKdACXzE1mM,1343
|
||||
alembic/testing/util.py,sha256=CQrcQDA8fs_7ME85z5ydb-Bt70soIIID-qNY1vbR2dg,3350
|
||||
alembic/testing/warnings.py,sha256=cDDWzvxNZE6x9dME2ACTXSv01G81JcIbE1GIE_s1kvg,831
|
||||
alembic/util/__init__.py,sha256=_Zj_xp6ssKLyoLHUFzmKhnc8mhwXW8D8h7qyX-wO56M,1519
|
||||
alembic/util/__pycache__/__init__.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/compat.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/editor.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/exc.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/langhelpers.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/messaging.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/pyfiles.cpython-311.pyc,,
|
||||
alembic/util/__pycache__/sqla_compat.cpython-311.pyc,,
|
||||
alembic/util/compat.py,sha256=Vt5xCn5Y675jI4seKNBV4IVnCl9V4wyH3OBI2w7U0EY,4248
|
||||
alembic/util/editor.py,sha256=JIz6_BdgV8_oKtnheR6DZoB7qnrHrlRgWjx09AsTsUw,2546
|
||||
alembic/util/exc.py,sha256=ZBlTQ8g-Jkb1iYFhFHs9djilRz0SSQ0Foc5SSoENs5o,564
|
||||
alembic/util/langhelpers.py,sha256=LpOcovnhMnP45kTt8zNJ4BHpyQrlF40OL6yDXjqKtsE,10026
|
||||
alembic/util/messaging.py,sha256=3bEBoDy4EAXETXAvArlYjeMITXDTgPTu6ZoE3ytnzSw,3294
|
||||
alembic/util/pyfiles.py,sha256=kOBjZEytRkBKsQl0LAj2sbKJMQazjwQ_5UeMKSIvVFo,4730
|
||||
alembic/util/sqla_compat.py,sha256=9OYPTf-GCultAIuv1PoiaqYXAApZQxUOqjrOaeJDAik,14790
|
||||
5
venv/Lib/site-packages/alembic-1.16.4.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (80.9.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||