Первый коммит

This commit is contained in:
greyjoy
2026-07-31 00:49:40 +04:00
parent 618d5b3a65
commit b53aa305e8
20 changed files with 753 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+22
View File
@@ -0,0 +1,22 @@
from django.contrib import admin
from .models import ConsultationRequest
@admin.register(ConsultationRequest)
class ConsultationRequestAdmin(admin.ModelAdmin):
list_display = (
"name",
"subject",
"exam",
"preferred_date",
"preferred_time",
"phone",
"is_processed",
)
list_filter = ("subject", "exam", "is_processed", "preferred_date")
search_fields = ("name", "phone", "contact", "comment")
list_editable = ("is_processed",)
readonly_fields = ("created_at",)
date_hierarchy = "preferred_date"
+8
View File
@@ -0,0 +1,8 @@
from django.apps import AppConfig
class ConsultationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "consultations"
verbose_name = "Консультации"
+47
View File
@@ -0,0 +1,47 @@
from django import forms
from django.utils import timezone
from .models import ConsultationRequest
class ConsultationRequestForm(forms.ModelForm):
class Meta:
model = ConsultationRequest
fields = (
"name",
"phone",
"contact",
"subject",
"exam",
"preferred_date",
"preferred_time",
"comment",
)
widgets = {
"name": forms.TextInput(attrs={"placeholder": "Как к вам обращаться"}),
"phone": forms.TelInput(attrs={"placeholder": "+7 (999) 123-45-67"}),
"contact": forms.TextInput(attrs={"placeholder": "@username, WhatsApp и т. п."}),
"preferred_date": forms.DateInput(attrs={"type": "date"}),
"preferred_time": forms.TimeInput(attrs={"type": "time"}),
"comment": forms.Textarea(
attrs={
"placeholder": "Например: текущий класс, темы, которые вызывают сложности",
"rows": 4,
}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs["class"] = "form-control"
self.fields["preferred_date"].widget.attrs["min"] = timezone.localdate().isoformat()
self.fields["contact"].required = False
self.fields["comment"].required = False
def clean_preferred_date(self):
preferred_date = self.cleaned_data["preferred_date"]
if preferred_date < timezone.localdate():
raise forms.ValidationError("Выберите сегодняшнюю дату или более позднюю.")
return preferred_date
+86
View File
@@ -0,0 +1,86 @@
# Generated by Django 5.0 on 2026-07-30
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="ConsultationRequest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, verbose_name="Имя ученика")),
(
"phone",
models.CharField(
max_length=30,
validators=[
django.core.validators.RegexValidator(
message="Введите телефон в привычном формате.",
regex="^[0-9+()\\-\\s]{7,30}$",
)
],
verbose_name="Телефон",
),
),
(
"contact",
models.CharField(
blank=True,
max_length=100,
verbose_name="Telegram или другой способ связи",
),
),
(
"subject",
models.CharField(
choices=[("physics", "Физика"), ("mathematics", "Математика")],
max_length=20,
verbose_name="Предмет",
),
),
(
"exam",
models.CharField(
choices=[
("oge", "ОГЭ"),
("ege", "ЕГЭ"),
("other", "Другая консультация"),
],
max_length=20,
verbose_name="Цель подготовки",
),
),
("preferred_date", models.DateField(verbose_name="Желаемая дата")),
("preferred_time", models.TimeField(verbose_name="Желаемое время")),
(
"comment",
models.TextField(blank=True, max_length=1000, verbose_name="Комментарий"),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Создана")),
(
"is_processed",
models.BooleanField(default=False, verbose_name="Заявка обработана"),
),
],
options={
"verbose_name": "Заявка на консультацию",
"verbose_name_plural": "Заявки на консультации",
"ordering": ("is_processed", "preferred_date", "preferred_time"),
},
),
]
+1
View File
@@ -0,0 +1 @@
+42
View File
@@ -0,0 +1,42 @@
from django.core.validators import RegexValidator
from django.db import models
class ConsultationRequest(models.Model):
class Subject(models.TextChoices):
PHYSICS = "physics", "Физика"
MATHEMATICS = "mathematics", "Математика"
class Exam(models.TextChoices):
OGE = "oge", "ОГЭ"
EGE = "ege", "ЕГЭ"
OTHER = "other", "Другая консультация"
name = models.CharField("Имя ученика", max_length=100)
phone = models.CharField(
"Телефон",
max_length=30,
validators=[
RegexValidator(
regex=r"^[0-9+()\-\s]{7,30}$",
message="Введите телефон в привычном формате.",
)
],
)
contact = models.CharField("Telegram или другой способ связи", max_length=100, blank=True)
subject = models.CharField("Предмет", max_length=20, choices=Subject.choices)
exam = models.CharField("Цель подготовки", max_length=20, choices=Exam.choices)
preferred_date = models.DateField("Желаемая дата")
preferred_time = models.TimeField("Желаемое время")
comment = models.TextField("Комментарий", max_length=1000, blank=True)
created_at = models.DateTimeField("Создана", auto_now_add=True)
is_processed = models.BooleanField("Заявка обработана", default=False)
class Meta:
verbose_name = "Заявка на консультацию"
verbose_name_plural = "Заявки на консультации"
ordering = ("is_processed", "preferred_date", "preferred_time")
def __str__(self):
return f"{self.name}: {self.get_subject_display()}{self.preferred_date:%d.%m.%Y}"
@@ -0,0 +1,194 @@
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Подготовка к ОГЭ и ЕГЭ по физике и математике с преподавателем Игорем Олеговичем Безрукавовым.">
<title>Физика и математика — Игорь Олегович Безрукавов</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Source+Serif+4:opsz,wght@8..60,600;8..60,700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<header class="site-header">
<div class="container navigation">
<a class="brand" href="#top" aria-label="На главную">ИО<span>·</span>учёба</a>
<nav class="nav-links" aria-label="Основная навигация">
<a href="#about">О преподавателе</a>
<a href="#subjects">Направления</a>
<a href="#booking">Запись</a>
</nav>
<a class="nav-button" href="#booking">Записаться</a>
</div>
</header>
<main id="top">
<section class="hero">
<div class="container hero-grid">
<div class="hero-content">
<p class="eyebrow">Физика и математика · 5–11 классы</p>
<h1>Готовимся к экзаменам <em>с пониманием</em>, а не наугад.</h1>
<p class="hero-text">Индивидуальные консультации по подготовке к ОГЭ и ЕГЭ: разбираем сложные темы, выстраиваем логику решения и спокойно идём к цели.</p>
<div class="hero-actions">
<a class="button button-primary" href="#booking">Записаться на консультацию</a>
<a class="text-link" href="#about">Узнать обо мне <span aria-hidden="true"></span></a>
</div>
<div class="hero-stats" aria-label="Ключевые факты">
<div><strong>10+</strong><span>лет опыта</span></div>
<div><strong>2</strong><span>предмета</span></div>
<div><strong>ОГЭ · ЕГЭ</strong><span>подготовка к экзаменам</span></div>
</div>
</div>
<div class="formula-card" aria-label="Физика и математика">
<div class="card-orbit orbit-one"></div>
<div class="card-orbit orbit-two"></div>
<span class="formula formula-e">E = mc²</span>
<span class="formula formula-root">√x</span>
<span class="formula formula-sum">Σ</span>
<div class="formula-center">
<p>точные науки</p>
<strong>понятно<br>и спокойно</strong>
</div>
</div>
</div>
</section>
<section class="section about" id="about">
<div class="container split-layout">
<p class="section-kicker">О преподавателе</p>
<div>
<h2>Игорь Олегович<br>Безрукавов</h2>
<p class="lead">Преподаватель физики и математики с опытом преподавания и репетиторства более 10 лет.</p>
<p>Окончил Астраханский государственный университет: бакалавриат и магистратуру по специальности «Преподавание физики и математики».</p>
<p>На консультации важно не просто получить ответ, а разобраться, почему решение работает — тогда знания остаются с учеником и на контрольной, и на экзамене.</p>
</div>
</div>
</section>
<section class="section subjects" id="subjects">
<div class="container">
<div class="section-heading">
<div>
<p class="section-kicker">Направления</p>
<h2>Подготовка в нужном темпе</h2>
</div>
<p>Подберём фокус консультации под текущий уровень, цели и дату экзамена.</p>
</div>
<div class="subject-grid">
<article class="subject-card physics-card">
<span class="subject-icon">F</span>
<p class="card-kicker">Предмет</p>
<h3>Физика</h3>
<ul>
<li>Механика, термодинамика, электродинамика</li>
<li>Задачи с формулами и графиками</li>
<li>Подготовка к ОГЭ и ЕГЭ</li>
</ul>
<a href="#booking">Записаться <span aria-hidden="true"></span></a>
</article>
<article class="subject-card math-card">
<span class="subject-icon">π</span>
<p class="card-kicker">Предмет</p>
<h3>Математика</h3>
<ul>
<li>Алгебра, геометрия, вероятность</li>
<li>Уравнения, неравенства, задачи</li>
<li>Подготовка к ОГЭ и ЕГЭ</li>
</ul>
<a href="#booking">Записаться <span aria-hidden="true"></span></a>
</article>
</div>
</div>
</section>
<section class="section process">
<div class="container">
<p class="section-kicker">Как это работает</p>
<h2>Три простых шага</h2>
<div class="steps">
<article><span>01</span><h3>Оставьте заявку</h3><p>Выберите предмет, экзамен и удобное время.</p></article>
<article><span>02</span><h3>Подтвердим время</h3><p>Игорь Олегович свяжется с вами по указанным контактам.</p></article>
<article><span>03</span><h3>Начнём подготовку</h3><p>Определим цель и разберём первые задачи.</p></article>
</div>
</div>
</section>
<section class="booking" id="booking">
<div class="container booking-layout">
<div class="booking-copy">
<p class="section-kicker">Запись на консультацию</p>
<h2>Сделайте первый шаг к уверенному экзамену.</h2>
<p>Заполните форму — время будет окончательно подтверждено после связи с преподавателем.</p>
<p class="privacy-note">Контакты используются только для ответа на вашу заявку.</p>
</div>
<div class="form-card">
{% if messages %}
{% for message in messages %}
<div class="message message-{{ message.tags }}" role="status">{{ message }}</div>
{% endfor %}
{% endif %}
{% if form.non_field_errors %}
<div class="form-errors">{{ form.non_field_errors }}</div>
{% endif %}
<form method="post" action="#booking" novalidate>
{% csrf_token %}
<div class="form-grid">
<div class="field full-width">
<label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>
{{ form.name }}
{% for error in form.name.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field">
<label for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label>
{{ form.phone }}
{% for error in form.phone.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field">
<label for="{{ form.contact.id_for_label }}">{{ form.contact.label }}</label>
{{ form.contact }}
{% for error in form.contact.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field">
<label for="{{ form.subject.id_for_label }}">{{ form.subject.label }}</label>
{{ form.subject }}
{% for error in form.subject.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field">
<label for="{{ form.exam.id_for_label }}">{{ form.exam.label }}</label>
{{ form.exam }}
{% for error in form.exam.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field">
<label for="{{ form.preferred_date.id_for_label }}">{{ form.preferred_date.label }}</label>
{{ form.preferred_date }}
{% for error in form.preferred_date.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field">
<label for="{{ form.preferred_time.id_for_label }}">{{ form.preferred_time.label }}</label>
{{ form.preferred_time }}
{% for error in form.preferred_time.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
<div class="field full-width">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }}</label>
{{ form.comment }}
{% for error in form.comment.errors %}<span class="error">{{ error }}</span>{% endfor %}
</div>
</div>
<button class="button button-primary submit-button" type="submit">Отправить заявку</button>
</form>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-content">
<a class="brand" href="#top">ИО<span>·</span>учёба</a>
<p>Физика и математика · подготовка к ОГЭ и ЕГЭ</p>
<p>© {% now "Y" %} Игорь Олегович Безрукавов</p>
</div>
</footer>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = "consultations"
urlpatterns = [
path("", views.home, name="home"),
]
+21
View File
@@ -0,0 +1,21 @@
from django.contrib import messages
from django.shortcuts import redirect, render
from django.urls import reverse
from .forms import ConsultationRequestForm
def home(request):
if request.method == "POST":
form = ConsultationRequestForm(request.POST)
if form.is_valid():
form.save()
messages.success(
request,
"Заявка отправлена. Игорь Олегович свяжется с вами, чтобы подтвердить время консультации.",
)
return redirect(f"{reverse('consultations:home')}#booking")
else:
form = ConsultationRequestForm()
return render(request, "consultations/home.html", {"form": form})