Первый коммит
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ConsultationsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "consultations"
|
||||
verbose_name = "Консультации"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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&family=Source+Serif+4:opsz,wght@8..60,600;8..60,700&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>
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = "consultations"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.home, name="home"),
|
||||
]
|
||||
|
||||
@@ -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})
|
||||
Reference in New Issue
Block a user