Commit df2617f1 authored by Willard's avatar Willard

Initial commit

parents
.vscode
migrations
venv
canteeneo/static/uploads
*.pyc
\ No newline at end of file
import os.path
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
app = Flask(__name__)
app.config['SECRET_KEY'] = 'canteeneo_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:therootisdead@localhost/canteeneo'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = os.path.realpath('.\\canteeneo\\static\uploads')
login_manager = LoginManager()
login_manager.init_app(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
import models
import controllers
\ No newline at end of file
import os.path
from canteeneo import app, login_manager, db
from flask import render_template, flash, redirect, request, url_for, jsonify
from flask_login import login_required, login_user, logout_user, current_user
from forms import OwnerLoginForm, OwnerRegisterForm, StallRegisterForm, DishRegisterForm
from models import Owner, Stall, Location, Dish
from werkzeug.utils import secure_filename
from datetime import datetime
@app.route('/')
def index():
if current_user.is_authenticated:
return redirect(url_for('stalls'))
else:
return render_template('landing.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('stalls'))
form = OwnerLoginForm()
if request.method == 'POST':
is_valid, owner = form.validate()
if is_valid:
login_user(owner)
return redirect(url_for('stalls'))
else:
flash_form_errors(form)
return render_template('login.html', form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect('/')
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('stalls'))
form = OwnerRegisterForm()
if request.method == 'POST':
if form.validate():
owner = Owner(form.name.data, form.username.data, form.email.data, form.password.data)
db.session.add(owner)
db.session.commit()
return redirect(url_for('login'))
else:
flash_form_errors(form)
return render_template('register.html', form=form)
@app.route('/stalls')
@login_required
def stalls():
return render_template('stalls.html', owner=current_user, stalls=current_user.stalls.all())
@app.route('/stalls/<int:stall_id>')
@login_required
def stall(stall_id):
stall = Stall.query.filter_by(id=int(stall_id)).first()
if stall is None or stall.owner != current_user:
return redirect(url_for('stalls'))
dishes = stall.dishes.all()
return render_template('stall.html', stall=stall, dishes=dishes, upload_folder=app.config['UPLOAD_FOLDER'])
@app.route('/stalls/new', methods=["GET","POST"])
@login_required
def new_stall():
form = StallRegisterForm()
form.location.choices = [(loc.id, loc.name) for loc in Location.query.all()]
if request.method == 'POST':
if form.validate():
stall = Stall(form.name.data, form.description.data, current_user.id, form.location.data)
db.session.add(stall)
db.session.commit()
return redirect(url_for('stalls'))
else:
flash_form_errors(form)
return redirect(url_for('new_stall'))
return render_template('newstall.html', form=form)
@app.route('/stalls/<int:stall_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_stall(stall_id):
stall = Stall.query.filter_by(id=int(stall_id)).first()
if stall is None or stall.owner != current_user:
return redirect(url_to('stalls'))
form = StallRegisterForm()
form.location.choices = [(loc.id, loc.name) for loc in Location.query.all()]
if request.method == 'POST':
if form.validate(editing=True):
stall.name = form.name.data
stall.description = form.description.data
stall.location_id = form.location.data
db.session.commit()
return redirect(url_for('stalls'))
else:
flash_form_errors(form)
return redirect(url_for('edit_stall', values=[('stall_id', stall_id)]))
else:
form.name.data = stall.name
form.description.data = stall.description
form.location.data = stall.location_id
return render_template('editstall.html', form=form, stall=stall)
@app.route('/stalls/<int:stall_id>/delete', methods=['POST'])
@login_required
def delete_stall(stall_id):
stall = Stall.query.filter_by(id=int(stall_id)).first()
if stall is None or stall.owner != current_user:
return redirect(url_for('stalls'))
db.session.delete(stall)
db.session.commit()
return redirect(url_for('stalls'))
@app.route('/stalls/<int:stall_id>/dish/new', methods=["GET","POST"])
@login_required
def new_dish(stall_id):
stall = Stall.query.filter_by(id=int(stall_id)).first()
if stall is None or stall.owner != current_user:
return redirect(url_for('stalls'))
form = DishRegisterForm()
if request.method == 'POST':
if form.validate():
filename = secure_filename(form.image.data.filename)
dish = Dish(form.name.data, form.description.data, form.price.data, stall, filename)
form.image.data.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
db.session.add(dish)
db.session.commit()
return redirect(url_for('stall', stall_id=stall.id))
else:
flash_form_errors(form)
return redirect(url_for('new_dish', stall_id=stall.id))
else:
return render_template('newdish.html', form=form, stall=stall)
@app.route('/stalls/<int:stall_id>/dish/<int:dish_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_dish(stall_id, dish_id):
stall = Stall.query.filter_by(id=int(stall_id)).first()
if stall is None or stall.owner != current_user:
return redirect(url_for('stalls'))
dish = Dish.query.filter_by(id=dish_id).first()
if dish is None or dish.stall != stall:
return redirect(url_for('stall', stall_id=stall_id))
form = DishRegisterForm()
if request.method == 'POST':
if form.validate(editing=True):
dish.name = form.name.data
dish.description = form.description.data
dish.price = form.price.data
dish.image_path = secure_filename(form.image.data.filename)
form.image.data.save(os.path.join(app.config['UPLOAD_FOLDER'], dish.image_path))
db.session.commit()
return redirect(url_for('stall', stall_id=stall_id))
else:
flash_form_errors(form)
return redirect(url_for('edit_dish', stall_id=stall_id, dish_id=dish_id))
else:
form.name.data = dish.name
form.description.data = dish.description
form.price.data = dish.price
return render_template('editdish.html', form=form, stall=stall, dish=dish)
@app.route('/stalls/<int:stall_id>/dish/<int:dish_id>/delete', methods=['POST'])
@login_required
def delete_dish(stall_id, dish_id):
stall = Stall.query.filter_by(id=int(stall_id)).first()
if stall is None or stall.owner != current_user:
return redirect(url_for('stalls'))
dish = Dish.query.filter_by(id=dish_id).first()
if dish is None or dish.stall != stall:
return redirect(url_for('stall', stall_id=stall_id))
db.session.delete(dish)
db.session.commit()
return redirect(url_for('stall', stall_id=stall_id))
@login_manager.user_loader
def load_user(owner_id):
return Owner.query.filter_by(id=int(owner_id)).first()
@login_manager.unauthorized_handler
def unauthorized():
flash('You have to be logged in to view this page!')
return redirect(url_for('login'))
@app.route('/api/all')
def all():
data = {'update_time': str(datetime.now()), 'dishes': [], 'stalls': [], 'locations': []}
for dish in Dish.query.all():
data['dishes'].append({
'id': dish.id,
'name': dish.name,
'price': dish.price,
'stall_id': dish.stall_id,
'image_path': dish.image_path
})
for stall in Stall.query.all():
data['stalls'].append({
'id': stall.id,
'name': stall.name
})
for loc in Location.query.all():
data['locations'].append({
'id': loc.id,
'name': loc.name
})
return jsonify(**data)
@app.route('/api/search/dish')
def search():
query = request.args.get('name')
result = Dish.query.filter(Dish.name.like("%" + query + "%")).all()
data = []
for dish in result:
data.append({
'id': dish.id,
'name': dish.name,
'price': dish.price,
'stall_name': dish.stall.name,
'image_path': dish.image_path
})
return jsonify(data)
def flash_form_errors(form):
for field, errors in form.errors.items():
for error in errors:
flash(u"Error in the %s field - %s" % (getattr(form, field).label.text, error))
\ No newline at end of file
from flask import flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextField, TextAreaField, SelectField, DecimalField, FileField
from wtforms.validators import DataRequired, EqualTo, Email
from models import Owner, Stall, Dish
from werkzeug.security import generate_password_hash, check_password_hash
class OwnerLoginForm(FlaskForm):
username = StringField('Username', [DataRequired()])
password = PasswordField('Password', [DataRequired()])
def validate(self):
if not FlaskForm.validate(self):
return False, None
owner = Owner.query.filter_by(username=self.username.data).first()
if owner is None:
flash('User does not exist!')
return False, None
if not owner.check_password(self.password.data):
flash('Wrong password!')
return False, None
return True, owner
class OwnerRegisterForm(FlaskForm):
email = StringField('Email', [DataRequired(), Email()])
name = StringField('Name', [DataRequired()])
username = StringField('Username', [DataRequired()])
password = PasswordField('Password', [DataRequired()])
confirm_password = PasswordField('Confirm Password', [EqualTo('password')])
def validate(self):
if not FlaskForm.validate(self):
return False
owner = Owner.query.filter_by(email=self.email.data).first()
if owner is not None:
flash('Email is taken!')
return False
owner = Owner.query.filter_by(username=self.username.data).first()
if owner is not None:
flash('Username is taken!')
return False
return True
class StallRegisterForm(FlaskForm):
name = StringField('Name', [DataRequired()])
description = TextAreaField('Description', [DataRequired()])
image = FileField('Image')
location = SelectField(coerce=int)
def validate(self, editing=False):
if not FlaskForm.validate(self):
return False
stall = Stall.query.filter_by(name=self.name.data).first()
if not editing:
if stall is not None:
flash('Stall name is taken!')
return False
return True
class DishRegisterForm(FlaskForm):
name = StringField('Name', [DataRequired()])
description = TextAreaField('Description', [DataRequired()])
image = FileField('Image')
price = DecimalField('Price', [DataRequired()])
def validate(self, editing=False):
if not FlaskForm.validate(self):
return False
dish = Dish.query.filter_by(name=self.name.data).first()
if not editing:
if dish is not None:
flash('Dish name is taken!')
return False
return True
\ No newline at end of file
from canteeneo import db
from werkzeug.security import generate_password_hash, check_password_hash
import hashlib
class Stall(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
owner_id = db.Column(db.Integer, db.ForeignKey('owner.id'))
location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
dishes = db.relationship('Dish', backref='stall', cascade='all,delete', lazy='dynamic')
def __init__(self, name, description, owner_id, location_id):
self.name = name
self.description = description
self.owner_id = owner_id
self.location_id = location_id
class Dish(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
price = db.Column(db.Float)
stall_id = db.Column(db.Integer, db.ForeignKey('stall.id'))
image_path = db.Column(db.String(160), unique=True)
def __init__(self, name, description, price, stall, image_path):
self.name = name
self.description = description
self.price = price
self.stall_id = stall.id
self.image_path = image_path
class Owner(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
username = db.Column(db.String(80))
email = db.Column(db.String(80))
password = db.Column(db.String(160))
stalls = db.relationship('Stall', backref='owner', lazy='dynamic')
def __init__(self, name, username, email, password):
self.name = name
self.username = username
self.email = email
self.set_password(password)
def __repr__(self):
return self.username
def set_password(self, password):
self.password = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password, password)
def is_active(self):
return True
def get_id(self):
return str(self.id)
def is_authenticated(self):
return True
def is_anonymous(self):
return False
class Location(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
def __init__(self, name):
self.name = name
\ No newline at end of file
This diff is collapsed.
@font-face {
font-family: 'Raleway';
src: url('Raleway-Regular.ttf');
}
body {
margin: 0;
overflow-x: hidden;
overflow-y: auto;
background-color: #FFFFFF;
}
* {
font-family: 'Raleway';
font-size: 18pt;
}
button {
background-color: #3333FF;
border: 2px solid #3333DD;
color: #FFFFFF;
outline: 0;
font-size: 32pt;
min-width: 8em;
margin: .25em;
}
button:hover {
background-color: #3333DD;
}
button:active {
background-color: #3333DD;
}
.button-delete {
background-color: #FF3333;
border: 2px solid #DD3333;
}
.button-delete:hover {
background-color: #DD3333;
}
.button-delete:active {
background-color: #DD3333;
}
.field-label {
font-size: 32pt;
text-align: center;
}
.input-row {
display: flex;
flex-direction: column;
align-items: center;
margin-top: .5em;
margin-bottom: .5em;
}
.text-field {
font-size: 32px;
background-color: #FFFFFF;
border: 2px solid #DDDDDD;
outline: 0;
width: 15em;
}
.text-area {
font-size: 32pt;
background-color: #FFFFFF;
border: 2px solid #DDDDDD;
outline: 0;
resize: none;
}
.flash-container {
position: fixed;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flex-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.dashboard-container {
width: 60vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.dashboard-row {
display: flex;
}
#login-form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
min-width: 750px;
}
#register-form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
width: 750px;
}
#new-stall-form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
width: 750px;
}
.form-input {
display: flex;
flex-direction: column;
align-self: stretch;
}
.list {
display: flex;
flex-wrap: wrap;
width: 60vw;
min-width: 750px;
justify-content: space-between;
}
.list-item {
display: flex;
width: 40%;
margin: 10px;
background-color: white;
}
.list-image {
flex-grow: 0;
}
.list-info {
display: flex;
flex-direction: column;
justify-content: space-around;
flex-grow: 1;
margin-left: 1.5em;
}
.list-name {
font-size: 28pt;
}
.list-button {
font-size: 18pt;
}
#image-upload {
align-self: center;
margin-left: 1em;
}
\ No newline at end of file
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> {% block head %}
<title>Canteeneo - {% block title %}{% endblock%}</title>
{% endblock %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="flex-container">
{% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %}
<span>{{ message }}</span><br> {% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %}
</div>
</body>
<script src="{{ url_for('static', filename='jquery-ajax.js') }}"></script>
</html>
\ No newline at end of file
{% extends "base.html" %} {% block title %}Edit Dish Info{% endblock %} {% block content %}
<h1>Dish Info</h1>
<div id="new-stall-form">
<form action="{{ url_for('edit_dish', stall_id=stall.id, dish_id=dish.id) }}" enctype="multipart/form-data" method="POST">
{{ form.csrf_token }}
<div class="form-input">
<div class="input-row">
{{ form.name.label(class_="field-label") }} {{ form.name(class_="text-field") }}
</div>
<div class="input-row">
<span style="display: flex;">
<img id="image-preview" src="{{url_for('static', filename='uploads/'+dish.image_path)}}" width="160px" height="160px">
{{ form.image(id="image-upload") }}
</span>
</div>
<div class="input-row">
{{ form.description.label(class_="field-label") }} {{ form.description(class_="text-area", rows=6, cols=32) }}
</div>
<div class="input-row">
{{ form.price.label(class_="field-label") }} {{ form.price(class_="text-field", type="number", style="width: 5em;") }}
</div>
</div>
<div class="dashboard-row">
<button type="submit">Apply</button>
<a href="{{ url_for('stall', stall_id=stall.id) }}"><button type="button" class="link-button">Back</button></a>
</div>
</form>
<form action="{{ url_for('delete_dish', stall_id=stall.id, dish_id=dish.id) }}" method="POST">
<button type="submit" class="button-delete">Delete</button></a>
</form>
</div>
<script>
document.getElementById("image-upload").onchange = function() {
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById("image-preview").src = e.target.result;
};
reader.readAsDataURL(this.files[0])
}
</script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Edit Stall Info{% endblock %}
{% block content %}
<h1>Stall Info</h1>
<form action="{{ url_for('edit_stall', stall_id=stall.id) }}" method="POST">
{{ form.csrf_token }}
<div id="new-stall-form">
<div class="form-input">
<div class="input-row">
{{ form.name.label(class_="field-label") }} {{ form.name(class_="text-field") }}
</div>
<div class="input-row">
{{ form.description.label(class_="field-label") }} {{ form.description(class_="text-area", rows=6, cols=32) }}
</div>
<div class="input-row">
{{ form.location.label(class_="field-label") }} {{ form.location(class_="text-field") }}
</div>
</div>
<div class="dashboard-row">
<button type="submit">Submit</button>
<a href="{{ url_for('stall', stall_id=stall.id) }}"><button type="button">Back</button></a>
</div>
</div>
</form>
<form action="{{ url_for('delete_stall', stall_id=stall.id) }}" method="POST">
<button type="submit" class="button-delete">Delete</button>
</form>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Landing Page{% endblock %}
{% block content %}
<a href="/login"><button class="link-button">Login</button></a>
<a href="/register"><button class="link-button">Register</button></a>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<form method="POST" action="/login">
{{ form.csrf_token }}
<div id="login-form">
<div class="form-input">
<div class="input-row">
{{ form.username.label(class_="field-label") }}
{{ form.username(class_="text-field") }}
</div>
<div class="input-row">
{{ form.password.label(class_="field-label") }}
{{ form.password(class_="text-field") }}
</div>
</div>
<button type="submit">Login</button>
</div>
</form>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %} {% block title %}Add Food Item{% endblock %} {% block content %}
<h1>Dish Info</h1>
<form action="{{ url_for('new_dish', stall_id=stall.id) }}" enctype="multipart/form-data" method="POST">
{{ form.csrf_token }}
<div id="new-stall-form">
<div class="form-input">
<div class="input-row">
{{ form.name.label(class_="field-label") }} {{ form.name(class_="text-field") }}
</div>
<div class="input-row">
<span style="display: flex;">
<img id="image-preview" width="160px" height="160px">
{{ form.image(id="image-upload") }}
</span>
</div>
<div class="input-row">
{{ form.description.label(class_="field-label") }} {{ form.description(class_="text-area", rows=6, cols=32) }}
</div>
<div class="input-row">
{{ form.price.label(class_="field-label") }} {{ form.price(class_="text-field", type="number", style="width: 5em;") }}
</div>
</div>
<div class="dashboard-row">
<button type="submit">Add</button>
<a href="{{ url_for('stalls', stall_id=stall.id) }}"><button type="button" class="link-button">Back</button></a>
</div>
</div>
</form>
<script>
document.getElementById("image-upload").onchange = function() {
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById("image-preview").src = e.target.result;
};
reader.readAsDataURL(this.files[0])
}
</script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Register your Stall{% endblock %}
{% block content %}
<h1>Stall Info</h1>
<form action="{{url_for('new_stall')}}" method="POST">
{{ form.csrf_token }}
<div id="new-stall-form">
<div class="form-input">
<div class="input-row">
{{ form.name.label(class_="field-label") }} {{ form.name(class_="text-field") }}
</div>
<div class="input-row">
{{ form.description.label(class_="field-label") }} {{ form.description(class_="text-area", rows=6, cols=32) }}
</div>
<div class="input-row">
{{ form.location.label(class_="field-label") }} {{ form.location(class_="text-field") }}
</div>
</div>
<div class="dashboard-row">
<button type="submit">Add</button>
<a href="{{url_for('stalls')}}"><button type="button" class="link-button">Back</button></a>
</div>
</div>
</form>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block content %}
<form method="POST" action="/register">
{{ form.csrf_token }}
<div id="register-form">
<div class="form-input">
<div class="input-row">
{{ form.email.label(class_="field-label")}}
{{ form.email(class_="text-field") }}
</div>
<div class="input-row">
{{ form.name.label(class_="field-label")}}
{{ form.name(class_="text-field") }}
</div>
<div class="input-row">
{{ form.username.label(class_="field-label")}}
{{ form.username(class_="text-field") }}
</div>
<div class="input-row">
{{ form.password.label(class_="field-label")}}
{{ form.password(class_="text-field") }}
</div>
<div class="input-row">
{{ form.confirm_password.label(class_="field-label")}}
{{ form.confirm_password(class_="text-field") }}
</div>
</div>
<button type="submit">Register</button>
</div>
</form>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %} {% block title %} {{stall.name}} {% endblock %} {% block content %}
<div class="dashboard-container">
<div class="dashboard-row">
<span style="font-size: 32pt; flex-grow: 2;">
{{stall.name}}
</span>
<a href="{{ url_for('new_dish', stall_id=stall.id) }}"><button class="link-button" style="flex-grow: 1;">Add Food Item</button></a>
<a href="{{ url_for('edit_stall', stall_id=stall.id) }}"><button class="link-button" style="flex-grow: 1;">Edit Info</button></a>
</div>
<div class="dashboard-row">
<div class="list">
{% if dishes|length > 0 %} {% for dish in dishes %}
<div class="list-item">
<div class "list-image">
<img width="200px" height="200px" src="{{url_for('static', filename='uploads/'+dish.image_path)}}" alt="">
</div>
<div class="list-info">
<span class="list-name">{{dish.name}}</span>
<span class="list-price">Php{{dish.price}}</span>
<a href="{{ url_for('edit_dish', stall_id=stall.id, dish_id=dish.id) }}"><button class="list-button">Edit Dish</button></a>
</div>
</div>
{% endfor %} {% endif %}
</div>
</div>
<div class="dashboard-row">
<a href="{{ url_for('stalls') }}"><button class="link-button">Dashboard</button></a>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="dashboard-container">
<div class="dashboard-row" style="justify-content: space-around; align-items: baseline;">
<span style="font-size: 32pt; flex-grow: 2;">Hello, {{ owner.name }}</span>
<a href="{{url_for('new_stall')}}"><button class="link-button" style="flex-grow: 1;">Register a Stall</button></a>
<a href="{{url_for('logout')}}"><button class="link-button" style="flex-grow: 1;">Logout</button></a>
</div>
<div class="dashboard-row">
<div class="list">
{% if stalls|length > 0 %}
{% for stall in stalls %}
<div class="list-item" style="width: 100%;">
<div class="list-info">
<span class="list-name">{{stall.name}}</span>
</div>
<div class="list-infor">
<a href="{{ url_for('stall', stall_id=stall.id) }}"><button class="list-button">Manage Stall</button></a>
</div>
</div>
{% endfor %}
{% else %}
<p>You have no stalls registered!</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
from canteeneo import app
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment