diff options
author | 2022-05-12 15:08:19 +0100 | |
---|---|---|
committer | 2022-05-12 15:08:19 +0100 | |
commit | 8e7527ae1045f49cd326a394e0f07d95e64a5b6f (patch) | |
tree | d12c8d9edae8916a49da554f488a2a6ee6a75294 | |
parent | d473ffe12f72109bc304ca341571a10cc4cf8f38 (diff) | |
download | victoria-flask.tar.gz victoria-flask.tar.bz2 victoria-flask.zip |
added routes to admin/routes.py and begun on admin formsflask
18 files changed, 445 insertions, 34 deletions
diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/LICENSE.md diff --git a/routes.txt b/docs/routes.txt index c95a2c4..c95a2c4 100644 --- a/routes.txt +++ b/docs/routes.txt diff --git a/victoria/__pycache__/__init__.cpython-310.pyc b/victoria/__pycache__/__init__.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..a2c4fba --- /dev/null +++ b/victoria/__pycache__/__init__.cpython-310.pyc diff --git a/victoria/__pycache__/config.cpython-310.pyc b/victoria/__pycache__/config.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..f43dea0 --- /dev/null +++ b/victoria/__pycache__/config.cpython-310.pyc diff --git a/victoria/__pycache__/models.cpython-310.pyc b/victoria/__pycache__/models.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..64004a6 --- /dev/null +++ b/victoria/__pycache__/models.cpython-310.pyc diff --git a/victoria/admin/__pycache__/__init__.cpython-310.pyc b/victoria/admin/__pycache__/__init__.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..b58c293 --- /dev/null +++ b/victoria/admin/__pycache__/__init__.cpython-310.pyc diff --git a/victoria/admin/__pycache__/routes.cpython-310.pyc b/victoria/admin/__pycache__/routes.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..d87b7c0 --- /dev/null +++ b/victoria/admin/__pycache__/routes.cpython-310.pyc diff --git a/victoria/admin/forms.py b/victoria/admin/forms.py index 5faad3b..f5c1648 100644 --- a/victoria/admin/forms.py +++ b/victoria/admin/forms.py @@ -1,8 +1,47 @@ from flask_wtf import FlaskForm from flask_wtf.file import FileAllowed, FileField -from wtforms import StringField, SubmitField, BooleanField +from wtforms import DateField, TextAreaField, StringField, SubmitField, BooleanField, PasswordField from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError from flask_login import current_user from victoria.models import Artist, Series, Work, Tag, License +class LoginForm(FlaskForm): + email = StringField('Email', + validators=[DataRequired(), Email()]) + submit = SubmitField('Send Login Code') + +class OTPForm(FlaskForm): + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') + +class UpdateArtistForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)]) + email = StringField('Email', validators=[DataRequired(), Email()]) + name = StringField('Name', validators=[DataRequired(), Length(min=2, max=100)]) + picture = FileField('Update Picture', validators=[FileAllowed(['jpg', 'png', 'jpeg', 'gif'])]) + bio = TextAreaField('Bio') + submit = SubmitField('Update') + + def validate_username(self, username): + if username.data != current_user.username: + user = Artist.query.filter_by(username=username.data).first() + if user: + raise ValidationError('Username is taken, please choose a different one') + def validate_email(self, email): + if email.data != current_user.email: + user = Artist.query.filter_by(email=email.data).first() + if user: + raise ValidationError('Email is taken, please choose a different one') + +class WorkForm(FlaskForm): + title = StringField('Title', validators=[Length(min=2, max=120)]) + date = DateField('Date') + description = TextAreaField('Bio') + tags = StringField('Title', validators=[Length(min=2, max=120)]) + license = StringField('Title', validators=[Length(min=2, max=20)]) + +class MediaForm(FlaskForm): + media = FileField('Update Picture', validators=[FileAllowed(['jpg', 'png', 'jpeg', 'gif'])]) + alt_text = TextAreaField('Alt Text') + diff --git a/victoria/admin/routes.py b/victoria/admin/routes.py index e721075..55dbe1b 100644 --- a/victoria/admin/routes.py +++ b/victoria/admin/routes.py @@ -2,29 +2,104 @@ from flask import render_template, url_for, redirect, request, Blueprint from flask_login import login_user, current_user, logout_user, login_required from victoria import db from victoria.models import Artist, Work, Series, Tag, License -from victoria.admin.forms import -from victoria.admin.utils import send_otp_email +from victoria.admin.forms import LoginForm +#from victoria.admin.utils import send_otp_email admin = Blueprint('admin', __name__) @admin.route("/admin/login", methods=['GET', 'POST']) -@login_required -def admin_dashboard(): +def login(): + form = LoginForm() return render_template('admin.html', title='Admin Dashboard') -@admin.route("/admin", methods=['GET', 'POST']) -@admin.route("/admin/dashboard", methods=['GET', 'POST']) +@admin.route("/admin/login/<string:otp>") +def otp_login(otp): + return redirect(url_for('admin.dashboard')) + +@admin.route("/admin") +@admin.route("/admin/dashboard") @login_required -def admin_dashboard(): +def dashboard(): return render_template('admin.html', title='Admin Dashboard') - -@admin.route("/admin", methods=['GET', 'POST']) + +@admin.route("/admin/works") @login_required -def admin_dashboard(): - return render_template('admin.html', title='Admin Dashboard') - -@admin.route("/admin", methods=['GET', 'POST']) +def works(): + return render_template('admin/works.html', title='Works') + +@admin.route("/admin/work/new") @login_required -def admin_dashboard(): - return render_template('admin.html', title='Admin Dashboard') - +def new_work(): + return render_template('admin/new_work.html', title='New work') + +@admin.route("/admin/work/<int:work_id>") +@login_required +def edit_work(work_id): + return render_template('admin/work.html', title=work_title) + +@admin.route("/admin/work/<int:work_id>/delete", methods=['POST']) +@login_required +def delete_work(work_id): + flash('The work has been deleted.', 'success') + return redirect(url_for('admin.works')) + +@admin.route("/admin/series") +@login_required +def series(): + return render_template('admin/series.html', title='Series') + +@admin.route("/admin/series/new") +@login_required +def new_series(): + return render_template('admin/new_series.html', title='New Series') + +@admin.route("/admin/series/<int:series_id>") +@login_required +def edit_series(series_id): + return render_template('admin/edit_series.html', title=series_title) + +@admin.route("/admin/series/<int:series_id>/delete", methods=['POST']) +@login_required +def delete_series(series_id): + flash('The series has been deleted.', 'success') + return redirect(url_for('admin.series')) + +@admin.route("/admin/artist") +@login_required +def artist(): + return render_template('admin/edit_artist.html', title='Edit Artist Profile') + +@admin.route("/admin/tags") +@login_required +def tags(): + #list tags + return render_template('admin/list_tags.html', title='Tags') + +@admin.route("/admin/licenses") +@login_required +def licenses(): + return render_template('admin/licenses.html', title='Licences') + +@admin.route("/admin/license/new") +@login_required +def new_license(): + return render_template('admin/new_license.html', title='New License') + +@admin.route("/admin/license/<int:license_id>") +@login_required +def edit_license(license_id): + return render_template('admin/edit_license.html', title=license_title) + +@admin.route("/admin/license/<int:license_id>/delete", methods=['POST']) +@login_required +def delete_license(license_id): + flash('The license has been deleted.', 'success') + return redirect(url_for('admin.licenses')) + +@admin.route("/admin/settings") +@login_required +def settings(): + return render_template('admin/settings.html', title='Settings') + + + diff --git a/victoria/errors/__pycache__/__init__.cpython-310.pyc b/victoria/errors/__pycache__/__init__.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..c371450 --- /dev/null +++ b/victoria/errors/__pycache__/__init__.cpython-310.pyc diff --git a/victoria/errors/__pycache__/handlers.cpython-310.pyc b/victoria/errors/__pycache__/handlers.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..cb64952 --- /dev/null +++ b/victoria/errors/__pycache__/handlers.cpython-310.pyc diff --git a/victoria/main/__pycache__/__init__.cpython-310.pyc b/victoria/main/__pycache__/__init__.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..1a73e77 --- /dev/null +++ b/victoria/main/__pycache__/__init__.cpython-310.pyc diff --git a/victoria/main/__pycache__/routes.cpython-310.pyc b/victoria/main/__pycache__/routes.cpython-310.pyc Binary files differnew file mode 100644 index 0000000..b6c0329 --- /dev/null +++ b/victoria/main/__pycache__/routes.cpython-310.pyc diff --git a/victoria/main/routes.py b/victoria/main/routes.py index 52eafa8..8272e63 100644 --- a/victoria/main/routes.py +++ b/victoria/main/routes.py @@ -1,14 +1,11 @@ from flask import Blueprint, request, render_template -from flaskblog.models import Post main = Blueprint('main', __name__) @main.route("/") @main.route("/home") def home(): - page = request.args.get('page', 1, type=int) - posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=5) - return render_template('home.html', posts=posts) + return render_template('home.html') @main.route("/about") def about(): diff --git a/victoria/models.py b/victoria/models.py index a37724a..6a8dc89 100644 --- a/victoria/models.py +++ b/victoria/models.py @@ -2,6 +2,68 @@ from victoria import db, login_manager from flask_login import UserMixin from datetime import datetime +# for sqlalchemy_media +import json +import functools +from os import path, getcwd +from sqlalchemy import TypeDecorator, Unicode +from sqlalchemy_media import File, Image, ImageValidator, ImageProcessor, ImageAnalyzer, StoreManager, \ + FileSystemStore +from sqlalchemy_media.constants import MB, KB + +WORKING_DIR = path.abspath(getcwd()) +TEMP_PATH = path.join(WORKING_DIR, 'static', 'media') + +StoreManager.register( + 'fs', + functools.partial(FileSystemStore, TEMP_PATH, 'http://localhost:5000/static/media'), + default=True +) + +# JSON mutable type for SQLite +class Json(TypeDecorator): + impl = Unicode + + def process_bind_param(self, value, engine): + return json.dumps(value) + + def process_result_value(self, value, engine): + if value is None: + return None + return json.loads(value) + +class ProfilePicture(Image): + __pre_processors__ = [ + ImageAnalyzer(), + ImageValidator( + minimum=(10, 10), + maximum=(3840, 3840), + content_types=('image/jpeg', 'image/png', 'image/gif') + ), + ImageProcessor( + fmt='jpeg' + ) + ] + __max_length__ = 10 * MB + __min_length__ = 10 * KB + +class MediaFile(Image): + __pre_processors__ = [ + ImageAnalyzer(), + ImageValidator( + minimum=(10, 10), + maximum=(3840, 3840), + content_types=('image/jpeg', 'image/png', 'image/gif') + ), + ImageProcessor( + fmt='jpeg' + ) + ] + __max_length__ = 20 * MB + __min_length__ = 10 * KB + + +# login_manager @login_manager.user_loader def load_user(artist_id): return Artist.query.get(int(artist_id)) @@ -30,12 +92,12 @@ class Artist(db.Model, UserMixin): username = db.Column(db.String(20), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) name = db.Column(db.String(120)) - profile_picture = db.Column(db.String(20)) + profile_picture = db.Column(ProfilePicture.as_mutable(Json)) bio = db.Column(db.Text) - works = db.relationship('Work', secondary=tags, lazy='subquery', + works = db.relationship('Work', secondary=artist_work, lazy='subquery', backref=db.backref('artist', lazy=True)) - series = db.relationship('Series', secondary=tags, lazy='subquery', + series = db.relationship('Series', secondary=artist_series, lazy='subquery', backref=db.backref('artist', lazy=True)) def __repr__(self): @@ -43,37 +105,45 @@ class Artist(db.Model, UserMixin): class Work(db.Model): id = db.Column(db.Integer, primary_key=True) - image_file = db.Column(db.String(20), unique=True, nullable=False) + + media = db.relationship('Media', backref='work', lazy=True) + title = db.Column(db.String(120), nullable=False, default='Untitled') - date = db.Column(db.DateTime), + date = db.Column(db.DateTime) date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) description = db.Column(db.Text) - alt_text = db.Column(db.Text) # tags tags = db.relationship('Tag', secondary=tags, lazy='subquery', - backref=db.backref('works', lazy=True)) + backref=db.backref('work', lazy=True)) # license - license = db.Column(db.String(20), db.ForeignKey('license.license')) + license_id = db.Column(db.String(20), db.ForeignKey('license.id')) def __repr__(self): return f"Work('{self.title}', '{self.image_file}', '{self.alt_text}')" +# Media table, as each work can have multiple images. +class Media(db.Model): + id = db.Column(db.Integer, primary_key=True) + media = db.Column(MediaFile.as_mutable(Json), nullable=False) + alt_text = db.Column(db.Text) + work_id = db.Column(db.Integer, db.ForeignKey('work.id'), nullable=False) + class Series(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(120), nullable=False, default='Untitled') - date = db.Column(db.DateTime), + date = db.Column(db.DateTime) date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) location = db.Column(db.String(120)) description = db.Column(db.Text) # tree self-relationship - children = db.relationship('Series', backref='parent', lazy=True) - parent_id = db.Column(db.Integer, db.ForeignKey('parent.id'), nullable=False) + children = db.relationship('Series', backref='parent', lazy=True, remote_side=[id]) + parent_id = db.Column(db.Integer, db.ForeignKey('series.id')) # many-to-many relationships # works - works = db.relationship('Work', secondary=tags, lazy='subquery', + works = db.relationship('Work', secondary=series_work, lazy='subquery', backref=db.backref('series', lazy=True)) @@ -81,7 +151,8 @@ class Tag(db.Model): tag = db.Column(db.String(20), primary_key=True) class License(db.Model): - license = db.Column(db.String(20), primary_key=True) + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(20), nullable=False) text = db.Column(db.Text) works = db.relationship('Work', backref='license', lazy=True) diff --git a/victoria/site.db b/victoria/site.db Binary files differnew file mode 100644 index 0000000..2504ed1 --- /dev/null +++ b/victoria/site.db diff --git a/victoria/static/main.css b/victoria/static/main.css new file mode 100644 index 0000000..2790857 --- /dev/null +++ b/victoria/static/main.css @@ -0,0 +1,188 @@ +h1 { + color: #9561bd; +} + +bottom { + float: bottom; +} + +* { + overflow-wrap:break-word; + cursor: var(--default), auto; + +} + +header { + padding: 20px; +} + +p, a { + font-family: sans-serif; +} + +a { + cursor: var(--pointer), auto; + display: inline-block; + color: inherit; + position: relative; + text-decoration: none; + transition: linear 0.2s; + color: var(--purple); +} + +a:before { + background: var(--rainbowright); + content: ''; + height: 2px; + position: absolute; + bottom: -1.5px; + width: 100%; + left: 50%; + transform: translateX(-50%); + transition: width 0.2s ease-in-out; +} + +a:hover{ + color: var(--white); +} + +a:hover:before{ + width: 0; +} + + + +html{ + min-height: 100%; +} + +body { + color: var(--lightblue); + font-family: serif; + font-weight: normal; + + margin: 0; + padding: 0; + + background-color: #ffffff; + + + min-height: 100%; + height: 100%; + + background-size: auto; + background-position: center; +} + +.flexbox{ + padding-top: 1.5%; + padding-bottom: 4%; + display:flex; + justify-content: space-between; +} + +.in-flexbox { + box-sizing: border-box; + height: 100%; + padding: 0; +} + +.container{ + padding: 1% 2%; + width: 74%; + height: 100%; +} + +.sidebar{ + position: sticky; + top: 1.5%; + width: 24%; +} + +.sidebar div{ + margin-bottom: 13px; + padding: 2% 10%; +} + +.nav a, .subpage a{ + color: var(--lightblue); +} + +.nav a:hover, .subpage a:hover{ + color: var(--white); + transition: linear 0.2s; +} + +.nav p, .subpage p{ + line-height: 1em; +} + + +@media (max-width:1024px) +{ + .flexbox{ + flex-direction: column; + justify-content: space-around; + align-items: center; + + } + .sidebar { + display: flex; + position: relative; + flex-direction: row; + width: 74%; + justify-content: space-between; + box-sizing: border-box; + gap: 2%; + } + + .sidebar div{ + margin-bottom: 13px; + padding: 1% 2%; + flex-grow: 1; + } +} + +@media (max-width: 720px) +{ + .container{ + width: 90%; + } + .sidebar{ + width: 90%; + } + + .nav h2{ + font-size: 1.4em; + } + .nav p{ + font-size: 11px; + } + + a:before { + height: 2px; + bottom: -2px; + } + +} + +.body { + width: 60%; + height: 95%; + + margin: auto; + margin-top: 2.5%; + + padding: 0; + +} + +@media (max-width:1024px) +{ + .body { + width: 100%; + padding: 0; + margin: auto; + } +} + diff --git a/victoria/templates/base.html b/victoria/templates/base.html new file mode 100644 index 0000000..9b8186c --- /dev/null +++ b/victoria/templates/base.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <!-- Bootstrap CSS --> + <link rel="stylesheet" type="text/css" href="static/main.css"> + + <title>VICTORIA KENNAUGH</title> +</head> +<body> + <div class="body"> + <header class="header top"> + <h1>VICTORIA KENNAUGH</h1> + </header> + <div class="flexbox"> + <nav class="sidebar in-flexbox"> + <div class="nav outline"> + <p><a class="accordion" href="#">series 1</a></p> + <p><a class="accordion" href="#">series 2</a></p> + <p><a class="accordion" href="#">series 3</a></p> + </div> + <div class="nav outline bottom"> + <p><a href="#">works</a></p> + <p><a href="#">about</a></p> + <p><a href="#">contact</a></p> + <p><a href="#">insta</a></p> + </div> + </nav> + + <main class="container outline in-flexbox"> + {% block content %}{% endblock %} + </main> + </div> + <footer> + </footer> + </div> +</body> +</html> |