Upload files to "/"

This commit is contained in:
Danylo Cherednychenko 2026-03-30 10:27:37 +00:00
parent bfd6e68ac0
commit 9fdc21ca7d
13 changed files with 1120 additions and 0 deletions

Binary file not shown.

15
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
Flask==3.0.3
pymongo==4.8.0

33
start-app.sh Normal file
View 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
View 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

Binary file not shown.