A lot works!
This commit is contained in:
3
.idea/mvl.iml
generated
3
.idea/mvl.iml
generated
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="Flask">
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</component>
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="jdk" jdkName="Pipenv (mvl)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Pipenv (mvl)" jdkType="Python SDK" />
|
||||||
|
|||||||
69
app.py
69
app.py
@@ -1,22 +1,28 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import Flask, render_template, jsonify, abort
|
from flask import Flask, render_template, jsonify, abort, send_file, request, redirect
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
from base64 import standard_b64decode, standard_b64encode
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from model import Post, Category, db
|
from model import db, Post, Category, ImageBase64
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Check for environment variable
|
# Check for environment variable
|
||||||
if not os.getenv("DATABASE_URL"):
|
env_vars = ["DATABASE_URL", "PASSWORD"]
|
||||||
raise RuntimeError("DATABASE_URL is not set")
|
for env_var in env_vars:
|
||||||
|
if not os.getenv(env_var):
|
||||||
|
raise RuntimeError(f"{env_var} is not set")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL")
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL")
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['JSON_SORT_KEYS'] = False
|
app.config['JSON_SORT_KEYS'] = False
|
||||||
|
|
||||||
|
# Bind db to application
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
posts = Post.get_posts()
|
posts = Post.get_posts()
|
||||||
@@ -25,7 +31,6 @@ def index():
|
|||||||
|
|
||||||
@app.route('/<string:category_name>')
|
@app.route('/<string:category_name>')
|
||||||
def category(category_name):
|
def category(category_name):
|
||||||
|
|
||||||
category_item = Category.query.filter(func.lower(Category.name) == category_name.replace('_', ' ')).first()
|
category_item = Category.query.filter(func.lower(Category.name) == category_name.replace('_', ' ')).first()
|
||||||
if not category_item:
|
if not category_item:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
@@ -35,6 +40,11 @@ def category(category_name):
|
|||||||
return render_template("main/index.html", posts=posts)
|
return render_template("main/index.html", posts=posts)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/contact')
|
||||||
|
def contact():
|
||||||
|
return render_template("main/contact.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/post/<int:post_id>", methods=["POST"])
|
@app.route("/api/post/<int:post_id>", methods=["POST"])
|
||||||
def get_post(post_id):
|
def get_post(post_id):
|
||||||
post = Post.query.get(post_id)
|
post = Post.query.get(post_id)
|
||||||
@@ -44,8 +54,47 @@ def get_post(post_id):
|
|||||||
return jsonify({"success": False})
|
return jsonify({"success": False})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login", methods=["GET"])
|
@app.route('/images/<string:filename>')
|
||||||
def login():
|
def get_image(filename):
|
||||||
return render_template("adm/login.html")
|
image_b64 = ImageBase64.query.filter(ImageBase64.filename == filename).first()
|
||||||
|
if not image_b64:
|
||||||
|
return abort(404)
|
||||||
|
image = standard_b64decode(image_b64.data)
|
||||||
|
return send_file(BytesIO(image), mimetype=image_b64.mimetype, attachment_filename=filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/adm/uploadfile', methods=['POST', 'GET'])
|
||||||
|
def file_uploaded():
|
||||||
|
if request.method == 'POST':
|
||||||
|
if not request.form.get('password') == os.getenv('PASSWORD'):
|
||||||
|
abort(401)
|
||||||
|
# check if the post request has the file part
|
||||||
|
if 'file' not in request.files:
|
||||||
|
# flash('No file part')
|
||||||
|
return abort(400)
|
||||||
|
# return redirect(request.url)
|
||||||
|
file = request.files['file']
|
||||||
|
# if user does not select file, browser also
|
||||||
|
# submit an empty part without filename
|
||||||
|
if file.filename == '':
|
||||||
|
# flash('No selected file')
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
if file:
|
||||||
|
data = standard_b64encode(file.read()).decode()
|
||||||
|
print(file.filename, file.mimetype)
|
||||||
|
print(data)
|
||||||
|
database_object = ImageBase64(filename=file.filename, mimetype=file.mimetype, data=data)
|
||||||
|
db.session.add(database_object)
|
||||||
|
db.session.commit()
|
||||||
|
# if file and allowed_file(file.filename):
|
||||||
|
# filename = secure_filename(file.filename)
|
||||||
|
# file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
|
# return redirect(url_for('uploaded_file',
|
||||||
|
# filename=filename))
|
||||||
|
return render_template("adm/uploadfile.html")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.app_context().push()
|
||||||
|
db.create_all()
|
||||||
|
|||||||
10
model.py
10
model.py
@@ -26,7 +26,7 @@ class Post(db.Model):
|
|||||||
"title": self.title,
|
"title": self.title,
|
||||||
"intro": self.intro,
|
"intro": self.intro,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"images": self.images}
|
"images": [image.uri for image in self.images]}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_posts():
|
def get_posts():
|
||||||
@@ -44,6 +44,14 @@ class Image(db.Model):
|
|||||||
post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False)
|
post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageBase64(db.Model):
|
||||||
|
__tablename__ = "image_store"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
filename = db.Column(db.String(30), unique=True, nullable=False)
|
||||||
|
mimetype = db.Column(db.String(127), nullable=False)
|
||||||
|
data = db.Column(db.Text, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class Category(db.Model):
|
class Category(db.Model):
|
||||||
__tablename__ = "category"
|
__tablename__ = "category"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Start of Custom CSS
|
* Start of Custom CSS
|
||||||
*/
|
*/
|
||||||
|
.nav-link, .navbar-brand {
|
||||||
|
font-family: 'Amatic SC', cursive;
|
||||||
|
font-size: 40px; }
|
||||||
|
|
||||||
.card-img-overlay {
|
.card-img-overlay {
|
||||||
opacity: 0; }
|
opacity: 0; }
|
||||||
|
|
||||||
@@ -17,8 +21,11 @@
|
|||||||
.card .card-img-overlay .card-title-bg .card-text {
|
.card .card-img-overlay .card-title-bg .card-text {
|
||||||
opacity: 1; }
|
opacity: 1; }
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
margin: 10px; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding-top: 4.5rem; }
|
padding-top: 6.5rem; }
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
$margin: 20px;
|
$margin: 20px;
|
||||||
$half-margin: $margin / 2;
|
$half-margin: $margin / 2;
|
||||||
|
|
||||||
|
// Nav
|
||||||
|
.nav-link, .navbar-brand {
|
||||||
|
font-family: 'Amatic SC', cursive;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Overlay is hidden
|
// Overlay is hidden
|
||||||
.card-img-overlay {
|
.card-img-overlay {
|
||||||
@@ -36,8 +45,13 @@ $half-margin: $margin / 2;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contact page
|
||||||
|
.social-icon {
|
||||||
|
margin: $half-margin;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding-top: 4.5rem;
|
padding-top: 6.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
21
templates/adm/uploadfile.html
Normal file
21
templates/adm/uploadfile.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'adm/layout.html' %}
|
||||||
|
{% block main %}
|
||||||
|
<main class="container">
|
||||||
|
<h1>Upload new Photo</h1>
|
||||||
|
<form class="" method=post enctype=multipart/form-data>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for=password>Password</label>
|
||||||
|
<input class="form-control" id=password type=password name=password>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input" id="file" name="file">
|
||||||
|
<label class="custom-file-label" for="file">Choose file</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="btn btn-primary form-control" type=submit value=Upload>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,8 +5,13 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Amatic+SC" rel="stylesheet">
|
||||||
<!-- Bootstrap first, then custom style -->
|
<!-- Bootstrap first, then custom style -->
|
||||||
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css"
|
||||||
|
integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr"
|
||||||
|
crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
@@ -22,6 +27,7 @@
|
|||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
|
||||||
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
|
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block nav %}{% endblock %}
|
{% block nav %}{% endblock %}
|
||||||
|
|||||||
22
templates/main/contact.html
Normal file
22
templates/main/contact.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "main/layout.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<main class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-md-6 align-content-center">
|
||||||
|
<h1 class="text-center">Hoi!</h1>
|
||||||
|
<p>Na het behalen van mijn diploma 'Product Design' van de Hogeschool van Amsterdam in februari 2018 ben
|
||||||
|
ik gaan werken bij een grafisch ontwerpbureau in Haarlem.
|
||||||
|
Daarnaast ben ik steeds meer bezig met eigen opdrachten, zoals te zien is in dit portfolio.</p>
|
||||||
|
<div class="text-center ">
|
||||||
|
<a href="https://www.linkedin.com/in/marlousvanleeuwen/"><i
|
||||||
|
class="social-icon fab fa-linkedin-in fa-3x fa-fw text-dark"></i></a>
|
||||||
|
<a href="mailto:marlousvleeuwen@live.nl"><i
|
||||||
|
class="social-icon far fa-envelope fa-3x fa-fw text-dark"></i></a>
|
||||||
|
<a href="https://www.instagram.com/marlousvleeuwen/"><i
|
||||||
|
class="social-icon fab fa-instagram fa-3x fa-fw text-dark"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
data-post_id="{{ post.id }}"
|
data-post_id="{{ post.id }}"
|
||||||
>
|
>
|
||||||
<div class="card border-light">
|
<div class="card border-light">
|
||||||
<img class="card-img" src="https://placeimg.com/400/400/any" alt="random image">
|
<img class="card-img" src="{{ post.images[0].uri }}" alt="random image">
|
||||||
|
|
||||||
<div class="card-img-overlay d-flex align-items-center justify-content-center">
|
<div class="card-img-overlay d-flex align-items-center justify-content-center">
|
||||||
<div class="card-title-bg bg-dark w-100 d-flex align-items-center justify-content-center">
|
<div class="card-title-bg bg-dark w-100 d-flex align-items-center justify-content-center">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="modal fade" id="detailModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
<div class="modal fade" id="detailModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title text-center" id="details-title">Modal title</h5>
|
<h5 class="modal-title text-center" id="details-title">Modal title</h5>
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="details-intro"></div>
|
<div id="details-intro" class="font-weight-bold"></div>
|
||||||
|
<div id="details-images"></div>
|
||||||
<div id="details-description"></div>
|
<div id="details-description"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,20 +20,25 @@
|
|||||||
<script>
|
<script>
|
||||||
$('#detailModal').on('show.bs.modal', function (event) {
|
$('#detailModal').on('show.bs.modal', function (event) {
|
||||||
let card = $(event.relatedTarget); // Button that triggered the modal
|
let card = $(event.relatedTarget); // Button that triggered the modal
|
||||||
{#let name = card.data('name'); // Extract info from data-* attributes#}
|
|
||||||
let post_id = card.data('post_id');
|
let post_id = card.data('post_id');
|
||||||
|
|
||||||
$.post(`/api/post/${post_id}`, data => {
|
$.post(`/api/post/${post_id}`, data => {
|
||||||
if (!data.success) {
|
|
||||||
console.error("No data for post");
|
let post = data.post;
|
||||||
return;
|
console.log(post);
|
||||||
}
|
|
||||||
let modal = $(this);
|
$('#details-title').text(post.title);
|
||||||
console.log(data);
|
$('#details-intro').text(post.intro);
|
||||||
modal.find('#details-title').text(data.post.title);
|
$('#details-description').text(post.description);
|
||||||
modal.find('#details-intro').text(data.post.intro);
|
|
||||||
modal.find('#details-description').text(data.post.description);
|
$('#details-images').text('');
|
||||||
|
post.images.forEach(image => {
|
||||||
|
$('#details-images').append(`<img src=${image}>`)
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.on('shown.bs.modal', function () {
|
||||||
|
$('#detailModal').scrollTop(0);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-top">
|
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-top">
|
||||||
<a class="navbar-brand" href="{{ url_for("index") }}">Marlous</a>
|
<a class="navbar-brand font-weight-bold" href="{{ url_for("index") }}">Marlous</a>
|
||||||
|
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
||||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
@@ -8,8 +8,19 @@
|
|||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item active">
|
{% set items = [("Product Design", 'product_design'), ("Grafisch", "grafisch"), ("Fotografie", "fotografie")] %}
|
||||||
<a class="nav-link" href="#">Home<span class="sr-only">(current)</span></a>
|
{% for item in items %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if item[1] == request.path[1:] %}active{% endif %}"
|
||||||
|
href={{ url_for("category", category_name=item[1]) }}>
|
||||||
|
{{ item[0] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
{% set name = "contact" %}
|
||||||
|
<a class="nav-link {% if name == request.path[1:] %}active{% endif %}" href="{{ url_for("contact") }}">Contact</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user