Upload files to "/"
This commit is contained in:
parent
bfd6e68ac0
commit
9fdc21ca7d
BIN
Docker flask mongo website documentation.docx
Normal file
BIN
Docker flask mongo website documentation.docx
Normal file
Binary file not shown.
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# I will create everything needed for the application
|
||||
./prepare-app.sh
|
||||
|
||||
# I will start the application
|
||||
./start-app.sh
|
||||
|
||||
# The app should be available at:
|
||||
# http://localhost:8080
|
||||
|
||||
# I will open a web browser and work with the application.
|
||||
|
||||
# I will pause the application
|
||||
./stop-app.sh
|
||||
|
||||
# I will delete everything related to the application
|
||||
./remove-app.sh
|
||||
380
app.py
Normal file
380
app.py
Normal file
@ -0,0 +1,380 @@
|
||||
# Imports the os module so the app can read environment variables
|
||||
# such as SECRET_KEY, MONGO_URI, and PORT.
|
||||
import os
|
||||
|
||||
# Imports time so the code can wait and retry when MongoDB is not ready yet.
|
||||
import time
|
||||
|
||||
# Imports datetime so the app can store the current date and time
|
||||
# when a new product is created.
|
||||
from datetime import datetime
|
||||
|
||||
# Flask imports:
|
||||
# - Flask creates the web application
|
||||
# - render_template loads HTML files from the templates folder
|
||||
# - request reads form data sent from the browser
|
||||
# - redirect sends the user to another page
|
||||
# - url_for builds a URL from a route function name
|
||||
# - flash stores short success/error messages to show on the next page
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash
|
||||
|
||||
# PyMongo imports:
|
||||
# - MongoClient connects Python to MongoDB
|
||||
# - ASCENDING is used when creating an index in ascending order
|
||||
from pymongo import MongoClient, ASCENDING
|
||||
|
||||
# Import the specific error raised when trying to insert a duplicate value
|
||||
# into a field that has a unique index.
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
|
||||
# Imports ObjectId so MongoDB document IDs in the URL can be converted
|
||||
# into the correct MongoDB ID type for queries.
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
# Creates the Flask application object.
|
||||
app = Flask(__name__)
|
||||
|
||||
# Sets the secret key used by Flask for flash messages and sessions.
|
||||
# It first tries to read SECRET_KEY from the environment.
|
||||
# If not found, it uses "dev-secret-key" as a default.
|
||||
app.secret_key = os.getenv("SECRET_KEY", "dev-secret-key")
|
||||
|
||||
# Reads the MongoDB connection string from an environment variable.
|
||||
# If none is provided, it uses the default Docker service connection.
|
||||
MONGO_URI = os.getenv("MONGO_URI", "mongodb://admin:admin123@mongo:27017/")
|
||||
|
||||
# Reads the MongoDB database name from the environment.
|
||||
# Defaults to "appdb".
|
||||
MONGO_DB_NAME = os.getenv("MONGO_DB_NAME", "appdb")
|
||||
|
||||
# Reads the MongoDB collection name from the environment.
|
||||
# Defaults to "products".
|
||||
MONGO_COLLECTION_NAME = os.getenv("MONGO_COLLECTION_NAME", "products")
|
||||
|
||||
# Reads the port number for the Flask app from the environment.
|
||||
# Defaults to 8080 and converts it into an integer.
|
||||
PORT = int(os.getenv("PORT", "8080"))
|
||||
|
||||
# These variables will be filled later after MongoDB is connected.
|
||||
# mongo_client will store the MongoClient object.
|
||||
# products_collection will store the products collection object.
|
||||
mongo_client = None
|
||||
products_collection = None
|
||||
|
||||
|
||||
# This function connects the application to MongoDB.
|
||||
# It retries multiple times because, in Docker, MongoDB may take a few
|
||||
# seconds to start after the Flask app container starts.
|
||||
def connect_to_mongo():
|
||||
# Tells Python that these names refer to the global variables above,
|
||||
# not new local variables inside the function.
|
||||
global mongo_client, products_collection
|
||||
|
||||
# Try up to 30 times to connect to MongoDB.
|
||||
for attempt in range(30):
|
||||
try:
|
||||
# Create the MongoDB client.
|
||||
# serverSelectionTimeoutMS=2000 means each failed attempt times out
|
||||
# after 2 seconds instead of hanging too long.
|
||||
mongo_client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=2000)
|
||||
|
||||
# Ping the database to confirm it is reachable.
|
||||
mongo_client.admin.command("ping")
|
||||
|
||||
# Select the database configured in MONGO_DB_NAME.
|
||||
db = mongo_client[MONGO_DB_NAME]
|
||||
|
||||
# Select the collection configured in MONGO_COLLECTION_NAME.
|
||||
products_collection = db[MONGO_COLLECTION_NAME]
|
||||
|
||||
# Create a unique index on product_id.
|
||||
# This prevents two products from having the same product_id.
|
||||
products_collection.create_index(
|
||||
[("product_id", ASCENDING)],
|
||||
unique=True
|
||||
)
|
||||
|
||||
# If connection and setup succeed, exit the function.
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
# If connection fails, print a message showing which retry attempt failed.
|
||||
print(f"MongoDB not ready yet (attempt {attempt + 1}/30): {e}")
|
||||
|
||||
# Wait 2 seconds before trying again.
|
||||
time.sleep(2)
|
||||
|
||||
# If all 30 attempts fail, stop the app with an error.
|
||||
raise RuntimeError("Could not connect to MongoDB after multiple attempts.")
|
||||
|
||||
|
||||
# Converts a form value into a float.
|
||||
# Used for fields like price and rating.
|
||||
def parse_float(value):
|
||||
# If value is None, replace it with an empty string, then strip spaces.
|
||||
value = (value or "").strip()
|
||||
|
||||
# If the field was left empty, return None instead of crashing.
|
||||
if value == "":
|
||||
return None
|
||||
|
||||
# Convert the value to a float.
|
||||
return float(value)
|
||||
|
||||
|
||||
# Converts a form value into an integer.
|
||||
# Used for product_id.
|
||||
def parse_int(value):
|
||||
# If value is None, replace it with an empty string, then strip spaces.
|
||||
value = (value or "").strip()
|
||||
|
||||
# If the field was left empty, return None.
|
||||
if value == "":
|
||||
return None
|
||||
|
||||
# Convert the value to an integer.
|
||||
return int(value)
|
||||
|
||||
|
||||
# Converts a comma-separated string of tags into a Python list.
|
||||
# Example: "gaming, pc, accessories" becomes ["gaming", "pc", "accessories"].
|
||||
def parse_tags(value):
|
||||
# Replace None with empty string and trim spaces.
|
||||
value = (value or "").strip()
|
||||
|
||||
# If the field is empty, return None.
|
||||
if not value:
|
||||
return None
|
||||
|
||||
# Split on commas, trim spaces around each tag, and discard empty tags.
|
||||
return [tag.strip() for tag in value.split(",") if tag.strip()]
|
||||
|
||||
|
||||
# Converts a raw MongoDB document into a cleaner dictionary for HTML templates.
|
||||
# This makes values easier to display and avoids issues with ObjectId or None values.
|
||||
def normalize_product(product):
|
||||
return {
|
||||
# Convert MongoDB's ObjectId into a normal string so it can be used in URLs.
|
||||
"_id": str(product.get("_id")),
|
||||
|
||||
# Read product_id, defaulting to empty string if missing.
|
||||
"product_id": product.get("product_id", ""),
|
||||
|
||||
# Read product name.
|
||||
"name": product.get("name", ""),
|
||||
|
||||
# Read category.
|
||||
"category": product.get("category", ""),
|
||||
|
||||
# If price is None, use empty string so the form field looks blank.
|
||||
# Otherwise keep the real price value.
|
||||
"price": "" if product.get("price") is None else product.get("price"),
|
||||
|
||||
# Make sure in_stock is always a real boolean.
|
||||
"in_stock": bool(product.get("in_stock", False)),
|
||||
|
||||
# If rating is None, use empty string for cleaner display.
|
||||
"rating": "" if product.get("rating") is None else product.get("rating"),
|
||||
|
||||
# Always return tags as a list.
|
||||
"tags": product.get("tags") or [],
|
||||
|
||||
# Also create a comma-separated version of tags for text input fields.
|
||||
"tags_text": ", ".join(product.get("tags") or []),
|
||||
|
||||
# Format created_at as a readable date-time string if it exists.
|
||||
"created_at": product.get("created_at").strftime("%Y-%m-%d %H:%M:%S")
|
||||
if product.get("created_at") else "",
|
||||
|
||||
# Read supplier name and country.
|
||||
"supplier_name": product.get("supplier_name", ""),
|
||||
"supplier_country": product.get("supplier_country", "")
|
||||
}
|
||||
|
||||
|
||||
# This route handles visits to the home page "/".
|
||||
# It loads all products from MongoDB and shows them in index.html.
|
||||
@app.route("/")
|
||||
def index():
|
||||
# Find all documents in the collection and sort them by product_id ascending.
|
||||
raw_products = list(products_collection.find().sort("product_id", 1))
|
||||
|
||||
# Normalize every MongoDB document before sending it to the template.
|
||||
products = [normalize_product(p) for p in raw_products]
|
||||
|
||||
# Render the HTML page and pass the products list into it.
|
||||
return render_template("index.html", products=products)
|
||||
|
||||
|
||||
# This route handles form submissions for creating a new product.
|
||||
# It only accepts POST requests.
|
||||
@app.route("/create", methods=["POST"])
|
||||
def create_product():
|
||||
try:
|
||||
# Build a new product dictionary from submitted form values.
|
||||
product = {
|
||||
# Convert product_id to integer.
|
||||
"product_id": parse_int(request.form.get("product_id")),
|
||||
|
||||
# Read the required name field and trim spaces.
|
||||
"name": request.form.get("name", "").strip(),
|
||||
|
||||
# Optional text fields become None if left blank.
|
||||
"category": request.form.get("category", "").strip() or None,
|
||||
|
||||
# Convert numeric fields.
|
||||
"price": parse_float(request.form.get("price")),
|
||||
|
||||
# Checkbox is "on" when checked.
|
||||
"in_stock": request.form.get("in_stock") == "on",
|
||||
|
||||
# Convert rating to float.
|
||||
"rating": parse_float(request.form.get("rating")),
|
||||
|
||||
# Convert tags from text to list.
|
||||
"tags": parse_tags(request.form.get("tags")),
|
||||
|
||||
# Store the current UTC time as the creation time.
|
||||
"created_at": datetime.utcnow(),
|
||||
|
||||
# Optional supplier fields.
|
||||
"supplier_name": request.form.get("supplier_name", "").strip() or None,
|
||||
"supplier_country": request.form.get("supplier_country", "").strip() or None
|
||||
}
|
||||
|
||||
# Validate that product_id was provided.
|
||||
if not product["product_id"]:
|
||||
flash("Product ID is required.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
# Validate that name was provided.
|
||||
if not product["name"]:
|
||||
flash("Name is required.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
# Insert the new product into MongoDB.
|
||||
products_collection.insert_one(product)
|
||||
|
||||
# Store a success message to show on the next page load.
|
||||
flash("Product created.", "success")
|
||||
|
||||
except DuplicateKeyError:
|
||||
# Happens when another product already has the same product_id.
|
||||
flash("That product_id already exists.", "error")
|
||||
|
||||
except ValueError:
|
||||
# Happens when user enters invalid numbers in numeric fields.
|
||||
flash("Invalid number entered for product_id, price, or rating.", "error")
|
||||
|
||||
except Exception as e:
|
||||
# Catch-all for any other error.
|
||||
flash(f"Create failed: {e}", "error")
|
||||
|
||||
# Return the user to the home page after processing.
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
# This route opens the edit page for a specific product.
|
||||
# The product's MongoDB _id is passed in the URL.
|
||||
@app.route("/edit/<product_id>")
|
||||
def edit_product(product_id):
|
||||
try:
|
||||
# Look up the product by its MongoDB ObjectId.
|
||||
product = products_collection.find_one({"_id": ObjectId(product_id)})
|
||||
|
||||
# If no product is found, show an error and go back home.
|
||||
if not product:
|
||||
flash("Product not found.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
# Render edit.html with the normalized product data.
|
||||
return render_template("edit.html", product=normalize_product(product))
|
||||
|
||||
except Exception:
|
||||
# Happens if product_id is not a valid ObjectId or something else fails.
|
||||
flash("Invalid product id.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
# This route handles updating an existing product.
|
||||
# It accepts POST data from the edit form.
|
||||
@app.route("/update/<product_id>", methods=["POST"])
|
||||
def update_product(product_id):
|
||||
try:
|
||||
# Build a dictionary of updated field values from the form.
|
||||
updated_product = {
|
||||
"name": request.form.get("name", "").strip(),
|
||||
"category": request.form.get("category", "").strip() or None,
|
||||
"price": parse_float(request.form.get("price")),
|
||||
"in_stock": request.form.get("in_stock") == "on",
|
||||
"rating": parse_float(request.form.get("rating")),
|
||||
"tags": parse_tags(request.form.get("tags")),
|
||||
"supplier_name": request.form.get("supplier_name", "").strip() or None,
|
||||
"supplier_country": request.form.get("supplier_country", "").strip() or None
|
||||
}
|
||||
|
||||
# Require the name field.
|
||||
if not updated_product["name"]:
|
||||
flash("Name is required.", "error")
|
||||
return redirect(url_for("edit_product", product_id=product_id))
|
||||
|
||||
# Update the matching document in MongoDB.
|
||||
# $set changes only the listed fields instead of replacing the whole document.
|
||||
result = products_collection.update_one(
|
||||
{"_id": ObjectId(product_id)},
|
||||
{"$set": updated_product}
|
||||
)
|
||||
|
||||
# If no document matched the given ID, show an error.
|
||||
if result.matched_count == 0:
|
||||
flash("Product not found.", "error")
|
||||
else:
|
||||
# Otherwise report success.
|
||||
flash("Product updated.", "success")
|
||||
|
||||
except ValueError:
|
||||
# Numeric parsing failed.
|
||||
flash("Invalid number entered for price or rating.", "error")
|
||||
|
||||
except Exception as e:
|
||||
# Any other problem during update.
|
||||
flash(f"Update failed: {e}", "error")
|
||||
|
||||
# Return to the main page after update.
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
# This route deletes a product.
|
||||
# It accepts POST requests so deletion is triggered by a form submission.
|
||||
@app.route("/delete/<product_id>", methods=["POST"])
|
||||
def delete_product(product_id):
|
||||
try:
|
||||
# Delete the document whose _id matches the URL parameter.
|
||||
result = products_collection.delete_one({"_id": ObjectId(product_id)})
|
||||
|
||||
# If nothing was deleted, the product was not found.
|
||||
if result.deleted_count == 0:
|
||||
flash("Product not found.", "error")
|
||||
else:
|
||||
# Otherwise show a success message.
|
||||
flash("Product deleted.", "success")
|
||||
|
||||
except Exception as e:
|
||||
# Handle invalid ObjectId or any other delete error.
|
||||
flash(f"Delete failed: {e}", "error")
|
||||
|
||||
# Return the user to the home page.
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
# This block only runs when this file is executed directly.
|
||||
# It will not run if the app is imported from another file.
|
||||
if __name__ == "__main__":
|
||||
# Connect to MongoDB before starting the web server.
|
||||
connect_to_mongo()
|
||||
|
||||
# Start the Flask development server.
|
||||
# host="0.0.0.0" makes it accessible from outside the container.
|
||||
# port=PORT uses the configured port.
|
||||
# debug=False disables Flask debug mode.
|
||||
app.run(host="0.0.0.0", port=PORT, debug=False)
|
||||
90
docker-compose.yml
Normal file
90
docker-compose.yml
Normal file
@ -0,0 +1,90 @@
|
||||
# Docker Compose file format version
|
||||
# 3.9 is a commonly used Compose file version
|
||||
# version: "3.9"
|
||||
|
||||
# Defines all containers (services) that belong to this application
|
||||
services:
|
||||
|
||||
# MongoDB database service
|
||||
mongo:
|
||||
# Uses the official MongoDB image, version 7
|
||||
image: mongo:7
|
||||
|
||||
# Gives the container a fixed name so it is easier to identify in Docker
|
||||
container_name: mongo-crud-db
|
||||
|
||||
# Automatically restarts the container if it stops because of a failure
|
||||
restart: on-failure
|
||||
|
||||
# Environment variables passed into the MongoDB container
|
||||
environment:
|
||||
# Creates the MongoDB root/admin username on first startup
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
|
||||
# Creates the MongoDB root/admin password on first startup
|
||||
MONGO_INITDB_ROOT_PASSWORD: admin123
|
||||
|
||||
# Connects this container to the custom Docker network below
|
||||
networks:
|
||||
- mongo-crud-net
|
||||
|
||||
# Mounts a named Docker volume into MongoDB's data folder
|
||||
# This keeps database data persistent even if the container is recreated
|
||||
volumes:
|
||||
- mongo_crud_data:/data/db
|
||||
|
||||
# Flask web application service
|
||||
web:
|
||||
# Builds the image from the Dockerfile in the current folder
|
||||
build: .
|
||||
|
||||
# Gives the web container a fixed name
|
||||
container_name: mongo-crud-web
|
||||
|
||||
# Automatically restarts the container if it fails
|
||||
restart: on-failure
|
||||
|
||||
# Tells Docker to start the mongo service before the web service
|
||||
# Note: this does not guarantee MongoDB is fully ready, only that it starts first
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
# Environment variables passed into the Flask application
|
||||
environment:
|
||||
# MongoDB connection string used by the Flask app
|
||||
# The hostname "mongo" works because it is the service name on the Docker network
|
||||
MONGO_URI: mongodb://admin:admin123@mongo:27017/
|
||||
|
||||
# MongoDB database name the app should use
|
||||
MONGO_DB_NAME: appdb
|
||||
|
||||
# MongoDB collection name the app should use
|
||||
MONGO_COLLECTION_NAME: products
|
||||
|
||||
# Flask secret key used for sessions and flash messages
|
||||
SECRET_KEY: mongo-crud-secret
|
||||
|
||||
# Port the Flask app listens on inside the container
|
||||
PORT: 8080
|
||||
|
||||
# Maps ports between the host machine and the container
|
||||
# Left side = host port, right side = container port
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
# Connects the web container to the same Docker network as MongoDB
|
||||
networks:
|
||||
- mongo-crud-net
|
||||
|
||||
# Defines custom Docker networks used by the application
|
||||
networks:
|
||||
mongo-crud-net:
|
||||
# Explicitly sets the network name instead of letting Docker generate one
|
||||
name: mongo-crud-net
|
||||
|
||||
# Defines named Docker volumes used by the application
|
||||
volumes:
|
||||
mongo_crud_data:
|
||||
# Explicitly sets the volume name
|
||||
# This volume stores MongoDB files persistently
|
||||
name: mongo_crud_data
|
||||
189
edit.html
Normal file
189
edit.html
Normal file
@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Declares this file as an HTML5 document -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Sets the character encoding so text displays correctly -->
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!-- Makes the page responsive on smaller screens -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Text shown in the browser tab -->
|
||||
<title>Edit Product</title>
|
||||
|
||||
<style>
|
||||
/* Makes width calculations include padding and borders */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* Styles the page background, spacing, and default font */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Centers the page content and limits its width */
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* White card-style box that holds the form */
|
||||
.box {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Centers the main page heading */
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Lays out the form fields with spacing between them */
|
||||
form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Makes labels bold so they are easier to read */
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Shared styling for text and number input fields */
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Adds a gray background to read-only fields */
|
||||
.readonly {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
/* Places checkbox and label side by side */
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Places the action buttons next to each other */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Shared styling for buttons and button-like links */
|
||||
button, a {
|
||||
display: inline-block;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Green button for saving changes */
|
||||
.btn-save { background: #28a745; color: white; }
|
||||
|
||||
/* Gray button/link for going back */
|
||||
.btn-back { background: #6c757d; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Main centered wrapper for the page -->
|
||||
<div class="container">
|
||||
|
||||
<!-- Page heading -->
|
||||
<h1>Edit Product</h1>
|
||||
|
||||
<!-- Card container holding the edit form -->
|
||||
<div class="box">
|
||||
|
||||
<!-- Form sends updated data to the Flask update route -->
|
||||
<!-- product._id is inserted by Jinja so Flask knows which product to update -->
|
||||
<form action="/update/{{ product._id }}" method="POST">
|
||||
|
||||
<!-- Read-only display of the product ID -->
|
||||
<div>
|
||||
<label>Product ID</label>
|
||||
|
||||
<!-- The user can see the product ID but cannot edit it -->
|
||||
<input type="number" value="{{ product.product_id }}" readonly class="readonly">
|
||||
</div>
|
||||
|
||||
<!-- Editable product name field -->
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input type="text" name="name" value="{{ product.name }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Editable category field -->
|
||||
<div>
|
||||
<label>Category</label>
|
||||
<input type="text" name="category" value="{{ product.category }}">
|
||||
</div>
|
||||
|
||||
<!-- Editable price field; step allows decimal values like 19.99 -->
|
||||
<div>
|
||||
<label>Price</label>
|
||||
<input type="number" step="0.01" name="price" value="{{ product.price }}">
|
||||
</div>
|
||||
|
||||
<!-- Editable rating field; step allows values like 4.5 -->
|
||||
<div>
|
||||
<label>Rating</label>
|
||||
<input type="number" step="0.1" name="rating" value="{{ product.rating }}">
|
||||
</div>
|
||||
|
||||
<!-- Editable tags field -->
|
||||
<!-- tags_text is already prepared in Python as comma-separated text -->
|
||||
<div>
|
||||
<label>Tags</label>
|
||||
<input type="text" name="tags" value="{{ product.tags_text }}">
|
||||
</div>
|
||||
|
||||
<!-- Editable supplier name field -->
|
||||
<div>
|
||||
<label>Supplier Name</label>
|
||||
<input type="text" name="supplier_name" value="{{ product.supplier_name }}">
|
||||
</div>
|
||||
|
||||
<!-- Editable supplier country field -->
|
||||
<div>
|
||||
<label>Supplier Country</label>
|
||||
<input type="text" name="supplier_country" value="{{ product.supplier_country }}">
|
||||
</div>
|
||||
|
||||
<!-- Checkbox for stock availability -->
|
||||
<div class="checkbox-row">
|
||||
|
||||
<!-- Jinja checks the box if product.in_stock is true -->
|
||||
<input type="checkbox" name="in_stock" id="in_stock" {% if product.in_stock %}checked{% endif %}>
|
||||
|
||||
<label for="in_stock">In Stock</label>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="actions">
|
||||
|
||||
<!-- Submits the form and saves the changes -->
|
||||
<button type="submit" class="btn-save">Save Changes</button>
|
||||
|
||||
<!-- Returns to the main page without saving -->
|
||||
<a href="/" class="btn-back">Back</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
316
index.html
Normal file
316
index.html
Normal file
@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Declares this file as an HTML5 document -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Sets the character encoding so text displays correctly -->
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!-- Makes the page responsive on phones and tablets -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- The title shown in the browser tab -->
|
||||
<title>MongoDB Products Manager</title>
|
||||
|
||||
<style>
|
||||
/* Makes width and height calculations include padding and border */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* Styles the whole page background, spacing, and default font */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Centers the main content and limits how wide it can grow */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Centers the page headings */
|
||||
h1, h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Reusable white card-style section box */
|
||||
.box {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Space below the flash message area */
|
||||
.messages {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Base styling for any flash message */
|
||||
.message {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Green styling for success messages */
|
||||
.message.success {
|
||||
background: #e8f7ec;
|
||||
color: #1e6b35;
|
||||
border: 1px solid #bfe5c8;
|
||||
}
|
||||
|
||||
/* Red styling for error messages */
|
||||
.message.error {
|
||||
background: #ffe8e8;
|
||||
color: #8a1f1f;
|
||||
border: 1px solid #e6bcbc;
|
||||
}
|
||||
|
||||
/* Two-column layout for the "Add Product" form */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Common styling for text and number inputs */
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Makes an element span the full width of the form grid */
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Aligns checkbox and label nicely on one line */
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Shared button styling for real buttons and link-buttons */
|
||||
button, .btn {
|
||||
display: inline-block;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Green create button */
|
||||
.btn-create { background: #28a745; color: white; }
|
||||
|
||||
/* Blue edit button */
|
||||
.btn-edit { background: #007bff; color: white; }
|
||||
|
||||
/* Red delete button */
|
||||
.btn-delete { background: #dc3545; color: white; }
|
||||
|
||||
/* Allows horizontal scrolling if the table is too wide */
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Makes the table fill the available width */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
/* Styles the table cells */
|
||||
th, td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Light background for table headers */
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Places edit and delete actions next to each other */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Message shown when there are no products */
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* On smaller screens, change the form to one column */
|
||||
@media (max-width: 800px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Main centered page wrapper -->
|
||||
<div class="container">
|
||||
<!-- Main title of the page -->
|
||||
<h1>MongoDB Products Manager</h1>
|
||||
|
||||
<!-- First card: flash messages and add-product form -->
|
||||
<div class="box">
|
||||
|
||||
<!-- Jinja block: gets flashed messages from Flask -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
|
||||
<!-- Only show the message area if there are messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
|
||||
<!-- Loop through each flashed message -->
|
||||
{% for category, message in messages %}
|
||||
|
||||
<!-- category becomes a CSS class like success or error -->
|
||||
<div class="message {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Section heading -->
|
||||
<h2>Add Product</h2>
|
||||
|
||||
<!-- Form for creating a new product -->
|
||||
<form action="/create" method="POST" class="form-grid">
|
||||
|
||||
<!-- Required numeric product ID -->
|
||||
<input type="number" name="product_id" placeholder="Product ID" required>
|
||||
|
||||
<!-- Required product name -->
|
||||
<input type="text" name="name" placeholder="Name" required>
|
||||
|
||||
<!-- Optional category -->
|
||||
<input type="text" name="category" placeholder="Category">
|
||||
|
||||
<!-- Optional price with decimal support -->
|
||||
<input type="number" step="0.01" name="price" placeholder="Price">
|
||||
|
||||
<!-- Optional rating with decimal support -->
|
||||
<input type="number" step="0.1" name="rating" placeholder="Rating">
|
||||
|
||||
<!-- Optional comma-separated tags -->
|
||||
<input type="text" name="tags" placeholder="Tags (comma separated)">
|
||||
|
||||
<!-- Optional supplier name -->
|
||||
<input type="text" name="supplier_name" placeholder="Supplier Name">
|
||||
|
||||
<!-- Optional supplier country -->
|
||||
<input type="text" name="supplier_country" placeholder="Supplier Country">
|
||||
|
||||
<!-- Checkbox row spanning full width -->
|
||||
<div class="checkbox-row full-width">
|
||||
<!-- Checked by default -->
|
||||
<input type="checkbox" name="in_stock" id="in_stock" checked>
|
||||
<label for="in_stock">In Stock</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit button spanning full width -->
|
||||
<div class="full-width">
|
||||
<button type="submit" class="btn-create">Create Product</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Second card: product table -->
|
||||
<div class="box">
|
||||
<h2>Products</h2>
|
||||
|
||||
<!-- Only show the table if there are products -->
|
||||
{% if products %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- Table headers -->
|
||||
<th>product_id</th>
|
||||
<th>name</th>
|
||||
<th>category</th>
|
||||
<th>price</th>
|
||||
<th>in_stock</th>
|
||||
<th>rating</th>
|
||||
<th>tags</th>
|
||||
<th>supplier_name</th>
|
||||
<th>supplier_country</th>
|
||||
<th>created_at</th>
|
||||
<th>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<!-- Loop through each product passed in from Flask -->
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<!-- Display each product field -->
|
||||
<td>{{ product.product_id }}</td>
|
||||
<td>{{ product.name }}</td>
|
||||
|
||||
<!-- Show empty string if category is missing -->
|
||||
<td>{{ product.category or '' }}</td>
|
||||
|
||||
<!-- Show empty string if price is blank -->
|
||||
<td>{{ product.price if product.price != '' else '' }}</td>
|
||||
|
||||
<td>{{ product.in_stock }}</td>
|
||||
|
||||
<!-- Show empty string if rating is blank -->
|
||||
<td>{{ product.rating if product.rating != '' else '' }}</td>
|
||||
|
||||
<!-- tags_text is already prepared in Python as a comma-separated string -->
|
||||
<td>{{ product.tags_text }}</td>
|
||||
|
||||
<!-- Show supplier data if present -->
|
||||
<td>{{ product.supplier_name or '' }}</td>
|
||||
<td>{{ product.supplier_country or '' }}</td>
|
||||
|
||||
<!-- Show formatted creation date -->
|
||||
<td>{{ product.created_at }}</td>
|
||||
|
||||
<td>
|
||||
<div class="actions">
|
||||
<!-- Link to edit page for this specific product -->
|
||||
<a href="/edit/{{ product._id }}" class="btn btn-edit">Edit</a>
|
||||
|
||||
<!-- Small form to delete this product -->
|
||||
<form action="/delete/{{ product._id }}" method="POST" style="margin:0;">
|
||||
<!-- JavaScript confirm prevents accidental deletion -->
|
||||
<button type="submit" class="btn-delete" onclick="return confirm('Delete this product?')">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- If there are no products, show a placeholder message -->
|
||||
{% else %}
|
||||
<div class="empty">No products found yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
50
prepare-app.sh
Normal file
50
prepare-app.sh
Normal file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uses the user's Bash interpreter to run this script
|
||||
|
||||
set -e
|
||||
# Stops the script immediately if any command fails
|
||||
|
||||
echo "Preparing application..."
|
||||
# Prints a message so the user knows the setup process has started
|
||||
|
||||
echo "Creating Docker network..."
|
||||
# Prints a message before checking or creating the Docker network
|
||||
|
||||
docker network inspect mongo-crud-net >/dev/null 2>&1 || docker network create mongo-crud-net
|
||||
# Tries to inspect the Docker network named "mongo-crud-net"
|
||||
# >/dev/null hides normal output
|
||||
# 2>&1 hides error output
|
||||
# If the network does not exist, the command after || runs and creates it
|
||||
|
||||
echo "Creating Docker volume..."
|
||||
# Prints a message before checking or creating the Docker volume
|
||||
|
||||
docker volume inspect mongo_crud_data >/dev/null 2>&1 || docker volume create mongo_crud_data
|
||||
# Tries to inspect the Docker volume named "mongo_crud_data"
|
||||
# If the volume does not exist, it creates it
|
||||
# This volume will be used to store MongoDB data persistently
|
||||
|
||||
echo "Building application images..."
|
||||
# Prints a message before building the Docker images
|
||||
|
||||
docker compose build
|
||||
# Builds the Docker images defined in docker-compose.yml
|
||||
# This usually builds the web app image and prepares everything needed to run the app
|
||||
|
||||
echo ""
|
||||
# Prints an empty line for cleaner terminal output
|
||||
|
||||
echo "Preparation complete."
|
||||
# Prints a message showing the setup finished successfully
|
||||
|
||||
echo "Created or verified:"
|
||||
# Prints a heading for the summary of what now exists
|
||||
|
||||
echo " - network: mongo-crud-net"
|
||||
# Shows the Docker network that was checked or created
|
||||
|
||||
echo " - volume: mongo_crud_data"
|
||||
# Shows the Docker volume that was checked or created
|
||||
|
||||
echo " - images: built via docker compose"
|
||||
# Shows that the Docker images were built from the Compose configuration
|
||||
10
remove-app.sh
Normal file
10
remove-app.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "Removing application containers, network, and volume..."
|
||||
|
||||
docker compose down -v --remove-orphans || true
|
||||
docker network rm mongo-crud-net >/dev/null 2>&1 || true
|
||||
docker volume rm mongo_crud_data >/dev/null 2>&1 || true
|
||||
|
||||
echo "Application removed completely."
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Flask==3.0.3
|
||||
pymongo==4.8.0
|
||||
33
start-app.sh
Normal file
33
start-app.sh
Normal file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uses the user's Bash interpreter to run this script
|
||||
|
||||
set -e
|
||||
# Stops the script immediately if any command fails
|
||||
|
||||
echo "Starting application..."
|
||||
# Prints a message so the user knows the startup process has begun
|
||||
|
||||
docker compose up -d
|
||||
# Starts the services defined in docker-compose.yml in detached mode
|
||||
# "detached mode" means the containers run in the background
|
||||
|
||||
echo ""
|
||||
# Prints an empty line for cleaner terminal output
|
||||
|
||||
echo "Application started."
|
||||
# Confirms that the application startup command completed successfully
|
||||
|
||||
echo "Open the website at:"
|
||||
# Prints a label for the website address
|
||||
|
||||
echo " http://localhost:8080"
|
||||
# Shows the browser address where the user can open the web application
|
||||
|
||||
echo ""
|
||||
# Prints another empty line for readability
|
||||
|
||||
echo "To follow logs, run:"
|
||||
# Prints a label for the log command
|
||||
|
||||
echo " docker compose logs -f"
|
||||
# Shows the command that displays live logs from the running containers
|
||||
19
stop-app.sh
Normal file
19
stop-app.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uses the user's Bash interpreter to run this script
|
||||
|
||||
set -e
|
||||
# Stops the script immediately if any command fails
|
||||
|
||||
echo "Stopping application services..."
|
||||
# Prints a message so the user knows the shutdown process has started
|
||||
|
||||
docker compose stop
|
||||
# Stops the running containers defined in docker-compose.yml
|
||||
# The containers are stopped but not deleted
|
||||
|
||||
echo "Application stopped."
|
||||
# Confirms that the application services were stopped successfully
|
||||
|
||||
echo "State is preserved because containers were stopped, not removed."
|
||||
# Explains that data and container state are still kept
|
||||
# because stop does not remove containers or volumes
|
||||
BIN
~$cker flask mongo website documentation.docx
Normal file
BIN
~$cker flask mongo website documentation.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user