Commit
·
95fd3af
1
Parent(s):
cdf4e4e
Create README.md
Browse files- README.md +67 -14
- ice_breaking_challenge/__init__.py +51 -0
- ice_breaking_challenge/auth.py +116 -0
- ice_breaking_challenge/blog.py +125 -0
- ice_breaking_challenge/db.py +52 -0
- ice_breaking_challenge/schema.sql +20 -0
- ice_breaking_challenge/static/style.css +134 -0
- ice_breaking_challenge/templates/auth/login.html +15 -0
- ice_breaking_challenge/templates/auth/register.html +15 -0
- ice_breaking_challenge/templates/base.html +24 -0
- ice_breaking_challenge/templates/blog/create.html +15 -0
- ice_breaking_challenge/templates/blog/index.html +28 -0
- ice_breaking_challenge/templates/blog/update.html +19 -0
README.md
CHANGED
@@ -1,14 +1,67 @@
|
|
1 |
-
|
2 |
-
```
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Ice Breaking Challenge
|
2 |
+
```mermaid
|
3 |
+
sequenceDiagram
|
4 |
+
actor user as 사용자
|
5 |
+
participant browser as 브라우저
|
6 |
+
participant flask as Flask
|
7 |
+
%% participant sheets as Google Sheets
|
8 |
+
%% participant gemma as Fine-Tuned Gemma 2
|
9 |
+
|
10 |
+
|
11 |
+
autonumber
|
12 |
+
critical 팀 정보 입력
|
13 |
+
user ->> browser: Hugging Face Space 접근
|
14 |
+
browser ->> flask: `팀 정보 입력 페이지` 요청 (json)
|
15 |
+
flask ->> browser: `팀 정보 입력 페이지` 응답 (html & js)
|
16 |
+
browser ->> user: `팀 정보 입력 페이지` 렌더링
|
17 |
+
end
|
18 |
+
|
19 |
+
critical 설문 QR
|
20 |
+
user ->> browser: `팀 정보 입력 페이지`의 `다음` 버튼 클릭
|
21 |
+
browser ->> flask: `설문 QR 페이지` 요청 (json)
|
22 |
+
flask ->> browser: `설문 QR 페이지` 응답 (html & js)
|
23 |
+
browser ->> user: `설문 QR 페이지` 렌더링
|
24 |
+
end
|
25 |
+
|
26 |
+
critical 자기소개
|
27 |
+
user ->> browser: `팀 정보 입력 페이지`의 `다음` 버튼 클릭
|
28 |
+
browser ->> flask: `자기소개 페이지` 요청 (json)
|
29 |
+
flask ->> browser: `자기소개 페이지` 응답 (html & js)
|
30 |
+
browser ->> user: `자기소개 페이지` 렌더링
|
31 |
+
Note right of user: 질문 생성 완료될 때까지 `다음` 버튼 렌더링 X
|
32 |
+
end
|
33 |
+
|
34 |
+
critical 설문 및 질문 생성 완료 확인
|
35 |
+
browser ->> flask: 설문 완료 확인 요청 (json)
|
36 |
+
create participant sheets as Google Sheets
|
37 |
+
flask ->> sheets: 설문 요청
|
38 |
+
destroy sheets
|
39 |
+
sheets ->> flask: 설문 응답
|
40 |
+
flask ->> flask: 팀 정보와 설문 내역 대조
|
41 |
+
create participant gemma
|
42 |
+
flask ->> gemma: 설문 완료되었다면, 질문 생성 요청
|
43 |
+
destroy gemma
|
44 |
+
gemma ->> flask: 질문 생성 응답
|
45 |
+
flask ->> browser: 질문 생성 완료된 경우 `다음` 버튼 응답
|
46 |
+
browser ->> user: `다음` 버튼 렌더링
|
47 |
+
end
|
48 |
+
|
49 |
+
critical 젬마 생성 질문 1번
|
50 |
+
user ->> browser: `자기소개 페이지`의 `다음` 버튼 클릭
|
51 |
+
browser ->> flask: `생성 질문 1번 페이지 요청` (json)
|
52 |
+
flask ->> browser: `생성 질문 1번 페이지 응답` (html & js)
|
53 |
+
browser ->> user: `생성 질문 1번 페이지 응답` 렌더링
|
54 |
+
end
|
55 |
+
|
56 |
+
critical 젬마 생성 질문 2번
|
57 |
+
user ->> browser: `생성 질문 1번 페이지`의 `다음` 버튼 클릭 (json)
|
58 |
+
Note left of flask: 이하 동일
|
59 |
+
end
|
60 |
+
|
61 |
+
critical 친해지셨나요
|
62 |
+
user ->> browser: `생성 질문 마지막 페이지`의 `다음` 버튼 클릭 (json)
|
63 |
+
browser ->> flask: `생성 질문 마지막 페이지` 요청 (json)
|
64 |
+
flask ->> browser: `생성 질문 마지막 페이지` 응답 (html & js)
|
65 |
+
browser ->> user: `생성 질문 마지막 페이지` 렌더링
|
66 |
+
end
|
67 |
+
```
|
ice_breaking_challenge/__init__.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
from flask import Flask
|
4 |
+
|
5 |
+
|
6 |
+
def create_app(test_config=None):
|
7 |
+
"""Create and configure an instance of the Flask application."""
|
8 |
+
app = Flask(__name__, instance_relative_config=True)
|
9 |
+
app.config.from_mapping(
|
10 |
+
# a default secret that should be overridden by instance config
|
11 |
+
SECRET_KEY="dev",
|
12 |
+
# store the database in the instance folder
|
13 |
+
DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"),
|
14 |
+
)
|
15 |
+
|
16 |
+
if test_config is None:
|
17 |
+
# load the instance config, if it exists, when not testing
|
18 |
+
app.config.from_pyfile("config.py", silent=True)
|
19 |
+
else:
|
20 |
+
# load the test config if passed in
|
21 |
+
app.config.update(test_config)
|
22 |
+
|
23 |
+
# ensure the instance folder exists
|
24 |
+
try:
|
25 |
+
os.makedirs(app.instance_path)
|
26 |
+
except OSError:
|
27 |
+
pass
|
28 |
+
|
29 |
+
@app.route("/hello")
|
30 |
+
def hello():
|
31 |
+
return "Hello, World!"
|
32 |
+
|
33 |
+
# register the database commands
|
34 |
+
from . import db
|
35 |
+
|
36 |
+
db.init_app(app)
|
37 |
+
|
38 |
+
# apply the blueprints to the app
|
39 |
+
from . import auth
|
40 |
+
from . import blog
|
41 |
+
|
42 |
+
app.register_blueprint(auth.bp)
|
43 |
+
app.register_blueprint(blog.bp)
|
44 |
+
|
45 |
+
# make url_for('index') == url_for('blog.index')
|
46 |
+
# in another app, you might define a separate main index here with
|
47 |
+
# app.route, while giving the blog blueprint a url_prefix, but for
|
48 |
+
# the tutorial the blog will be the main index
|
49 |
+
app.add_url_rule("/", endpoint="index")
|
50 |
+
|
51 |
+
return app
|
ice_breaking_challenge/auth.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import functools
|
2 |
+
|
3 |
+
from flask import Blueprint
|
4 |
+
from flask import flash
|
5 |
+
from flask import g
|
6 |
+
from flask import redirect
|
7 |
+
from flask import render_template
|
8 |
+
from flask import request
|
9 |
+
from flask import session
|
10 |
+
from flask import url_for
|
11 |
+
from werkzeug.security import check_password_hash
|
12 |
+
from werkzeug.security import generate_password_hash
|
13 |
+
|
14 |
+
from .db import get_db
|
15 |
+
|
16 |
+
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
17 |
+
|
18 |
+
|
19 |
+
def login_required(view):
|
20 |
+
"""View decorator that redirects anonymous users to the login page."""
|
21 |
+
|
22 |
+
@functools.wraps(view)
|
23 |
+
def wrapped_view(**kwargs):
|
24 |
+
if g.user is None:
|
25 |
+
return redirect(url_for("auth.login"))
|
26 |
+
|
27 |
+
return view(**kwargs)
|
28 |
+
|
29 |
+
return wrapped_view
|
30 |
+
|
31 |
+
|
32 |
+
@bp.before_app_request
|
33 |
+
def load_logged_in_user():
|
34 |
+
"""If a user id is stored in the session, load the user object from
|
35 |
+
the database into ``g.user``."""
|
36 |
+
user_id = session.get("user_id")
|
37 |
+
|
38 |
+
if user_id is None:
|
39 |
+
g.user = None
|
40 |
+
else:
|
41 |
+
g.user = (
|
42 |
+
get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone()
|
43 |
+
)
|
44 |
+
|
45 |
+
|
46 |
+
@bp.route("/register", methods=("GET", "POST"))
|
47 |
+
def register():
|
48 |
+
"""Register a new user.
|
49 |
+
|
50 |
+
Validates that the username is not already taken. Hashes the
|
51 |
+
password for security.
|
52 |
+
"""
|
53 |
+
if request.method == "POST":
|
54 |
+
username = request.form["username"]
|
55 |
+
password = request.form["password"]
|
56 |
+
db = get_db()
|
57 |
+
error = None
|
58 |
+
|
59 |
+
if not username:
|
60 |
+
error = "Username is required."
|
61 |
+
elif not password:
|
62 |
+
error = "Password is required."
|
63 |
+
|
64 |
+
if error is None:
|
65 |
+
try:
|
66 |
+
db.execute(
|
67 |
+
"INSERT INTO user (username, password) VALUES (?, ?)",
|
68 |
+
(username, generate_password_hash(password)),
|
69 |
+
)
|
70 |
+
db.commit()
|
71 |
+
except db.IntegrityError:
|
72 |
+
# The username was already taken, which caused the
|
73 |
+
# commit to fail. Show a validation error.
|
74 |
+
error = f"User {username} is already registered."
|
75 |
+
else:
|
76 |
+
# Success, go to the login page.
|
77 |
+
return redirect(url_for("auth.login"))
|
78 |
+
|
79 |
+
flash(error)
|
80 |
+
|
81 |
+
return render_template("auth/register.html")
|
82 |
+
|
83 |
+
|
84 |
+
@bp.route("/login", methods=("GET", "POST"))
|
85 |
+
def login():
|
86 |
+
"""Log in a registered user by adding the user id to the session."""
|
87 |
+
if request.method == "POST":
|
88 |
+
username = request.form["username"]
|
89 |
+
password = request.form["password"]
|
90 |
+
db = get_db()
|
91 |
+
error = None
|
92 |
+
user = db.execute(
|
93 |
+
"SELECT * FROM user WHERE username = ?", (username,)
|
94 |
+
).fetchone()
|
95 |
+
|
96 |
+
if user is None:
|
97 |
+
error = "Incorrect username."
|
98 |
+
elif not check_password_hash(user["password"], password):
|
99 |
+
error = "Incorrect password."
|
100 |
+
|
101 |
+
if error is None:
|
102 |
+
# store the user id in a new session and return to the index
|
103 |
+
session.clear()
|
104 |
+
session["user_id"] = user["id"]
|
105 |
+
return redirect(url_for("index"))
|
106 |
+
|
107 |
+
flash(error)
|
108 |
+
|
109 |
+
return render_template("auth/login.html")
|
110 |
+
|
111 |
+
|
112 |
+
@bp.route("/logout")
|
113 |
+
def logout():
|
114 |
+
"""Clear the current session, including the stored user id."""
|
115 |
+
session.clear()
|
116 |
+
return redirect(url_for("index"))
|
ice_breaking_challenge/blog.py
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint
|
2 |
+
from flask import flash
|
3 |
+
from flask import g
|
4 |
+
from flask import redirect
|
5 |
+
from flask import render_template
|
6 |
+
from flask import request
|
7 |
+
from flask import url_for
|
8 |
+
from werkzeug.exceptions import abort
|
9 |
+
|
10 |
+
from .auth import login_required
|
11 |
+
from .db import get_db
|
12 |
+
|
13 |
+
bp = Blueprint("blog", __name__)
|
14 |
+
|
15 |
+
|
16 |
+
@bp.route("/")
|
17 |
+
def index():
|
18 |
+
"""Show all the posts, most recent first."""
|
19 |
+
db = get_db()
|
20 |
+
posts = db.execute(
|
21 |
+
"SELECT p.id, title, body, created, author_id, username"
|
22 |
+
" FROM post p JOIN user u ON p.author_id = u.id"
|
23 |
+
" ORDER BY created DESC"
|
24 |
+
).fetchall()
|
25 |
+
return render_template("blog/index.html", posts=posts)
|
26 |
+
|
27 |
+
|
28 |
+
def get_post(id, check_author=True):
|
29 |
+
"""Get a post and its author by id.
|
30 |
+
|
31 |
+
Checks that the id exists and optionally that the current user is
|
32 |
+
the author.
|
33 |
+
|
34 |
+
:param id: id of post to get
|
35 |
+
:param check_author: require the current user to be the author
|
36 |
+
:return: the post with author information
|
37 |
+
:raise 404: if a post with the given id doesn't exist
|
38 |
+
:raise 403: if the current user isn't the author
|
39 |
+
"""
|
40 |
+
post = (
|
41 |
+
get_db()
|
42 |
+
.execute(
|
43 |
+
"SELECT p.id, title, body, created, author_id, username"
|
44 |
+
" FROM post p JOIN user u ON p.author_id = u.id"
|
45 |
+
" WHERE p.id = ?",
|
46 |
+
(id,),
|
47 |
+
)
|
48 |
+
.fetchone()
|
49 |
+
)
|
50 |
+
|
51 |
+
if post is None:
|
52 |
+
abort(404, f"Post id {id} doesn't exist.")
|
53 |
+
|
54 |
+
if check_author and post["author_id"] != g.user["id"]:
|
55 |
+
abort(403)
|
56 |
+
|
57 |
+
return post
|
58 |
+
|
59 |
+
|
60 |
+
@bp.route("/create", methods=("GET", "POST"))
|
61 |
+
@login_required
|
62 |
+
def create():
|
63 |
+
"""Create a new post for the current user."""
|
64 |
+
if request.method == "POST":
|
65 |
+
title = request.form["title"]
|
66 |
+
body = request.form["body"]
|
67 |
+
error = None
|
68 |
+
|
69 |
+
if not title:
|
70 |
+
error = "Title is required."
|
71 |
+
|
72 |
+
if error is not None:
|
73 |
+
flash(error)
|
74 |
+
else:
|
75 |
+
db = get_db()
|
76 |
+
db.execute(
|
77 |
+
"INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)",
|
78 |
+
(title, body, g.user["id"]),
|
79 |
+
)
|
80 |
+
db.commit()
|
81 |
+
return redirect(url_for("blog.index"))
|
82 |
+
|
83 |
+
return render_template("blog/create.html")
|
84 |
+
|
85 |
+
|
86 |
+
@bp.route("/<int:id>/update", methods=("GET", "POST"))
|
87 |
+
@login_required
|
88 |
+
def update(id):
|
89 |
+
"""Update a post if the current user is the author."""
|
90 |
+
post = get_post(id)
|
91 |
+
|
92 |
+
if request.method == "POST":
|
93 |
+
title = request.form["title"]
|
94 |
+
body = request.form["body"]
|
95 |
+
error = None
|
96 |
+
|
97 |
+
if not title:
|
98 |
+
error = "Title is required."
|
99 |
+
|
100 |
+
if error is not None:
|
101 |
+
flash(error)
|
102 |
+
else:
|
103 |
+
db = get_db()
|
104 |
+
db.execute(
|
105 |
+
"UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id)
|
106 |
+
)
|
107 |
+
db.commit()
|
108 |
+
return redirect(url_for("blog.index"))
|
109 |
+
|
110 |
+
return render_template("blog/update.html", post=post)
|
111 |
+
|
112 |
+
|
113 |
+
@bp.route("/<int:id>/delete", methods=("POST",))
|
114 |
+
@login_required
|
115 |
+
def delete(id):
|
116 |
+
"""Delete a post.
|
117 |
+
|
118 |
+
Ensures that the post exists and that the logged in user is the
|
119 |
+
author of the post.
|
120 |
+
"""
|
121 |
+
get_post(id)
|
122 |
+
db = get_db()
|
123 |
+
db.execute("DELETE FROM post WHERE id = ?", (id,))
|
124 |
+
db.commit()
|
125 |
+
return redirect(url_for("blog.index"))
|
ice_breaking_challenge/db.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sqlite3
|
2 |
+
|
3 |
+
import click
|
4 |
+
from flask import current_app
|
5 |
+
from flask import g
|
6 |
+
|
7 |
+
|
8 |
+
def get_db():
|
9 |
+
"""Connect to the application's configured database. The connection
|
10 |
+
is unique for each request and will be reused if this is called
|
11 |
+
again.
|
12 |
+
"""
|
13 |
+
if "db" not in g:
|
14 |
+
g.db = sqlite3.connect(
|
15 |
+
current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES
|
16 |
+
)
|
17 |
+
g.db.row_factory = sqlite3.Row
|
18 |
+
|
19 |
+
return g.db
|
20 |
+
|
21 |
+
|
22 |
+
def close_db(e=None):
|
23 |
+
"""If this request connected to the database, close the
|
24 |
+
connection.
|
25 |
+
"""
|
26 |
+
db = g.pop("db", None)
|
27 |
+
|
28 |
+
if db is not None:
|
29 |
+
db.close()
|
30 |
+
|
31 |
+
|
32 |
+
def init_db():
|
33 |
+
"""Clear existing data and create new tables."""
|
34 |
+
db = get_db()
|
35 |
+
|
36 |
+
with current_app.open_resource("schema.sql") as f:
|
37 |
+
db.executescript(f.read().decode("utf8"))
|
38 |
+
|
39 |
+
|
40 |
+
@click.command("init-db")
|
41 |
+
def init_db_command():
|
42 |
+
"""Clear existing data and create new tables."""
|
43 |
+
init_db()
|
44 |
+
click.echo("Initialized the database.")
|
45 |
+
|
46 |
+
|
47 |
+
def init_app(app):
|
48 |
+
"""Register database functions with the Flask app. This is called by
|
49 |
+
the application factory.
|
50 |
+
"""
|
51 |
+
app.teardown_appcontext(close_db)
|
52 |
+
app.cli.add_command(init_db_command)
|
ice_breaking_challenge/schema.sql
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-- Initialize the database.
|
2 |
+
-- Drop any existing data and create empty tables.
|
3 |
+
|
4 |
+
DROP TABLE IF EXISTS user;
|
5 |
+
DROP TABLE IF EXISTS post;
|
6 |
+
|
7 |
+
CREATE TABLE user (
|
8 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
9 |
+
username TEXT UNIQUE NOT NULL,
|
10 |
+
password TEXT NOT NULL
|
11 |
+
);
|
12 |
+
|
13 |
+
CREATE TABLE post (
|
14 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
15 |
+
author_id INTEGER NOT NULL,
|
16 |
+
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
17 |
+
title TEXT NOT NULL,
|
18 |
+
body TEXT NOT NULL,
|
19 |
+
FOREIGN KEY (author_id) REFERENCES user (id)
|
20 |
+
);
|
ice_breaking_challenge/static/style.css
ADDED
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
html {
|
2 |
+
font-family: sans-serif;
|
3 |
+
background: #eee;
|
4 |
+
padding: 1rem;
|
5 |
+
}
|
6 |
+
|
7 |
+
body {
|
8 |
+
max-width: 960px;
|
9 |
+
margin: 0 auto;
|
10 |
+
background: white;
|
11 |
+
}
|
12 |
+
|
13 |
+
h1, h2, h3, h4, h5, h6 {
|
14 |
+
font-family: serif;
|
15 |
+
color: #377ba8;
|
16 |
+
margin: 1rem 0;
|
17 |
+
}
|
18 |
+
|
19 |
+
a {
|
20 |
+
color: #377ba8;
|
21 |
+
}
|
22 |
+
|
23 |
+
hr {
|
24 |
+
border: none;
|
25 |
+
border-top: 1px solid lightgray;
|
26 |
+
}
|
27 |
+
|
28 |
+
nav {
|
29 |
+
background: lightgray;
|
30 |
+
display: flex;
|
31 |
+
align-items: center;
|
32 |
+
padding: 0 0.5rem;
|
33 |
+
}
|
34 |
+
|
35 |
+
nav h1 {
|
36 |
+
flex: auto;
|
37 |
+
margin: 0;
|
38 |
+
}
|
39 |
+
|
40 |
+
nav h1 a {
|
41 |
+
text-decoration: none;
|
42 |
+
padding: 0.25rem 0.5rem;
|
43 |
+
}
|
44 |
+
|
45 |
+
nav ul {
|
46 |
+
display: flex;
|
47 |
+
list-style: none;
|
48 |
+
margin: 0;
|
49 |
+
padding: 0;
|
50 |
+
}
|
51 |
+
|
52 |
+
nav ul li a, nav ul li span, header .action {
|
53 |
+
display: block;
|
54 |
+
padding: 0.5rem;
|
55 |
+
}
|
56 |
+
|
57 |
+
.content {
|
58 |
+
padding: 0 1rem 1rem;
|
59 |
+
}
|
60 |
+
|
61 |
+
.content > header {
|
62 |
+
border-bottom: 1px solid lightgray;
|
63 |
+
display: flex;
|
64 |
+
align-items: flex-end;
|
65 |
+
}
|
66 |
+
|
67 |
+
.content > header h1 {
|
68 |
+
flex: auto;
|
69 |
+
margin: 1rem 0 0.25rem 0;
|
70 |
+
}
|
71 |
+
|
72 |
+
.flash {
|
73 |
+
margin: 1em 0;
|
74 |
+
padding: 1em;
|
75 |
+
background: #cae6f6;
|
76 |
+
border: 1px solid #377ba8;
|
77 |
+
}
|
78 |
+
|
79 |
+
.post > header {
|
80 |
+
display: flex;
|
81 |
+
align-items: flex-end;
|
82 |
+
font-size: 0.85em;
|
83 |
+
}
|
84 |
+
|
85 |
+
.post > header > div:first-of-type {
|
86 |
+
flex: auto;
|
87 |
+
}
|
88 |
+
|
89 |
+
.post > header h1 {
|
90 |
+
font-size: 1.5em;
|
91 |
+
margin-bottom: 0;
|
92 |
+
}
|
93 |
+
|
94 |
+
.post .about {
|
95 |
+
color: slategray;
|
96 |
+
font-style: italic;
|
97 |
+
}
|
98 |
+
|
99 |
+
.post .body {
|
100 |
+
white-space: pre-line;
|
101 |
+
}
|
102 |
+
|
103 |
+
.content:last-child {
|
104 |
+
margin-bottom: 0;
|
105 |
+
}
|
106 |
+
|
107 |
+
.content form {
|
108 |
+
margin: 1em 0;
|
109 |
+
display: flex;
|
110 |
+
flex-direction: column;
|
111 |
+
}
|
112 |
+
|
113 |
+
.content label {
|
114 |
+
font-weight: bold;
|
115 |
+
margin-bottom: 0.5em;
|
116 |
+
}
|
117 |
+
|
118 |
+
.content input, .content textarea {
|
119 |
+
margin-bottom: 1em;
|
120 |
+
}
|
121 |
+
|
122 |
+
.content textarea {
|
123 |
+
min-height: 12em;
|
124 |
+
resize: vertical;
|
125 |
+
}
|
126 |
+
|
127 |
+
input.danger {
|
128 |
+
color: #cc2f2e;
|
129 |
+
}
|
130 |
+
|
131 |
+
input[type=submit] {
|
132 |
+
align-self: start;
|
133 |
+
min-width: 10em;
|
134 |
+
}
|
ice_breaking_challenge/templates/auth/login.html
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block header %}
|
4 |
+
<h1>{% block title %}Log In{% endblock %}</h1>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block content %}
|
8 |
+
<form method="post">
|
9 |
+
<label for="username">Username</label>
|
10 |
+
<input name="username" id="username" required>
|
11 |
+
<label for="password">Password</label>
|
12 |
+
<input type="password" name="password" id="password" required>
|
13 |
+
<input type="submit" value="Log In">
|
14 |
+
</form>
|
15 |
+
{% endblock %}
|
ice_breaking_challenge/templates/auth/register.html
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block header %}
|
4 |
+
<h1>{% block title %}Register{% endblock %}</h1>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block content %}
|
8 |
+
<form method="post">
|
9 |
+
<label for="username">Username</label>
|
10 |
+
<input name="username" id="username" required>
|
11 |
+
<label for="password">Password</label>
|
12 |
+
<input type="password" name="password" id="password" required>
|
13 |
+
<input type="submit" value="Register">
|
14 |
+
</form>
|
15 |
+
{% endblock %}
|
ice_breaking_challenge/templates/base.html
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<title>{% block title %}{% endblock %} - Flaskr</title>
|
3 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
4 |
+
<nav>
|
5 |
+
<h1><a href="{{ url_for('index') }}">Flaskr</a></h1>
|
6 |
+
<ul>
|
7 |
+
{% if g.user %}
|
8 |
+
<li><span>{{ g.user['username'] }}</span>
|
9 |
+
<li><a href="{{ url_for('auth.logout') }}">Log Out</a>
|
10 |
+
{% else %}
|
11 |
+
<li><a href="{{ url_for('auth.register') }}">Register</a>
|
12 |
+
<li><a href="{{ url_for('auth.login') }}">Log In</a>
|
13 |
+
{% endif %}
|
14 |
+
</ul>
|
15 |
+
</nav>
|
16 |
+
<section class="content">
|
17 |
+
<header>
|
18 |
+
{% block header %}{% endblock %}
|
19 |
+
</header>
|
20 |
+
{% for message in get_flashed_messages() %}
|
21 |
+
<div class="flash">{{ message }}</div>
|
22 |
+
{% endfor %}
|
23 |
+
{% block content %}{% endblock %}
|
24 |
+
</section>
|
ice_breaking_challenge/templates/blog/create.html
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block header %}
|
4 |
+
<h1>{% block title %}New Post{% endblock %}</h1>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block content %}
|
8 |
+
<form method="post">
|
9 |
+
<label for="title">Title</label>
|
10 |
+
<input name="title" id="title" value="{{ request.form['title'] }}" required>
|
11 |
+
<label for="body">Body</label>
|
12 |
+
<textarea name="body" id="body">{{ request.form['body'] }}</textarea>
|
13 |
+
<input type="submit" value="Save">
|
14 |
+
</form>
|
15 |
+
{% endblock %}
|
ice_breaking_challenge/templates/blog/index.html
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block header %}
|
4 |
+
<h1>{% block title %}Posts{% endblock %}</h1>
|
5 |
+
{% if g.user %}
|
6 |
+
<a class="action" href="{{ url_for('blog.create') }}">New</a>
|
7 |
+
{% endif %}
|
8 |
+
{% endblock %}
|
9 |
+
|
10 |
+
{% block content %}
|
11 |
+
{% for post in posts %}
|
12 |
+
<article class="post">
|
13 |
+
<header>
|
14 |
+
<div>
|
15 |
+
<h1>{{ post['title'] }}</h1>
|
16 |
+
<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
|
17 |
+
</div>
|
18 |
+
{% if g.user['id'] == post['author_id'] %}
|
19 |
+
<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
|
20 |
+
{% endif %}
|
21 |
+
</header>
|
22 |
+
<p class="body">{{ post['body'] }}</p>
|
23 |
+
</article>
|
24 |
+
{% if not loop.last %}
|
25 |
+
<hr>
|
26 |
+
{% endif %}
|
27 |
+
{% endfor %}
|
28 |
+
{% endblock %}
|
ice_breaking_challenge/templates/blog/update.html
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block header %}
|
4 |
+
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block content %}
|
8 |
+
<form method="post">
|
9 |
+
<label for="title">Title</label>
|
10 |
+
<input name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required>
|
11 |
+
<label for="body">Body</label>
|
12 |
+
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
|
13 |
+
<input type="submit" value="Save">
|
14 |
+
</form>
|
15 |
+
<hr>
|
16 |
+
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
|
17 |
+
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
|
18 |
+
</form>
|
19 |
+
{% endblock %}
|