Practice Completed
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Amirreza Panahi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def home():
|
||||||
|
data = {
|
||||||
|
"en_name": "Mohammad Amin Mirzaee",
|
||||||
|
"fa_name": "محمد امین میرزائی",
|
||||||
|
"stuNumber": "04121129705027",
|
||||||
|
}
|
||||||
|
return render_template("index.html", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
FEATURES = [
|
||||||
|
(
|
||||||
|
"render_template",
|
||||||
|
"EN: render_template is a function provided by Flask that renders an HTML template and returns it as a response to a client (browser). Flask uses a template engine called Jinja2. You write HTML files with placeholders or dynamic content, and render_template fills in those placeholders when the page is requested.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"render_template",
|
||||||
|
" فا: این تابع توسط فلسک ارائه شده و کد های اچ تی ام ال را که در پوشه تمپلیتس است رندر می کند، فلسک در واقع از انجین یا موتوری به نام جینجا2 استفاده می کند. شما می توانید فایل های اچ تی ام ال را به صورت مستقیم و یا پویا در فایل های پایتون به اچ تی ام ال وصل کنید و در خواست به صفحاتتون بفرستید",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/templates",
|
||||||
|
"EN: Store HTML files or templates that your Flask app will render. Flask automatically looks here when you call render_template.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/templates",
|
||||||
|
"فا: ذخیره فایل های اچ تی ام ال و قالب ها در برنامه فلسک خود که رندر خواهد کرد. فلسک اتوماتیک این پوشه را رصد می کند در صورتیکه تابع رندر_تمپلیتس فراخوانی شود",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/static",
|
||||||
|
"EN: Store static files like CSS, JavaScript, images, fonts — anything that doesn’t change dynamically. Flask serves files from this folder automatically.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/static",
|
||||||
|
"فا: تمامی فایل های استاتیک از جمله کد های سی اس اس و جاوا اسکریپت و عکس ها . فونت ها یا مدیا ها را ذخیره می کند که در حال تغییر به صورت پویا نیستند. فلسک این فایل ها را به صورت اتوماتیک برای شما تنظیم کرده و می توانید از مسیر آن استفاده کنید",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def homepage():
|
||||||
|
"""Render a simple HTML page that uses dynamic data."""
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
features=FEATURES,
|
||||||
|
current_time=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Flask==3.0.3
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const highlightButton = document.querySelector("[data-js='highlight']");
|
||||||
|
const timestampButton = document.querySelector("[data-js='timestamp']");
|
||||||
|
const container = document.querySelector("main");
|
||||||
|
const timestampTarget = document.querySelector("[data-js='timestamp-value']");
|
||||||
|
|
||||||
|
if (highlightButton) {
|
||||||
|
highlightButton.addEventListener("click", () => {
|
||||||
|
container.classList.toggle("highlighted");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestampButton) {
|
||||||
|
timestampButton.addEventListener("click", () => {
|
||||||
|
const now = new Date().toISOString().replace("T", " ").slice(0, 19) + "Z";
|
||||||
|
timestampTarget.textContent = now;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -0,0 +1,83 @@
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 2.5rem 1rem 4rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.15);
|
||||||
|
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.highlighted {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 20px 40px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.65rem 1.2rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>گزارش دوم – کار با HTML Templates در Flask</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='style.css') }}"
|
||||||
|
/>
|
||||||
|
<script src="{{ url_for('static', filename='app.js') }}" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>My Info</h1>
|
||||||
|
<p>Mohammad Amin Mirzaee - 04121129705027</p>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
What is <code>render_template</code> & What is the role of the
|
||||||
|
<code>/templates</code> and <code>/static</code> folders?
|
||||||
|
</h2>
|
||||||
|
<ul>
|
||||||
|
{% for title, description in features %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ title }}:</strong>
|
||||||
|
{{ description }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" data-js="highlight">Toggle highlight</button>
|
||||||
|
<button type="button" data-js="timestamp">Update timestamp</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The current (UTC) time was rendered by Flask:
|
||||||
|
<strong data-js="timestamp-value">
|
||||||
|
{{ current_time.strftime("%Y-%m-%d %H:%M:%S") }}Z
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Commands ( already in venv )
|
||||||
|
<ul>
|
||||||
|
<li><code>cd examples/html-basic</code> وارد پروژه می شویم</li>
|
||||||
|
<li>
|
||||||
|
<code>pip install -r requirements.txt</code> نصب پکیج های مورد نیاز
|
||||||
|
</li>
|
||||||
|
<li><code>python app.py</code> اجرای برنامه</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Before</h3>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('static', filename='img/before.png') }}"
|
||||||
|
alt="Before"
|
||||||
|
width="600"
|
||||||
|
/>
|
||||||
|
<h3>After</h3>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('static', filename='img/after.png') }}"
|
||||||
|
alt="After"
|
||||||
|
width="600"
|
||||||
|
/>
|
||||||
|
<h3>Hierarchy / Tree</h3>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('static', filename='img/tree.png') }}"
|
||||||
|
alt="After"
|
||||||
|
width="600"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Changes in <code>index.html</code> file: I changed the text of each
|
||||||
|
elements, also added some html elements/tag like img, p, h2, h3, ...
|
||||||
|
then using <code>/static</code> folder to put my images ( screenshots
|
||||||
|
) and using them in page with url_for function to find static
|
||||||
|
folder and filename ( using flask in order to import media ) I also
|
||||||
|
use ul and li tags to list commands I used for running the
|
||||||
|
application.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
تغییرات در فایل ایندکس اچ تی ام ال: من متن های هر تگی را تغییر دادم و
|
||||||
|
اضافه کردم یکسری تگ هایی مثل تگ آی ام جی، پی، تگ های اچ، .... و بعد از
|
||||||
|
پوشه استاتیک برای قرار دادن عکس ها و اسکرین شات ها با تابع مخصوص فلسک
|
||||||
|
برای پیدا کردن مسیر آن استفاده کردم در ادامه با تگ یو ال و ال آی لیستی
|
||||||
|
از فرمان های اجرای برنامه را قرار دادم
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# PostgreSQL connection string in the form postgresql://USER:PASS@HOST:PORT/DBNAME
|
||||||
|
DATABASE_URL=postgresql://me:root@localhost:5432/test
|
||||||
|
|
||||||
|
# Port for the Flask development server
|
||||||
|
PORT=5001
|
||||||
|
|
||||||
|
# Enable the debugger and auto-reload (true/false)
|
||||||
|
FLASK_DEBUG=true
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# PostgreSQL connection string in the form postgresql://USER:PASS@HOST:PORT/DBNAME
|
||||||
|
DATABASE_URL=postgresql://demo_user:demo_pass@localhost:5432/demo
|
||||||
|
|
||||||
|
# Port for the Flask development server
|
||||||
|
PORT=5001
|
||||||
|
|
||||||
|
# Enable the debugger and auto-reload (true/false)
|
||||||
|
FLASK_DEBUG=true
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
import os
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any, Mapping, Sequence
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2 import IntegrityError
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from flask import Flask, Response, jsonify, request
|
||||||
|
|
||||||
|
"""
|
||||||
|
Flask + PostgreSQL learning lab.
|
||||||
|
|
||||||
|
This single file now demonstrates every foundational database task you encounter
|
||||||
|
in CRUD apps:
|
||||||
|
|
||||||
|
- /init – create the table and helper indexes
|
||||||
|
- /reset – drop and recreate the schema (danger zone, but educational)
|
||||||
|
- /seed – insert sample rows with optional UPSERT behavior
|
||||||
|
- /health – verify the database connection
|
||||||
|
- /items – list items as JSON
|
||||||
|
- /items/<id> – read/update/delete individual rows (JSON payloads)
|
||||||
|
- /add – legacy query-param insert for quick URL experiments
|
||||||
|
- /search – filter by name/value plus pagination
|
||||||
|
- /stats – aggregate queries (min/max/avg)
|
||||||
|
- /schema – inspect the table definition using information_schema
|
||||||
|
- /list – tab-separated export for spreadsheets/CLI work
|
||||||
|
|
||||||
|
Connection details stay in environment variables so you can point the app at any
|
||||||
|
database without editing code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DATABASE_URL = os.getenv(
|
||||||
|
"DATABASE_URL",
|
||||||
|
"postgresql://postgres:root@localhost:5432/postgres",
|
||||||
|
)
|
||||||
|
PORT = int(os.getenv("PORT", "5000"))
|
||||||
|
DEBUG = os.getenv("FLASK_DEBUG", "false").lower() in {"1", "true", "yes"}
|
||||||
|
TRUE_VALUES = {"1", "true", "yes", "on"}
|
||||||
|
SELECT_COLUMNS = "id, name, value, note, created_at, updated_at"
|
||||||
|
|
||||||
|
SEED_ITEMS = [
|
||||||
|
{"name": "Desk", "value": 199.99, "note": "Spacious work surface"},
|
||||||
|
{"name": "Chair", "value": 89.5, "note": "Ergonomic and adjustable"},
|
||||||
|
{"name": "Lamp", "value": 35.0, "note": "LED task lighting"},
|
||||||
|
{"name": "Notebook", "value": 6.25, "note": "Grid-paper notebook"},
|
||||||
|
{"name": "Plant", "value": 18.75, "note": "Adds a splash of green"},
|
||||||
|
]
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_conn() -> psycopg2.extensions.connection:
|
||||||
|
"""Return a new psycopg2 connection using DATABASE_URL."""
|
||||||
|
return psycopg2.connect(DATABASE_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def create_items_table(drop_existing: bool = False) -> None:
|
||||||
|
"""(Re)create the demo table and the indexes we rely on."""
|
||||||
|
with get_conn() as conn, conn.cursor() as cur:
|
||||||
|
if drop_existing:
|
||||||
|
cur.execute("DROP TABLE IF EXISTS items")
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
value DOUBLE PRECISION NOT NULL CHECK (value >= 0),
|
||||||
|
note TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_items_value ON items (value)")
|
||||||
|
cur.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_items_created_at ON items (created_at)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _floatify(value: Any) -> float | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
return float(value)
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Convert psycopg2 rows into JSON-friendly data."""
|
||||||
|
return {
|
||||||
|
"id": row["id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"value": _floatify(row.get("value")),
|
||||||
|
"note": row.get("note") or "",
|
||||||
|
"created_at": row.get("created_at").isoformat()
|
||||||
|
if row.get("created_at")
|
||||||
|
else None,
|
||||||
|
"updated_at": row.get("updated_at").isoformat()
|
||||||
|
if row.get("updated_at")
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_select(sql: str, params: Sequence[Any] | None = None) -> list[dict[str, Any]]:
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(sql, params or ())
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return [_serialize_item(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _select_one(sql: str, params: Sequence[Any] | None = None) -> dict[str, Any] | None:
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(sql, params or ())
|
||||||
|
row = cur.fetchone()
|
||||||
|
return _serialize_item(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_name(raw: Any) -> str:
|
||||||
|
name = str(raw or "").strip()
|
||||||
|
if not name:
|
||||||
|
raise ValueError("name is required")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_value(raw: Any) -> float:
|
||||||
|
try:
|
||||||
|
value = float(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError("value must be a number")
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("value must be >= 0")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_note(raw: Any) -> str:
|
||||||
|
return str(raw or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_create_payload(data: Mapping[str, Any]) -> tuple[str, float, str]:
|
||||||
|
return (
|
||||||
|
_coerce_name(data.get("name")),
|
||||||
|
_coerce_value(data.get("value")),
|
||||||
|
_coerce_note(data.get("note")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_partial_payload(data: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
fields: dict[str, Any] = {}
|
||||||
|
if "name" in data:
|
||||||
|
fields["name"] = _coerce_name(data.get("name"))
|
||||||
|
if "value" in data:
|
||||||
|
fields["value"] = _coerce_value(data.get("value"))
|
||||||
|
if "note" in data:
|
||||||
|
fields["note"] = _coerce_note(data.get("note"))
|
||||||
|
if not fields:
|
||||||
|
raise ValueError("provide at least one of name, value, or note")
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_item(name: str, value: float, note: str) -> dict[str, Any]:
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO items (name, value, note)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
RETURNING {SELECT_COLUMNS}
|
||||||
|
""",
|
||||||
|
(name, value, note),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return _serialize_item(row)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_item(item_id: int, fields: Mapping[str, Any]) -> dict[str, Any] | None:
|
||||||
|
if not fields:
|
||||||
|
return None
|
||||||
|
assignments = ", ".join(f"{column} = %s" for column in fields)
|
||||||
|
params = [*fields.values(), item_id]
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE items
|
||||||
|
SET {assignments}, updated_at = NOW()
|
||||||
|
WHERE id = %s
|
||||||
|
RETURNING {SELECT_COLUMNS}
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return _serialize_item(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_item(item_id: int) -> bool:
|
||||||
|
with get_conn() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("DELETE FROM items WHERE id = %s", (item_id,))
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _as_bool(value: Any) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
return str(value or "").strip().lower() in TRUE_VALUES
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_limit(raw: Any, default: int = 20) -> int:
|
||||||
|
try:
|
||||||
|
limit = int(raw) if raw is not None else default
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
limit = default
|
||||||
|
return max(1, min(100, limit))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health_check():
|
||||||
|
"""Simple connection test that also returns the database server time."""
|
||||||
|
with get_conn() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT NOW()")
|
||||||
|
(current_time,) = cur.fetchone()
|
||||||
|
return jsonify({"status": "ok", "database_time": current_time.isoformat()})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/init")
|
||||||
|
def init_db():
|
||||||
|
"""Create the items table if it does not yet exist."""
|
||||||
|
create_items_table(drop_existing=False)
|
||||||
|
return "ok: table ready\n"
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/reset")
|
||||||
|
def reset_db():
|
||||||
|
"""Drop the table and recreate it from scratch."""
|
||||||
|
create_items_table(drop_existing=True)
|
||||||
|
return jsonify({"status": "ok", "message": "table dropped and recreated"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/seed")
|
||||||
|
def seed_data():
|
||||||
|
"""
|
||||||
|
Insert a batch of sample rows.
|
||||||
|
|
||||||
|
Accepts JSON:
|
||||||
|
{
|
||||||
|
"replace": true, # optional, truncate before inserting
|
||||||
|
"items": [
|
||||||
|
{"name": "...", "value": 1.23, "note": "..."},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = request.get_json(silent=True) or {}
|
||||||
|
replace = _as_bool(payload.get("replace") or request.args.get("replace"))
|
||||||
|
dataset = payload.get("items") or SEED_ITEMS
|
||||||
|
if not isinstance(dataset, list):
|
||||||
|
return jsonify({"error": "items must be a JSON list"}), 400
|
||||||
|
|
||||||
|
parsed_rows: list[tuple[str, float, str]] = []
|
||||||
|
for entry in dataset:
|
||||||
|
if not isinstance(entry, Mapping):
|
||||||
|
return jsonify({"error": "each item must be an object"}), 400
|
||||||
|
try:
|
||||||
|
parsed_rows.append(_parse_create_payload(entry))
|
||||||
|
except ValueError as exc:
|
||||||
|
return jsonify({"error": f"invalid item: {exc}"}), 400
|
||||||
|
|
||||||
|
if not parsed_rows:
|
||||||
|
return jsonify({"error": "no items to insert"}), 400
|
||||||
|
|
||||||
|
create_items_table(drop_existing=False)
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
if replace:
|
||||||
|
cur.execute("TRUNCATE TABLE items RESTART IDENTITY")
|
||||||
|
cur.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO items (name, value, note)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
ON CONFLICT (name) DO UPDATE
|
||||||
|
SET value = EXCLUDED.value,
|
||||||
|
note = EXCLUDED.note,
|
||||||
|
updated_at = NOW()
|
||||||
|
""",
|
||||||
|
parsed_rows,
|
||||||
|
)
|
||||||
|
cur.execute("SELECT COUNT(*) AS total FROM items")
|
||||||
|
total = cur.fetchone()["total"]
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"processed": len(parsed_rows),
|
||||||
|
"replace": replace,
|
||||||
|
"total_rows": total,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items")
|
||||||
|
def list_items_json():
|
||||||
|
"""Return all rows as JSON."""
|
||||||
|
rows = _run_select(f"SELECT {SELECT_COLUMNS} FROM items ORDER BY created_at")
|
||||||
|
return jsonify({"count": len(rows), "items": rows})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/items")
|
||||||
|
def create_item():
|
||||||
|
"""Create a row from a JSON payload."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
name, value, note = _parse_create_payload(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 400
|
||||||
|
try:
|
||||||
|
item = _insert_item(name, value, note)
|
||||||
|
except IntegrityError:
|
||||||
|
return jsonify({"error": "name must be unique"}), 409
|
||||||
|
return jsonify(item), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/<int:item_id>")
|
||||||
|
def get_item(item_id: int):
|
||||||
|
"""Fetch a single row by ID."""
|
||||||
|
row = _select_one(
|
||||||
|
f"SELECT {SELECT_COLUMNS} FROM items WHERE id = %s", (item_id,)
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return jsonify({"error": "item not found"}), 404
|
||||||
|
return jsonify(row)
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/items/<int:item_id>")
|
||||||
|
def replace_item(item_id: int):
|
||||||
|
"""Replace an existing row (requires name + value)."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
name, value, note = _parse_create_payload(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 400
|
||||||
|
try:
|
||||||
|
row = _update_item(item_id, {"name": name, "value": value, "note": note})
|
||||||
|
except IntegrityError:
|
||||||
|
return jsonify({"error": "name must be unique"}), 409
|
||||||
|
if not row:
|
||||||
|
return jsonify({"error": "item not found"}), 404
|
||||||
|
return jsonify(row)
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/items/<int:item_id>")
|
||||||
|
def update_item(item_id: int):
|
||||||
|
"""Partially update a row."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
fields = _parse_partial_payload(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 400
|
||||||
|
try:
|
||||||
|
row = _update_item(item_id, fields)
|
||||||
|
except IntegrityError:
|
||||||
|
return jsonify({"error": "name must be unique"}), 409
|
||||||
|
if not row:
|
||||||
|
return jsonify({"error": "item not found"}), 404
|
||||||
|
return jsonify(row)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/items/<int:item_id>")
|
||||||
|
def delete_item(item_id: int):
|
||||||
|
"""Remove a row permanently."""
|
||||||
|
if not _delete_item(item_id):
|
||||||
|
return jsonify({"error": "item not found"}), 404
|
||||||
|
return jsonify({"status": "ok", "deleted_id": item_id})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/search")
|
||||||
|
def search_items():
|
||||||
|
"""Filter by partial name and/or numeric ranges."""
|
||||||
|
name_query = (request.args.get("q") or "").strip()
|
||||||
|
min_value_raw = request.args.get("min_value")
|
||||||
|
max_value_raw = request.args.get("max_value")
|
||||||
|
limit = _parse_limit(request.args.get("limit"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
min_value = _coerce_value(min_value_raw) if min_value_raw else None
|
||||||
|
max_value = _coerce_value(max_value_raw) if max_value_raw else None
|
||||||
|
except ValueError as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 400
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
params: list[Any] = []
|
||||||
|
if name_query:
|
||||||
|
conditions.append("name ILIKE %s")
|
||||||
|
params.append(f"%{name_query}%")
|
||||||
|
if min_value is not None:
|
||||||
|
conditions.append("value >= %s")
|
||||||
|
params.append(min_value)
|
||||||
|
if max_value is not None:
|
||||||
|
conditions.append("value <= %s")
|
||||||
|
params.append(max_value)
|
||||||
|
|
||||||
|
where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||||
|
params.append(limit)
|
||||||
|
|
||||||
|
sql = f"""
|
||||||
|
SELECT {SELECT_COLUMNS}
|
||||||
|
FROM items
|
||||||
|
{where_clause}
|
||||||
|
ORDER BY value DESC
|
||||||
|
LIMIT %s
|
||||||
|
"""
|
||||||
|
rows = _run_select(sql, params)
|
||||||
|
return jsonify({"returned": len(rows), "items": rows})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/stats")
|
||||||
|
def stats():
|
||||||
|
"""Aggregate information about the table."""
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total_rows,
|
||||||
|
COALESCE(SUM(value), 0) AS total_value,
|
||||||
|
COALESCE(MIN(value), 0) AS min_value,
|
||||||
|
COALESCE(MAX(value), 0) AS max_value,
|
||||||
|
COALESCE(AVG(value), 0) AS avg_value
|
||||||
|
FROM items
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"total_rows": row["total_rows"],
|
||||||
|
"total_value": _floatify(row["total_value"]),
|
||||||
|
"min_value": _floatify(row["min_value"]),
|
||||||
|
"max_value": _floatify(row["max_value"]),
|
||||||
|
"avg_value": _floatify(row["avg_value"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/schema")
|
||||||
|
def describe_schema():
|
||||||
|
"""Inspect the table definition via information_schema."""
|
||||||
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'items'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
columns = cur.fetchall()
|
||||||
|
return jsonify({"columns": columns})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/add")
|
||||||
|
def add_item():
|
||||||
|
"""Insert a new row using query parameters (e.g. ?name=foo&value=42)."""
|
||||||
|
data = {
|
||||||
|
"name": request.args.get("name"),
|
||||||
|
"value": request.args.get("value"),
|
||||||
|
"note": request.args.get("note", ""),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
name, value, note = _parse_create_payload(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
return f"error: {exc}\n", 400
|
||||||
|
try:
|
||||||
|
item = _insert_item(name, value, note)
|
||||||
|
except IntegrityError:
|
||||||
|
return "error: name must be unique\n", 409
|
||||||
|
return f"ok: inserted id={item['id']}\n"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/list")
|
||||||
|
def list_items_text():
|
||||||
|
"""Return the stored items in a tab-separated plain-text response."""
|
||||||
|
with get_conn() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT id, name, value, note FROM items ORDER BY id")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
lines = ["id\tname\tvalue\tnote"] + [
|
||||||
|
f"{row[0]}\t{row[1]}\t{row[2]:.2f}\t{row[3]}" for row in rows
|
||||||
|
]
|
||||||
|
return Response("\n".join(lines) + "\n", mimetype="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="127.0.0.1", port=PORT, debug=DEBUG)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
Flask
|
||||||
|
psycopg2-binary
|
||||||
|
python-dotenv
|
||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
Flask==3.0.3
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
|
@ -0,0 +1,58 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>گزارش اول – برنامه Hello World با Flask</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='css/style.css') }}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body class="p-5">
|
||||||
|
<div class="card p-4 shadow">
|
||||||
|
<h1 class="mb-3 text-primary">Hello World, Welcome!</h1>
|
||||||
|
|
||||||
|
<p class="fs-4">My name is <strong>{{ data.en_name }}</strong>.</p>
|
||||||
|
<p class="fs-4">Student Number: {{ data.stuNumber }}</p>
|
||||||
|
<p class="fs-4">
|
||||||
|
نام و نام خانوادگی من <strong>{{ data.fa_name }}</strong> است
|
||||||
|
</p>
|
||||||
|
<p class="fs-4">شماره دانشجویی: {{ data.stuNumber }}</p>
|
||||||
|
<h6>
|
||||||
|
Flask is a web framework for Python that allows you to build websites,
|
||||||
|
web apps, and APIs easily.
|
||||||
|
</h6>
|
||||||
|
<h6>
|
||||||
|
فلسک یک فریم ورک یا چهارچوب وب است که برای پایتون است و به شما اجازه می
|
||||||
|
دهد تا وبسایت ها و وب اپ ها و ای پی آی ها را به راحتی بسازید
|
||||||
|
</h6>
|
||||||
|
<h3>Commands / دستورات</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>python get-pip.py</code> برای نصب پایپ</li>
|
||||||
|
<li><code>python -m venv venv</code> اجرا محیط مجازی و ایجاد پوشه آن</li>
|
||||||
|
<li><code>cd venv/Scripts</code> رفتن به مسیر پوشه و اکتیو کردن محیط</li>
|
||||||
|
<li><code>activate</code> با خط فرمان ویندوز</li>
|
||||||
|
<li><code>cd ../../</code> (Welcome-to-flask) بازگشت به مسیر پروژه</li>
|
||||||
|
<li><code>pip install -r requirements.txt</code> نصب برنامه های مورد نیاز</li>
|
||||||
|
<li><code>python app.py</code> اجرای برنامه با محیط</li>
|
||||||
|
<li><code>deactivate</code> در صورت بستن یا دی اکتیو کردن</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h5>Terminal Logs / گزارشات ترمینال</h5>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('static', filename='img/terminal.png') }}"
|
||||||
|
alt="Terminal"
|
||||||
|
width="800"
|
||||||
|
/>
|
||||||
|
<h5>Browser / مرورگر</h5>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('static', filename='img/browser.png') }}"
|
||||||
|
alt="Browser"
|
||||||
|
width="1200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||