見出し画像

CS50 2023 - Week10 Emoji


概要

Week10では、絵文字とその背景にある技術について学びます。
講義の主な内容は、精度の重要性、Unicodeにおける絵文字、コードポイント、そしてZWJ(ゼロ幅結合子)に関する詳細です。

今回の講義は課題と直接の関連はありませんが、非常に興味深い内容となっています。


Lab

Week10にはLabはありません。


Final Project

Final Projectはその名の通り、CS50最後のプロジェクトです。
課題の内容は、オリジナルのプログラムを開発することです。
テーマの選定は自由ですし、使用する言語や開発環境の制限もありません。
これまでに学んできたことや、新たに得た知識を活用して、自分の興味を持つものを作成しましょう。

…と言われても、何を作るべきか迷ってしまう人も多いかと思います。
私自身、具体的なアイディアがなかったため、何を作れば良いのか考える必要がありました。

CS50の公式サイトには、これまでの受講者が提出したFinal Projectのギャラリーがあります。ギャラリーの動画を眺めていると、世界中の様々な背景を持つ方々が学び、Final Projectを提出してきたんだなあと感じます。そして、閃きました。お寿司を紹介するサイトを作ろうと。

Japan Sushi Journey
私が知る限り、欧米の方々が知っているお寿司は、基本的なにぎり巻き寿司だけです。しかし、日本には各地域固有の郷土寿司というものがあります。これを紹介しないのはもったいない!

というわけで、日本を9つのエリアに分けて、それぞれのお寿司を紹介するサイトを作ることにしました。本当は47都道府県に分けて紹介できればベストではあったのですが、情報収集の困難さから泣く泣く断念しました。

提出方法
Final Projectの提出はこれまでと少々勝手が異なります。
プログラムと共に下記も提出必須のため、それを念頭に置いた上で制作を始めましょう。

  • プログラムを紹介するYoutube動画(最長3分)

  • プログラムの内容を説明するREADME.txt

以下は実際に私が提出したコード、動画、README.txtです。

app.py

import os

from cs50 import SQL
from flask import Flask, flash, url_for, redirect, render_template, request, session
from flask_session import Session
from tempfile import mkdtemp
from werkzeug.security import check_password_hash, generate_password_hash
from math import ceil

from helpers import apology, login_required

# Configure application
app = Flask(__name__)


# Define the new filter
@app.template_filter()
def rating_to_stars(rating):
    return "☆" * ceil(float(rating))


# Configure session to use filesystem (instead of signed cookies)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)

# Configure CS50 Library to use SQLite database
db = SQL("sqlite:///sushi.db")


@app.after_request
def after_request(response):
    """Ensure responses aren't cached"""
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    response.headers["Expires"] = 0
    response.headers["Pragma"] = "no-cache"
    return response


@app.context_processor
def inject_sushis():
    sushis = db.execute("SELECT * FROM sushi")
    return dict(sushis=sushis)


@app.route("/")
def index():
    sushis = db.execute("SELECT * FROM sushi")
    return render_template("index.html", sushis=sushis)


@app.route("/login", methods=["GET", "POST"])
def login():
    """Log user in"""

    # Forget any user_id
    session.clear()

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        # Ensure username was submitted
        if not request.form.get("username"):
            return apology("must provide username", 400)

        # Ensure password was submitted
        elif not request.form.get("password"):
            return apology("must provide password", 400)

        # Query database for username
        rows = db.execute(
            "SELECT * FROM users WHERE username = ?", request.form.get("username")
        )

        # Ensure username exists and password is correct
        if len(rows) != 1 or not check_password_hash(
            rows[0]["hash"], request.form.get("password")
        ):
            return apology("invalid username and/or password", 400)

        # Remember which user has logged in
        session["user_id"] = rows[0]["id"]

        # Redirect user to home page
        return redirect("/")

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        return render_template("login.html")


@app.route("/logout")
@login_required
def logout():
    """Log user out"""

    # Forget any user_id
    session.clear()

    # Redirect user to login form
    return redirect("/")


@app.route("/register", methods=["GET", "POST"])
def register():
    """Register user"""
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        confirmation = request.form.get("confirmation")

        # Ensure username was submitted
        if not username:
            return apology("must provide username", 400)

        # Ensure password was submitted
        elif not password:
            return apology("must provide password", 400)

        # Ensure password confirmation was submitted and matches password
        elif not confirmation or password != confirmation:
            return apology("passwords must match", 400)

        # Query database for username
        rows = db.execute("SELECT * FROM users WHERE username = ?", username)

        # Check if username already exists
        if len(rows) == 1:
            return apology("username already exists", 400)

        # Insert new user into database, storing a hash of the user's password
        db.execute(
            "INSERT INTO users (username, hash) VALUES (?, ?)",
            username,
            generate_password_hash(password),
        )

        # Redirect user to login page
        return redirect("/login")

    else:
        # User reached route via GET (as by clicking a link or via redirect)
        return render_template("register.html")


@app.route("/change_password", methods=["GET", "POST"])
@login_required
def change_password():
    """Change user password."""

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        # Ensure old password was submitted
        if not request.form.get("old_password"):
            return apology("must provide old password", 400)

        # Ensure new password was submitted
        if not request.form.get("new_password"):
            return apology("must provide new password", 400)

        # Ensure confirmation password is the same
        if request.form.get("new_password") != request.form.get("confirm_new_password"):
            return apology("new passwords must match", 400)

        # Query database for username
        rows = db.execute("SELECT * FROM users WHERE id = :id", id=session["user_id"])

        # Ensure old password is correct
        if len(rows) != 1 or not check_password_hash(
            rows[0]["hash"], request.form.get("old_password")
        ):
            return apology("invalid old password", 400)

        # Hash the new password
        hash = generate_password_hash(request.form.get("new_password"))

        # Update the database with the new password hash
        db.execute(
            "UPDATE users SET hash = :hash WHERE id = :id",
            hash=hash,
            id=session["user_id"],
        )

        # Flash a message to let the user know their password was updated
        flash("Password Changed")

        # Redirect user to home page
        return redirect("/")

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        return render_template("change_password.html")


@app.route("/sushi/<int:sushi_id>")
def sushi_detail(sushi_id):
    sushi = db.execute("SELECT * FROM sushi WHERE id = ?", sushi_id)[0]
    reviews = db.execute("SELECT * FROM reviews WHERE sushi_id = ?", sushi_id)
    average_rating = db.execute(
        "SELECT AVG(rating) FROM reviews WHERE sushi_id = ?", sushi_id
    )[0]["AVG(rating)"]
    return render_template(
        "sushi_detail.html", sushi=sushi, reviews=reviews, average_rating=average_rating
    )


@app.route("/post_review/<int:sushi_id>", methods=["POST"])
@login_required
def post_review(sushi_id):
    # Get user input from the form
    rating = request.form.get("rating")
    review = request.form.get("review")

    # Check if rating and review are not empty
    if not rating or not review:
        flash("Rating and review cannot be empty!")
        return redirect(url_for("sushi_detail", sushi_id=sushi_id))

    # Check if user has already reviewed this sushi
    existing_review = db.execute(
        "SELECT * FROM reviews WHERE user_id = ? AND sushi_id = ?",
        session["user_id"],
        sushi_id,
    )

    # If a review already exists, flash a message and redirect to sushi detail page
    if existing_review:
        flash(
            "You have already reviewed this sushi. You can edit or delete your existing review."
        )
        return redirect(url_for("sushi_detail", sushi_id=sushi_id))

    # Get username from user_id
    user_row = db.execute("SELECT username FROM users WHERE id = ?", session["user_id"])
    username = user_row[0]["username"] if user_row else None

    # If no review exists, insert review into the database
    db.execute(
        "INSERT INTO reviews (user_id, sushi_id, rating, review, user_name) VALUES (?, ?, ?, ?, ?)",
        session["user_id"],
        sushi_id,
        rating,
        review,
        username,
    )

    # Redirect user to the sushi detail page
    return redirect(url_for("sushi_detail", sushi_id=sushi_id))


@app.route("/edit_review/<int:review_id>", methods=["GET", "POST"])
@login_required
def edit_review(review_id):
    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        rating = request.form.get("rating")
        review = request.form.get("review")

        # Ensure rating and review were submitted properly
        if not rating or not review:
            return apology("must provide rating and review", 400)

        # Update review in the database
        db.execute(
            "UPDATE reviews SET rating = ?, review = ? WHERE id = ?",
            rating,
            review,
            review_id,
        )

        # Redirect user to the sushi detail page
        sushi_id = db.execute("SELECT sushi_id FROM reviews WHERE id = ?", review_id)[
            0
        ]["sushi_id"]
        return redirect(url_for("sushi_detail", sushi_id=sushi_id))

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        review = db.execute("SELECT * FROM reviews WHERE id = ?", review_id)[0]
        return render_template("edit_review.html", review=review)


@app.route("/delete_review/<int:review_id>", methods=["POST"])
@login_required
def delete_review(review_id):
    # Query database for sushi_id related to this review
    sushi_id = db.execute("SELECT sushi_id FROM reviews WHERE id = ?", review_id)[0][
        "sushi_id"
    ]

    # Delete review from the database
    db.execute("DELETE FROM reviews WHERE id = ?", review_id)

    # Redirect user to the sushi detail page
    return redirect(url_for("sushi_detail", sushi_id=sushi_id))

helpers.py

from flask import redirect, render_template, session
from functools import wraps


def apology(message, code=400):
    """Render message as an apology to user."""

    def escape(s):
        """
        Escape special characters.

        https://github.com/jacebrowning/memegen#special-characters
        """
        for old, new in [
            ("-", "--"),
            (" ", "-"),
            ("_", "__"),
            ("?", "~q"),
            ("%", "~p"),
            ("#", "~h"),
            ("/", "~s"),
            ('"', "''"),
        ]:
            s = s.replace(old, new)
        return s

    return render_template("apology.html", top=code, bottom=escape(message)), code


def login_required(f):
    """
    Decorate routes to require login.

    http://flask.pocoo.org/docs/0.12/patterns/viewdecorators/
    """

    @wraps(f)
    def decorated_function(*args, **kwargs):
        if session.get("user_id") is None:
            return redirect("/login")
        return f(*args, **kwargs)

    return decorated_function

styles.css

nav .navbar-brand {
    font-size: xx-large;
}

.rating {
    font-size: 0.8em;
    position: absolute;
    right: 0;
}

.sushi-image {
    max-width: 100%;
    height: auto;
}

.card {
    margin-top: 2rem;
}

.average-rating {
    margin-top: 5rem;
}

.nav-item.dropdown {
    margin-top: 10px;
}

index.html

{% extends "layout.html" %}

{% block title %}
    Sushi Portfolio
{% endblock %}

{% block main %}
    <main class="container py-5 text-center">
        <div class="row">
            <div class="col-12">
                <img src="{{ url_for('static', filename='images/map.png') }}" alt="Map Image" class="img-fluid" style="width: 60%;">
            </div>
        </div>
        <div class="row">
            {% for sushi in sushis %}
                <div class="col-md-4">
                    <div class="card mb-4">
                        <div class="card-body">
                            <h5 class="card-title">{{ sushi.sushi_name }}</h5>
                            <h6 class="card-subtitle mb-2 text-muted">{{ sushi.area }}</h6>
                            <a href="{{ url_for('sushi_detail', sushi_id=sushi.id) }}" class="btn btn-primary">Details</a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>
    </main>
{% endblock %}

layout.html

<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1, width=device-width">
        <!-- http://getbootstrap.com/docs/5.1/ -->
        <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" rel="stylesheet">
        <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
        <title>Japan Sushi Journey: {% block title %}{% endblock %}</title>
    </head>

    <body>

        <nav class="bg-light border navbar navbar-expand-md navbar-light">
            <div class="container-fluid">
                <a class="navbar-brand" href="/">Japan Sushi Journey</a>
                <button aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" data-bs-target="#navbar" data-bs-toggle="collapse" type="button">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbar">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="sushiDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                Sushi menu
                            </a>
                            <ul class="dropdown-menu" aria-labelledby="sushiDropdown">
                                {% for sushi in sushis %}
                                    <li><a class="dropdown-item" href="{{ url_for('sushi_detail', sushi_id=sushi.id) }}">{{ sushi.sushi_name }}</a></li>
                                {% endfor %}
                            </ul>
                        </li>
                    </ul>
                    {% if session["user_id"] %}
                        <ul class="navbar-nav ms-auto mt-2">
                            <li class="nav-item"><a class="nav-link" href="/change_password">Change Password</a></li>
                            <li class="nav-item"><a class="nav-link" href="/logout">Log Out</a></li>
                        </ul>
                    {% else %}
                        <ul class="navbar-nav ms-auto mt-2">
                            <li class="nav-item"><a class="nav-link" href="/register">Register</a></li>
                            <li class="nav-item"><a class="nav-link" href="/login">Log In</a></li>
                        </ul>
                    {% endif %}
                </div>
            </div>
        </nav>

        {% with messages = get_flashed_messages() %}
            {% if messages %}
                <div class="alert alert-warning">
                    {{ messages[0] }}
                </div>
            {% endif %}
        {% endwith %}

        <main class="container-fluid py-5 text-center">
            {% block main %}{% endblock %}
        </main>
        <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"></script>
    </body>

</html>

register.html
アカウント作成ページ

{% extends "layout.html" %}

{% block title %}
    Register
{% endblock %}

{% block main %}
    <form action="/register" method="post">
        <div class="mb-3">
            <input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="username" placeholder="Username" type="text">
        </div>
        <div class="mb-3">
            <input class="form-control mx-auto w-auto" name="password" placeholder="Password" type="password">
        </div>
        <div class="mb-3">
            <input class="form-control mx-auto w-auto" name="confirmation" placeholder="Password (again)" type="password">
        </div>
        <button class="btn btn-primary" type="submit">Register</button>
    </form>
{% endblock %}

login.html

{% extends "layout.html" %}

{% block title %}
    Log In
{% endblock %}

{% block main %}
    <form action="/login" method="post">
        <div class="mb-3">
            <input autocomplete="off" autofocus class="form-control mx-auto w-auto" id="username" name="username" placeholder="Username" type="text">
        </div>
        <div class="mb-3">
            <input class="form-control mx-auto w-auto" id="password" name="password" placeholder="Password" type="password">
        </div>
        <button class="btn btn-primary" type="submit">Log In</button>
    </form>
{% endblock %}

change_password.html
パスワード変更ページ

{% extends "layout.html" %}

{% block title %}
    Change Password
{% endblock %}

{% block main %}
    <form action="/change_password" method="post">
        <div class="mb-3">
            <input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="old_password" placeholder="Current Password" type="password">
        </div>
        <div class="mb-3">
            <input class="form-control mx-auto w-auto" name="new_password" placeholder="New Password" type="password">
        </div>
        <div class="mb-3">
            <input class="form-control mx-auto w-auto" name="confirm_new_password" placeholder="New Password (again)" type="password">
        </div>
        <button class="btn btn-primary" type="submit">Change Password</button>
    </form>
{% endblock %}

sushi_detail.html
寿司紹介ページ

{% extends "layout.html" %}

{% block title %}
    {{ sushi.sushi_name }}
{% endblock %}

{% block main %}
    <main class="container pt-3 pb-5">
        <div class="row">
            <div class="col-md-12">
                <h2 class="mb-3">{{ sushi.sushi_name }}</h2>
                <p class="text-muted mb-3">{{ sushi.area }}</p>
                <img src="{{ url_for('static', filename=sushi.image_url) }}" class="img-fluid sushi-image" alt="{{ sushi.sushi_name }}">
            </div>
        </div>
        <div class="row mt-5">
            <div class="col-md-12">
                <ul class="nav nav-tabs" id="myTab" role="tablist">
                    <li class="nav-item" role="presentation">
                        <a class="nav-link active" id="details-tab" data-bs-toggle="tab" href="#details" role="tab" aria-controls="details" aria-selected="true">Details</a>
                    </li>
                    {% if session["user_id"] and not user_review %}
                        <li class="nav-item" role="presentation">
                            <a class="nav-link" id="review-tab" data-bs-toggle="tab" href="#review" role="tab" aria-controls="review" aria-selected="false">Post Review</a>
                        </li>
                    {% endif %}
                </ul>
                <div class="tab-content" id="myTabContent">
                    <div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
                        <p class="mb-3 mt-3">{{ sushi.description.replace('\n', '<br>')|safe }}</p>
                        <div class="d-flex justify-content-center mb-3">
                            {% if sushi.purchase_url %}
                                <a href="{{ sushi.purchase_url }}" class="btn btn-success mt-2" style="margin-right: 5px;" target="_blank" rel="noopener noreferrer">Buy Now</a>
                            {% else %}

                                <button class="btn btn-dark mt-2" style="margin-right: 5px;" disabled>Not available for online purchase</button>
                            {% endif %}
                            <a href="{{ url_for('index') }}" class="btn btn-primary mt-2">Back</a>
                        </div>
                        {% if average_rating %}
                            <h4 class="mb-3 average-rating">Reviews: {{ average_rating|rating_to_stars }}</h4>
                        {% endif %}
                    <!-- Reviews section start here -->
                        {% if reviews %}
                            {% if reviews|length > 1 %}
                                <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="carousel" data-bs-interval="3000">
                                    <div class="carousel-inner">
                                        {% for review in reviews %}
                                            <div class="carousel-item {% if loop.first %} active {% endif %}">
                                                <div class="card mb-3 col-md-6 mx-auto">
                                                    <div class="card-body">
                                                        <h4>{{ review.user_name }}</h4>
                                                        <p>"{{ review.review }}"</p>
                                                        <h5 class="mt-3">{{ review.rating|rating_to_stars }}</h5>
                                                        {% if session["user_id"] == review.user_id %}
                                                            <a href="{{ url_for('edit_review', review_id=review.id) }}" class="btn btn-secondary">Edit</a>
                                                            <form action="{{ url_for('delete_review', review_id=review.id) }}" method="POST" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this review?');">
                                                                <button type="submit" class="btn btn-danger">Delete</button>
                                                            </form>
                                                        {% endif %}
                                                    </div>
                                                </div>
                                            </div>
                                        {% endfor %}
                                    </div>
                                    <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev">
                                        <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                        <span class="visually-hidden">Previous</span>
                                    </button>
                                    <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next">
                                        <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                        <span class="visually-hidden">Next</span>
                                    </button>
                                </div>
                            {% else %}
                                {% for review in reviews %}
                                    <div class="card mb-3 col-md-6 mx-auto">
                                        <div class="card-body">
                                            <h4>{{ review.user_name }}</h4>
                                            <p>{{ review.review }}</p>
                                            <h5 class="mt-3">{{ review.rating|rating_to_stars }}</h5>
                                            {% if session["user_id"] == review.user_id %}
                                                <a href="{{ url_for('edit_review', review_id=review.id) }}" class="btn btn-secondary">Edit</a>
                                                <form action="{{ url_for('delete_review', review_id=review.id) }}" method="POST" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this review?');">
                                                    <button type="submit" class="btn btn-danger">Delete</button>
                                                </form>
                                            {% endif %}

                                        </div>
                                    </div>
                                {% endfor %}
                            {% endif %}
                        {% endif %}
                    <!-- Reviews section ends here -->
                    </div>
                    {% if session["user_id"] and not user_review %}
                        <div class="tab-pane fade" id="review" role="tabpanel" aria-labelledby="review-tab">
                            <form method="POST" action="{{ url_for('post_review', sushi_id=sushi.id) }}">
                                <div class="form-group mb-3 mt-3">
                                    <strong><label for="rating">Rate Your Experience (1 = lowest, 5 = highest)</label></strong>
                                    <div id="rating">
                                        {% for i in range(1, 6) %}
                                            <div class="form-check form-check-inline">
                                                <input class="form-check-input" type="radio" name="rating" id="rating{{ i }}" value="{{ i }}">
                                                <label class="form-check-label" for="rating{{ i }}">{{ i }}</label>
                                            </div>
                                        {% endfor %}
                                    </div>
                                </div>
                                <div class="form-group mb-3 col-md-6 mx-auto">
                                    <strong><label for="review">Your Review</label></strong>
                                    <textarea class="form-control" id="review" name="review" rows="3"></textarea>
                                </div>
                                <button type="submit" class="btn btn-primary">Submit</button>
                            </form>
                        </div>
                    {% endif %}




                </div>
            </div>
        </div>
    </main>
{% endblock %}

post_review.html
寿司レビュー投稿ページ

{% extends "layout.html" %}

{% block title %}
    Post Review
{% endblock %}

{% block main %}
    <main class="container py-5 text-center">
        <h2>Post a Review</h2>
        <form action="{{ url_for('post_review', sushi_id=sushi_id) }}" method="POST">
            <div class="mb-3">
                <label for="rating" class="form-label">Rating</label>
                <input type="number" class="form-control" id="rating" name="rating" min="1" max="5" required>
            </div>
            <div class="mb-3">
                <label for="review" class="form-label">Review</label>
                <textarea class="form-control" id="review" name="review" rows="3" required></textarea>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </main>
{% endblock %}

edit_review.html
投稿済みレビューの編集ページ

{% extends "layout.html" %}

{% block title %}
    Edit Review
{% endblock %}

{% block main %}
    <main class="container py-5 text-center">
        <h2>Edit Your Review</h2>
        <form action="{{ url_for('edit_review', review_id=review.id) }}" method="POST">
            <div class="form-group mb-3">
                <strong><label for="rating" class="form-label">Rate Your Experience (1 = lowest, 5 = highest)</label></strong>
                <div id="rating">
                    {% for i in range(1, 6) %}
                        <div class="form-check form-check-inline">
                            <input class="form-check-input" type="radio" name="rating" id="rating{{ i }}" value="{{ i }}" {% if i == review.rating %} checked {% endif %}>
                            <label class="form-check-label" for="rating{{ i }}">{{ i }}</label>
                        </div>
                    {% endfor %}
                </div>
            </div>
            <div class="form-group mb-3 col-md-6 mx-auto">
                <strong><label for="review" class="form-label">Your Review</label></strong> <!-- Updated here -->
                <textarea class="form-control" id="review" name="review" rows="3" required>{{ review.review }}</textarea>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </main>
{% endblock %}

apology.html

{% extends "layout.html" %}

{% block title %}
    Apology
{% endblock %}

{% block main %}
<!-- https://memegen.link/ -->
    <img alt="{{ top }}" class="border img-fluid" src="http://memegen.link/custom/{{ top | urlencode }}/{{ bottom | urlencode }}.jpg?alt=https://i.imgur.com/CsCgN7Ll.png&width=400" title="{{ top }}">
{% endblock %}

README.txt

# Japan Sushi Journey

## Video Demo:
[My CS50 Final Project](https://youtu.be/qB5lkSq6yMM)

## Description:
Japan Sushi Journey offers a digital odyssey across Japan's sushi landscape. Crafted for sushi lovers and curious travellers, our platform comprehensively depicts various sushi types. Delve into a dynamic map, engross yourself in detailed descriptions, marvel at vivid images, and join the community by sharing your reviews.

## Site Overview:
Japan Sushi Journey divides the nation into nine distinct regions, each showcasing its representative sushi, letting users embark on a flavorful journey from their screens.

- **Homepage Cards**: The homepage is adorned with cards, each representing one of the nine sushi varieties from the different regions. Clicking the "Details" button inside these cards reveals a deeper look into the chosen sushi.

- **Sushi Detail Page**: Venturing into the sushi detail page, users are greeted with the sushi's name, a vivid image, and an enticing description. User-contributed reviews and the average rating further enrich this page. Reviews are showcased carousel-style, cycling every three seconds to offer a fresh perspective.

- **User Interaction**: Upon logging in, registered users can post reviews through the "Post Review" tab on the sushi detail page. However, we've limited the reviews to one sushi variety per user to maintain the site's integrity. But fear not! Users retain the right to edit or delete their reviews, ensuring they can always express their genuine opinion.

- **Purchasing Feature**: Not all sushi is available for online purchase. Some demand an in-person experience. However, a handy "Buy Now" button redirects users to the purchase page for those available online. This information is referenced from the sushi table in sushi.db. When a URL is absent, a "Not Available for Online Purchase" button appears.

- **Language Integration**: Anticipating our global audience, any Buy Now URLs pointing to Japanese text sites are integrated with Google Translate. This ensures the content gets automatically translated into English, making the purchasing process seamless and user-friendly.

## Technologies Used:
- Python
- Flask
- Javascript
- HTML
- CSS

## Files in the Project:
### Backend Files:
- `app.py`: Main application file handling routing and server-side logic.
- `helpers.py`: Contains helper functions for the application.
- `sushi.db`: Database storing sushi information, user data, and reviews.

### Templates:
- `apology.html`: Error handling page.
- `change_password.html`: Allows users to change their password.
- `edit_review.html`: Enables editing of user reviews.
- `index.html`: Main homepage with the map and sushi cards.
- `layout.html`: Base layout for the website.
- `login.html`: User login page.
- `post_review.html`: Page to post sushi reviews.
- `register.html`: User registration page.
- `sushi_detail.html`: Detailed page for each sushi variety.

### Static Files:
- `styles.css`: Styling sheet for the website.
- `images`: Directory containing all sushi images and the Japan map on the homepage.

## Design Choices:
- **Single Review Policy**: Limiting users to post just one review for each sushi type was deliberate. It was made to uphold the authenticity of reviews and deter spamming. This ensures that users give a genuine first impression without being influenced by multiple revisits.

- **'Not Available for Online Purchase' Button**: To preserve the authenticity and traditional essence of certain sushi types, a "Not Available for Online Purchase" button was integrated. This nuanced touch aims to convey the uniqueness and traditional value of specific sushi varieties that can't be bought online but must be experienced in person.

sushi.dbというデータベースも作成しています。
テーブルは以下の3つです。

  • sushi

  • users

  • reviews

sushiテーブルの内容は下図のとおりです。

最後に紹介動画です。

正直、動画の出来が今ひとつなので取り直したい気持ちでいっぱいなのです。しかし、これが実際に提出した動画であるため、このまま公開します。

当時、PCが不調だったため、動画はiPadで制作しました。
最初は動画にテロップを入れる一般的な方法を考えていましたが、ちょっとオリジナリティを持たせてみたくなり、PowerPointのスライドショー左にブラウザ、右にノートアプリという一風変わった独特な方法に。
なお、使用したのはCraftというお気に入りのノートアプリです。

Japan Sushi Journeyサイトでは、トップページには色分けされた9つのエリアを示す日本地図と、各エリアのお寿司情報が掲載されています。お寿司情報をクリックすると、該当する郷土寿司の紹介ページが開きます。

お寿司紹介ページには、お寿司の画像説明文商品購入ページへのリンクが設けられています。リンク先のページはGoogle翻訳を使用して自動的に英語に翻訳されます。ログイン済みのユーザーはお寿司のレビューを投稿することができます。投稿したレビューは、紹介ページにカルーセル方式で表示されます。レビューはひとつのお寿司につき、1ユーザー1レビューまでです。投稿済みのレビューは編集削除をすることができます。


さいごに

これにて、CS50完了です!
最後までお付き合いいただき、心より感謝申し上げます。

各Weekの内容を振り返って、非常によく練られたコースであると再認識しました。講義の明瞭さ、課題の適切さ、そしてコース全体の魅力に感動を覚えるほどです。

Week10のページにも明記されていますが、CS50の目的は、単に技術やその背景を学ぶだけでなく、獲得した知識を実際に活用する能力を培うことにあります。私は最初Pythonに興味がありましたが、このコースを通して、JavaScriptにも魅かれるようになりました。

CS50's Web Programming with Python and JavaScriptという、まさにうってつけのコースもあるようです。現在、新しい仕事を始めたばかりで学習に回せる時間が限られていますが、将来的に時間の余裕が生まれたら、是非とも挑戦してみたいと考えています。

このnoteを通じて、CS50に興味を持つきっかけになれたり、既に受講されている方々の学習のお力になれていたら、大変嬉しく思います。

この記事が気に入ったらサポートをしてみませんか?