Created
April 21, 2026 09:50
-
-
Save sunmeat/b4db9064dcd4db3a64e7980c7cbbc18e to your computer and use it in GitHub Desktop.
CRUD операції. веб інтерфейс
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # app / views.py: | |
| from datetime import datetime | |
| from django.shortcuts import render | |
| from django.http import HttpRequest | |
| from app.models import Author | |
| def home(request): | |
| message = None # повідомлення про успіх/помилку | |
| # додавання автора | |
| if request.method == 'POST' and request.POST.get('action') == 'create': | |
| name = request.POST.get('name', '').strip() | |
| birth_year = request.POST.get('birth_year', '').strip() | |
| rating = request.POST.get('rating', '').strip() | |
| if name: | |
| Author.objects.create( # створюємо нового автора | |
| name=name, | |
| birth_year=int(birth_year) if birth_year else None, | |
| rating=float(rating) if rating else None, | |
| ) | |
| message = f'Автора "{name}" додано!' | |
| else: | |
| message = 'Помилка: введіть ім\'я автора.' | |
| # оновлення автора | |
| elif request.method == 'POST' and request.POST.get('action') == 'update': | |
| author_id = request.POST.get('author_id') | |
| name = request.POST.get('name', '').strip() | |
| birth_year = request.POST.get('birth_year', '').strip() | |
| rating = request.POST.get('rating', '').strip() | |
| try: | |
| author = Author.objects.get(id=author_id) | |
| author.name = name | |
| author.birth_year = int(birth_year) if birth_year else None | |
| author.rating = float(rating) if rating else None | |
| author.save() | |
| message = f'Автора "{author.name}" оновлено!' | |
| except Author.DoesNotExist: | |
| message = 'Автора не знайдено.' | |
| # видалити автора | |
| elif request.method == 'POST' and request.POST.get('action') == 'delete': | |
| author_id = request.POST.get('author_id') | |
| try: | |
| author = Author.objects.get(id=author_id) | |
| name = author.name | |
| author.delete() | |
| message = f'Автора "{name}" видалено.' | |
| except Author.DoesNotExist: | |
| message = 'Автора не знайдено.' | |
| # отримання списку всіх авторів для відображення | |
| authors = Author.objects.all().order_by('id') | |
| return render(request, 'app/index.html', { | |
| 'title': 'Автори', | |
| 'year': datetime.now().year, | |
| 'authors': authors, | |
| 'message': message, | |
| }) | |
| ################################################################### | |
| def contact(request): | |
| assert isinstance(request, HttpRequest) | |
| return render(request, 'app/contact.html', { | |
| 'title': 'Contact', | |
| 'message': 'Your contact page.', | |
| 'year': datetime.now().year, | |
| }) | |
| def about(request): | |
| assert isinstance(request, HttpRequest) | |
| return render(request, 'app/about.html', { | |
| 'title': 'About', | |
| 'message': 'Your application description page.', | |
| 'year': datetime.now().year, | |
| }) | |
| ===================================================================================================================== | |
| app / templates / app / index.html: | |
| {% extends "app/layout.html" %} | |
| {% block content %} | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Geologica:wght@300;400;500;600&family=Unbounded:wght@700&display=swap'); | |
| :root { | |
| --bg: #0d0d0f; | |
| --surface: #16161a; | |
| --card: #1e1e24; | |
| --border: #2a2a32; | |
| --accent: #6c63ff; | |
| --accent2: #a78bfa; | |
| --green: #34d399; | |
| --red: #f87171; | |
| --text: #e8e8f0; | |
| --muted: #6b6b80; | |
| --radius: 10px; | |
| } | |
| .authors-page * { | |
| box-sizing: border-box; | |
| } | |
| .authors-page { | |
| font-family: 'Geologica', sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| padding: 40px 24px 80px; | |
| } | |
| .page-title { | |
| font-family: 'Unbounded', sans-serif; | |
| font-size: clamp(22px, 4vw, 36px); | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| margin-bottom: 6px; | |
| background: linear-gradient(135deg, var(--accent2), var(--accent)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .page-sub { | |
| color: var(--muted); | |
| font-size: 14px; | |
| margin-bottom: 36px; | |
| font-weight: 300; | |
| } | |
| .flash { | |
| padding: 12px 18px; | |
| border-radius: var(--radius); | |
| margin-bottom: 28px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| border-left: 3px solid var(--green); | |
| background: rgba(52, 211, 153, 0.08); | |
| color: var(--green); | |
| } | |
| .add-card { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 28px 28px 24px; | |
| margin-bottom: 36px; | |
| } | |
| .card-label { | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.12em; | |
| color: var(--accent2); | |
| margin-bottom: 18px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .card-label::after { | |
| content: ''; | |
| flex: 1; | |
| height: 1px; | |
| background: var(--border); | |
| } | |
| .add-form-row { | |
| display: grid; | |
| grid-template-columns: 2fr 1fr 1fr auto; | |
| gap: 12px; | |
| align-items: end; | |
| } | |
| @media (max-width: 640px) { | |
| .add-form-row { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .field-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .field-group label { | |
| font-size: 11px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| color: var(--muted); | |
| } | |
| .field-group input { | |
| background: var(--surface); | |
| border: 1.5px solid var(--border); | |
| border-radius: 7px; | |
| padding: 10px 14px; | |
| font-family: 'Geologica', sans-serif; | |
| font-size: 14px; | |
| color: var(--text); | |
| outline: none; | |
| transition: border-color 0.2s; | |
| width: 100%; | |
| } | |
| .field-group input:focus { | |
| border-color: var(--accent); | |
| } | |
| .field-group input::placeholder { | |
| color: var(--muted); | |
| } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 7px; | |
| font-family: 'Geologica', sans-serif; | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: opacity 0.15s, transform 0.15s; | |
| white-space: nowrap; | |
| } | |
| .btn:hover { | |
| opacity: 0.85; | |
| transform: translateY(-1px); | |
| } | |
| .btn:active { | |
| transform: translateY(0); | |
| } | |
| .btn-add { | |
| background: var(--accent); | |
| color: #fff; | |
| padding: 10px 24px; | |
| align-self: flex-end; | |
| } | |
| .btn-save { | |
| background: var(--surface); | |
| color: var(--accent2); | |
| border: 1px solid var(--accent); | |
| font-size: 12px; | |
| padding: 7px 14px; | |
| } | |
| .btn-delete { | |
| background: transparent; | |
| color: var(--red); | |
| border: 1px solid rgba(248,113,113,0.35); | |
| font-size: 12px; | |
| padding: 7px 14px; | |
| } | |
| .btn-delete:hover { | |
| background: rgba(248,113,113,0.1); | |
| opacity: 1; | |
| } | |
| .table-card { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| } | |
| .table-header { | |
| padding: 18px 24px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .table-header-title { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text); | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| } | |
| .count-badge { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| padding: 3px 12px; | |
| font-size: 12px; | |
| font-family: monospace; | |
| color: var(--accent2); | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| thead th { | |
| padding: 12px 20px; | |
| text-align: left; | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.15em; | |
| color: var(--muted); | |
| background: var(--surface); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| tbody tr { | |
| border-bottom: 1px solid var(--border); | |
| transition: background 0.15s; | |
| } | |
| tbody tr:last-child { | |
| border-bottom: none; | |
| } | |
| tbody tr:hover { | |
| background: rgba(108, 99, 255, 0.04); | |
| } | |
| td { | |
| padding: 14px 20px; | |
| font-size: 14px; | |
| vertical-align: middle; | |
| } | |
| .id-cell { | |
| font-family: monospace; | |
| font-size: 12px; | |
| color: var(--muted); | |
| width: 50px; | |
| } | |
| .name-cell { | |
| font-weight: 500; | |
| } | |
| .num-cell { | |
| font-family: monospace; | |
| color: var(--muted); | |
| } | |
| .rating-cell { | |
| font-family: monospace; | |
| font-weight: 600; | |
| color: var(--accent2); | |
| } | |
| .actions-cell { | |
| width: 1%; | |
| white-space: nowrap; | |
| } | |
| .edit-form { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .edit-form input[type="text"], | |
| .edit-form input[type="number"] { | |
| background: var(--surface); | |
| border: 1.5px solid var(--border); | |
| border-radius: 6px; | |
| padding: 7px 10px; | |
| font-family: 'Geologica', sans-serif; | |
| font-size: 13px; | |
| color: var(--text); | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| .edit-form input[type="text"] { | |
| width: 160px; | |
| } | |
| .edit-form input[type="number"] { | |
| width: 80px; | |
| } | |
| .edit-form input:focus { | |
| border-color: var(--accent); | |
| } | |
| .empty-state { | |
| padding: 64px 24px; | |
| text-align: center; | |
| color: var(--muted); | |
| } | |
| .empty-state .es-icon { | |
| font-size: 40px; | |
| margin-bottom: 12px; | |
| } | |
| .empty-state p { | |
| font-size: 14px; | |
| } | |
| .authors-page { | |
| margin-left: calc(-50vw + 50%); | |
| width: 100vw; | |
| } | |
| </style> | |
| <div class="authors-page"> | |
| <div class="page-title">Автори</div> | |
| {% if message %} | |
| <div class="flash">✓ {{ message }}</div> | |
| {% endif %} | |
| <!-- форма додавання автора --> | |
| <div class="add-card"> | |
| <div class="card-label">Додати нового автора</div> | |
| <form method="POST"> | |
| {% csrf_token %} | |
| <input type="hidden" name="action" value="create"> | |
| <div class="add-form-row"> | |
| <div class="field-group"> | |
| <label>Імʼя *</label> | |
| <input type="text" name="name" placeholder="Тарас Шевченко" required> | |
| </div> | |
| <div class="field-group"> | |
| <label>Рік народження</label> | |
| <input type="number" name="birth_year" placeholder="1814"> | |
| </div> | |
| <div class="field-group"> | |
| <label>Рейтинг (0–10)</label> | |
| <input type="number" name="rating" step="0.1" min="0" max="10" placeholder="9.8"> | |
| </div> | |
| <button type="submit" class="btn btn-add">+ Додати</button> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- таблиця авторів --> | |
| <div class="table-card"> | |
| <div class="table-header"> | |
| <span class="table-header-title">Список авторів</span> | |
| <span class="count-badge">{{ authors|length }}</span> | |
| </div> | |
| {% if authors %} | |
| <table> | |
| <tbody> | |
| {% for author in authors %} | |
| <tr> | |
| <td class="id-cell">#{{ author.id }}</td> | |
| <td class="name-cell" colspan="3"> | |
| <form method="POST" class="edit-form"> | |
| {% csrf_token %} | |
| <input type="hidden" name="action" value="update"> | |
| <input type="hidden" name="author_id" value="{{ author.id }}"> | |
| <input type="text" name="name" value="{{ author.name }}" placeholder="Імʼя" required> | |
| <input type="number" name="birth_year" value="{{ author.birth_year|default:'' }}" placeholder="Рік"> | |
| <input type="number" name="rating" value="{{ author.rating|default:'' }}" placeholder="0.0" step="0.1" min="0" max="10"> | |
| <button type="submit" class="btn btn-save">Оновити дані</button> | |
| </form> | |
| </td> | |
| <td class="actions-cell"> | |
| <form method="POST" onsubmit="return confirm('Видалити {{ author.name }}?')"> | |
| {% csrf_token %} | |
| <input type="hidden" name="action" value="delete"> | |
| <input type="hidden" name="author_id" value="{{ author.id }}"> | |
| <button type="submit" class="btn btn-delete">Видалити</button> | |
| </form> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| {% else %} | |
| <div class="empty-state"> | |
| <p>Авторів поки ще немає. Додайте першого!</p> | |
| </div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| {% endblock %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment