lab4 kiri
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, flash
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///accounting.db'
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
app.config['SECRET_KEY'] = "lab4_secret_key"
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- МОДЕЛЬ ----------------
|
||||||
|
|
||||||
|
class Employee(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
fullname = db.Column(db.String(120), nullable=False)
|
||||||
|
salary = db.Column(db.Integer, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Employee {self.id}>'
|
||||||
|
|
||||||
|
|
||||||
|
# ----------- ВАЛИДАЦИЯ БЭКЕНДА ------------
|
||||||
|
|
||||||
|
def validate_employee(fullname: str, salary: str):
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
fullname = fullname.strip()
|
||||||
|
|
||||||
|
if not fullname:
|
||||||
|
errors.append("ФИО не может быть пустым")
|
||||||
|
elif len(fullname) < 5 or len(fullname) > 120:
|
||||||
|
errors.append("ФИО должно быть от 5 до 120 символов")
|
||||||
|
elif not re.fullmatch(r"[A-Za-zА-Яа-яЁё\s\-\']+", fullname):
|
||||||
|
errors.append("ФИО может содержать только буквы, пробелы, дефисы и апострофы")
|
||||||
|
|
||||||
|
if not salary:
|
||||||
|
errors.append("Зарплата обязательна")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
salary = int(salary)
|
||||||
|
if salary <= 0 or salary > 10_000_000:
|
||||||
|
errors.append("Зарплата должна быть от 1 до 10 000 000")
|
||||||
|
except:
|
||||||
|
errors.append("Зарплата должна быть числом")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- РОУТЫ ----------------
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/employees")
|
||||||
|
def employees():
|
||||||
|
items = Employee.query.order_by(Employee.created_at.desc()).all()
|
||||||
|
total_salary = sum(e.salary for e in items)
|
||||||
|
return render_template("employees.html", employees=items, total_salary=total_salary)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/create", methods=["GET", "POST"])
|
||||||
|
def create_employee():
|
||||||
|
if request.method == "POST":
|
||||||
|
fullname = request.form.get("fullname", "")
|
||||||
|
salary = request.form.get("salary", "")
|
||||||
|
|
||||||
|
errors = validate_employee(fullname, salary)
|
||||||
|
if errors:
|
||||||
|
for e in errors:
|
||||||
|
flash(e, "error")
|
||||||
|
return render_template("create_employee.html", fullname=fullname, salary=salary)
|
||||||
|
|
||||||
|
employee = Employee(fullname=fullname.strip(), salary=int(salary))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(employee)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("employees"))
|
||||||
|
except:
|
||||||
|
return render_template("error.html", msg="Ошибка добавления")
|
||||||
|
|
||||||
|
return render_template("create_employee.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/employees/<int:id>")
|
||||||
|
def employee_detail(id):
|
||||||
|
emp = Employee.query.get_or_404(id)
|
||||||
|
return render_template("employee_detail.html", emp=emp)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/employees/<int:id>/delete")
|
||||||
|
def employee_delete(id):
|
||||||
|
emp = Employee.query.get_or_404(id)
|
||||||
|
try:
|
||||||
|
db.session.delete(emp)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("employees"))
|
||||||
|
except:
|
||||||
|
return render_template("error.html", msg="Ошибка удаления")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/employees/<int:id>/update", methods=["GET", "POST"])
|
||||||
|
def employee_update(id):
|
||||||
|
emp = Employee.query.get_or_404(id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
fullname = request.form.get("fullname", "")
|
||||||
|
salary = request.form.get("salary", "")
|
||||||
|
|
||||||
|
errors = validate_employee(fullname, salary)
|
||||||
|
if errors:
|
||||||
|
for e in errors:
|
||||||
|
flash(e, "error")
|
||||||
|
return render_template("employee_update.html", emp=emp)
|
||||||
|
|
||||||
|
emp.fullname = fullname.strip()
|
||||||
|
emp.salary = int(salary)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("employees"))
|
||||||
|
except:
|
||||||
|
return render_template("error.html", msg="Ошибка обновления")
|
||||||
|
|
||||||
|
return render_template("employee_update.html", emp=emp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
app.run(debug=True)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
Flask-SQLAlchemy==3.1.1
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
echo "🔧 Lab4 Application Setup & Launch"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# Check and create venv if not exists
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "📦 Virtual environment not found. Creating venv..."
|
||||||
|
python3 -m venv venv
|
||||||
|
echo "✅ venv created"
|
||||||
|
else
|
||||||
|
echo "✅ venv already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate venv
|
||||||
|
echo "🔄 Activating virtual environment..."
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install/upgrade requirements
|
||||||
|
if [ -f "requirements.txt" ]; then
|
||||||
|
echo "📥 Installing dependencies from requirements.txt..."
|
||||||
|
pip install -q --upgrade pip
|
||||||
|
pip install -q -r requirements.txt
|
||||||
|
echo "✅ Dependencies installed"
|
||||||
|
else
|
||||||
|
echo "⚠️ requirements.txt not found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create instance directory if not exists
|
||||||
|
if [ ! -d "instance" ]; then
|
||||||
|
echo "📁 Creating instance directory..."
|
||||||
|
mkdir -p instance
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove old database to create fresh one
|
||||||
|
if [ -f "instance/accounting.db" ]; then
|
||||||
|
echo "🗑️ Removing old database..."
|
||||||
|
rm instance/accounting.db
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "🚀 Starting Flask Application..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the Flask app
|
||||||
|
python app.py
|
||||||
|
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: linear-gradient(120deg,#e6f4ff,#f5fbff);
|
||||||
|
color:#1b2b34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(90deg, #084b83, #0d6ba3);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header nav a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header nav a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border-top: 1px solid #b5d9ff;
|
||||||
|
margin-top: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
background: #cce7ff;
|
||||||
|
padding: 15px;
|
||||||
|
display:flex;
|
||||||
|
gap:20px;
|
||||||
|
justify-content:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
text-decoration:none;
|
||||||
|
font-weight:bold;
|
||||||
|
color:#084b83;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h2,h3 { word-wrap: break-word; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background:white;
|
||||||
|
padding:20px;
|
||||||
|
margin:20px 0;
|
||||||
|
border-radius:12px;
|
||||||
|
box-shadow:0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #084b83;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group small {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
padding:12px;
|
||||||
|
border-radius:8px;
|
||||||
|
border:1px solid #b5d9ff;
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .btn {
|
||||||
|
background:#4da6ff;
|
||||||
|
color:white;
|
||||||
|
padding:10px 15px;
|
||||||
|
border-radius:8px;
|
||||||
|
text-decoration:none;
|
||||||
|
border:none;
|
||||||
|
cursor:pointer;
|
||||||
|
display:inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover, .btn:hover {
|
||||||
|
background: #3a8ee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #4da6ff 0%, #2e7fd6 100%);
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #3a8ee6 0%, #1f5db8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger { background:#ff6b6b; }
|
||||||
|
|
||||||
|
.btn.danger:hover { background: #ff5252; }
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ff8787 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.3);
|
||||||
|
animation: slideDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-window {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ff8787 100%);
|
||||||
|
border: 3px solid #d63031;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
|
||||||
|
animation: slideDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-header {
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item {
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Бухгалтерия{% endblock %}</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<div class="container header-inner">
|
||||||
|
<div class="logo">💼 Бухгалтерия</div>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Главная</a>
|
||||||
|
<a href="/employees">Сотрудники</a>
|
||||||
|
<a href="/create">Добавить</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="error-window">
|
||||||
|
<div class="error-header">⚠️ Ошибки валидации</div>
|
||||||
|
<div class="error-list">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="error-item">• {{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
Flask + SQLite • Лабораторная №4
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="card">
|
||||||
|
<h1>Добавить сотрудника</h1>
|
||||||
|
|
||||||
|
<form method="post" class="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fullname">ФИО (полное имя и фамилия)</label>
|
||||||
|
<input type="text" id="fullname" name="fullname"
|
||||||
|
placeholder="Введите ФИО"
|
||||||
|
value="{{ fullname or '' }}">
|
||||||
|
<small>Минимум 5 символов, поддерживаются дефисы и апострофы</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="salary">Зарплата (в рублях)</label>
|
||||||
|
<input type="number" id="salary" name="salary"
|
||||||
|
placeholder="Введите сумму"
|
||||||
|
value="{{ salary or '' }}">
|
||||||
|
<small>От 1 до 10 000 000 рублей</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-primary">Добавить сотрудника</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="card">
|
||||||
|
<h1>{{ emp.fullname }}</h1>
|
||||||
|
<p>Зарплата: {{ emp.salary }} ₽</p>
|
||||||
|
<p>Добавлен: {{ emp.created_at.date() }}</p>
|
||||||
|
|
||||||
|
<a class="btn" href="/employees/{{ emp.id }}/update">Редактировать</a>
|
||||||
|
<a class="btn danger" href="/employees/{{ emp.id }}/delete">Удалить</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="card">
|
||||||
|
<h1>Редактирование сотрудника</h1>
|
||||||
|
|
||||||
|
<form method="post" class="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fullname">ФИО (полное имя и фамилия)</label>
|
||||||
|
<input type="text" id="fullname" name="fullname"
|
||||||
|
value="{{ emp.fullname }}">
|
||||||
|
<small>Минимум 5 символов, поддерживаются дефисы и апострофы</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="salary">Зарплата (в рублях)</label>
|
||||||
|
<input type="number" id="salary" name="salary"
|
||||||
|
value="{{ emp.salary }}">
|
||||||
|
<small>От 1 до 10 000 000 рублей</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-primary">Обновить сотрудника</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Сотрудники{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Список сотрудников</h1>
|
||||||
|
|
||||||
|
<h3>Общий фонд зарплат: {{ total_salary }} ₽</h3>
|
||||||
|
|
||||||
|
{% if employees %}
|
||||||
|
{% for e in employees %}
|
||||||
|
<div class="card">
|
||||||
|
<h2>{{ e.fullname }}</h2>
|
||||||
|
<p>Зарплата: <b>{{ e.salary }} ₽</b></p>
|
||||||
|
<a class="btn" href="/employees/{{ e.id }}">Подробнее</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>Сотрудников пока нет</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>Ошибка</h1>
|
||||||
|
<p>{{ msg }}</p>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Добро пожаловать в систему бухгалтерии</h1>
|
||||||
|
<p>Используйте меню для навигации по приложению.</p>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user