# Внешний API АИС «Школа»

Этот гайд поможет подключить сайт, личный кабинет или фоновую синхронизацию к
АИС «Школа». Здесь есть короткий путь до первого рабочего запроса, примеры и
полный справочник полей. Веб-версия с навигацией и поиском доступна по адресу
`https://ais-school.ru/api-guide/`.

> Если вы впервые работаете с API, начните с раздела «Быстрый старт». Для уже
> работающей интеграции переходите к «Ресурсам и CRUD».

## Содержание

1. [Быстрый старт](#1-быстрый-старт)
2. [Авторизация](#2-авторизация)
3. [Ресурсы и CRUD](#3-ресурсы-и-crud)
4. [Отчёты](#4-отчёты)
5. [Ошибки](#5-ошибки)
6. [Безопасность](#6-безопасность)
7. [Опросы и отзывы](#опросы-и-отзывы)

## Перед началом

Базовый адрес API:

```text
https://ais-school.ru/api/v1/
```

Это официальный базовый адрес API АИС «Школа». Запросы и ответы используют JSON:

```http
Accept: application/json
Content-Type: application/json
```

`id` — целое число. Даты имеют вид `2026-09-12`, время — `14:30:00`, а
дата-время передаётся в ISO 8601, например `2026-09-12T14:30:00+10:00`.

Чтобы получить доступ к API, напишите на `director@ais-school.ru`. В письме
кратко опишите интеграцию, нужные школы и укажите точные HTTPS-адреса возврата
(`redirect_uris`). После согласования вы получите реквизиты программы и права
на нужные школы:

- `can_read` — просмотр данных;
- `can_write` — создание, изменение и удаление.

Программа не получает доступ к школе автоматически. Если грант для школы не
выдан, API вернёт `403`.

## 1. Быстрый старт

### Шаг 1. Выберите сценарий

| Что вы создаёте | Способ входа | Какие адреса школы использовать |
|---|---|---|
| Сайт или приложение, где входит человек | Authorization Code + PKCE | `/api/v1/school/...` |
| Серверную синхронизацию без пользователя | Client Credentials | `/api/v1/schools/{school_id}/...` |

Для пользовательского входа не храните общий `school_id`: API сам определит
школу по вошедшему профилю. Для фоновой интеграции `school_id` указывается в URL,
а доступ всё равно проверяется по гранту программы.

### Шаг 2. Получите токен

Для быстрого серверного теста запросите токен программы:

```bash
curl -X POST "https://ais-school.ru/api/v1/auth/token/" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "ais_...",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'
```

Ответ `200`:

```json
{
  "access_token": "длинная-случайная-строка",
  "token_type": "Bearer",
  "expires_in": 3600,
  "program": {"id": 7, "name": "Сайт школы"},
  "user_id": null,
  "school_id": null
}
```

Сохраните `access_token` на сервере интеграции и передавайте его в заголовке:

```http
Authorization: Bearer <access_token>
```

### Шаг 3. Сделайте первый запрос

Серверная интеграция может получить карточку разрешённой школы:

```bash
curl "https://ais-school.ru/api/v1/schools/10/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"
```

Приложение с пользовательским OAuth-токеном обращается к текущей школе:

```bash
curl "https://ais-school.ru/api/v1/school/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"
```

### Шаг 4. Посмотрите доступные данные

Не нужно угадывать имена ресурсов — запросите каталог:

```bash
curl "https://ais-school.ru/api/v1/school/resources/" \
  -H "Authorization: Bearer $TOKEN"
```

После этого, например, список предметов доступен по адресу:

```text
GET /api/v1/school/resources/subjects/
```

## 2. Авторизация

### Вход пользователя: Authorization Code + PKCE

Этот сценарий подходит для внешнего кабинета или приложения. Пользователь
вводит пароль только на странице АИС «Школа» — внешнее приложение пароль не
видит и не передаёт в API.

#### 1. Подготовьте защитные значения

Создайте случайные `state` и `code_verifier`. Из `code_verifier` вычислите
SHA-256 и закодируйте результат как Base64URL без символов `=` — это
`code_challenge`.

- `state` защищает возврат пользователя от подмены;
- `code_verifier` доказывает, что код обменивает то же приложение, которое
  начало вход;
- оба значения нужно временно сохранить в серверной сессии пользователя.

#### 2. Перенаправьте пользователя в АИС «Школа»

```text
GET https://ais-school.ru/api/v1/auth/authorize/?client_id=ais_...&redirect_uri=https%3A%2F%2Fclient.example%2Fcallback&response_type=code&scope=school.read&state=RANDOM_STATE&code_challenge=CHALLENGE&code_challenge_method=S256
```

`redirect_uri` должен полностью совпасть с одним из адресов, согласованных при
подключении: учитываются протокол, домен, путь и завершающий `/`.

Допустимые scope:

- `school.read` — запрос доступа на чтение;
- `school.write` — запрос доступа на изменение (для программы также нужен
  грант `can_write`).

После входа и подтверждения доступа браузер вернётся в приложение:

```text
https://client.example/callback?code=ONE_TIME_CODE&state=RANDOM_STATE
```

#### 3. Проверьте state и обменяйте код

Сначала сравните возвращённый `state` со значением из сессии. При несовпадении
немедленно остановите вход. Затем с сервера приложения выполните:

```bash
curl -X POST "https://ais-school.ru/api/v1/auth/token/" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "ais_...",
    "redirect_uri": "https://client.example/callback",
    "code": "ONE_TIME_CODE",
    "code_verifier": "ORIGINAL_RANDOM_VERIFIER"
  }'
```

Одноразовый код живёт 5 минут и после успешного обмена больше не принимается.
Токен по умолчанию действует 3600 секунд; срок настраивается через
`EXTERNAL_TOKEN_TTL`.

В ответе для пользовательского токена заполнены `user_id` и `school_id`:

```json
{
  "access_token": "длинная-случайная-строка",
  "token_type": "Bearer",
  "expires_in": 3600,
  "program": {"id": 7, "name": "Личный кабинет"},
  "user_id": 42,
  "school_id": 10
}
```

> Передача логина и пароля в `/auth/token/` отключена. Если отправить другой
> `grant_type`, API вернёт ошибку `password_grant_disabled`.

### Фоновая интеграция: Client Credentials

Этот сценарий подходит для сервера, планировщика или обмена данными без входа
человека. Используйте `grant_type=client_credentials`, `client_id` и
`client_secret`, как в быстром старте. Такой токен не связан с пользователем и
работает только со школами, разрешёнными грантами программы.

`client_secret` показывается при регистрации программы. Храните его в секретах
окружения, а не в JavaScript, мобильном приложении или репозитории.

### Проверка и отзыв токена

| Метод и адрес | Для чего нужен |
|---|---|
| `GET /api/v1/auth/me/` | Проверить программу, пользователя и срок токена |
| `GET /api/v1/auth/profile/` | Получить безопасный профиль вошедшего пользователя |
| `POST /api/v1/auth/revoke/` | Немедленно отозвать текущий токен |

Также поддерживается заголовок `Authorization: Token ...`, но для новых
интеграций рекомендуется стандартная схема `Bearer`.

### Профиль пользователя

`GET /api/v1/auth/profile/` не принимает параметры или тело:

```bash
curl "https://ais-school.ru/api/v1/auth/profile/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"
```

Ответ `200`:

```json
{
  "id": 42,
  "username": "ivanov",
  "first_name": "Иван",
  "last_name": "Иванов",
  "email": "ivanov@school.ru",
  "phone": "+7 900 000-00-00",
  "position": "Учитель математики",
  "role": "teacher",
  "school_id": 10,
  "birth_date": "1985-04-12",
  "gender": "M"
}
```

`birth_date` и `gender` могут быть `null` или пустыми. Ответ намеренно не
содержит пароль, хэш пароля, СНИЛС, документ, `is_staff`, `is_superuser` или
`blocked`.

У токена `client_credentials` нет профиля. Для него этот endpoint вернёт `404`:

```json
{"detail": "Токен не связан с профилем пользователя."}
```

### Адреса школы: какой вариант выбрать

| Операция | OAuth пользователя | Серверная интеграция |
|---|---|---|
| Карточка школы | `/api/v1/school/` | `/api/v1/schools/{school_id}/` |
| Полный снимок | `/api/v1/school/all/` | `/api/v1/schools/{school_id}/all/` |
| Каталог ресурсов | `/api/v1/school/resources/` | `/api/v1/schools/{school_id}/resources/` |
| Ресурс | `/api/v1/school/resources/{resource}/` | `/api/v1/schools/{school_id}/resources/{resource}/` |

OAuth-токен не может прочитать или изменить другую школу, даже если приложению
известен её `id`.

### Карточка школы

`GET` возвращает реквизиты, адреса, контакты, сведения о лицензии и
аккредитации, тариф, текущий учебный год и даты подписки. `PATCH` изменяет только
переданные поля и требует `can_write=true`.

```bash
curl -X PATCH "https://ais-school.ru/api/v1/school/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"short_name": "Новая школа", "phone": "+7 900 000-00-00"}'
```

### Полный снимок

`GET /api/v1/school/all/` возвращает карточку школы и весь связанный граф:
справочники, учеников, сотрудников, расписание, оценки, посещаемость, домашние
задания, документы, платежи и обращения.

```json
{
  "school": {"id": 10, "short_name": "Школа № 1", "...": "..."},
  "resources": {
    "subjects": [{"id": 4, "name": "Математика", "school": 10}],
    "groups": [{"id": 2, "name": "5А", "school": 10}],
    "schedule": [],
    "marks": []
  }
}
```

Полный снимок удобен для первичного импорта, но может быть большим. Для
регулярной синхронизации используйте отдельные ресурсы и фильтры.

Каталог возвращает реальные имена ресурсов текущей версии API:

```json
{
  "school_id": 10,
  "resources": ["academicyear", "groups", "marks", "schedule", "subjects"]
}
```

Имена нечувствительны к дефисам и подчёркиваниям: `schooldocument`,
`school-document` и `school_document` означают один ресурс.

## 3. Ресурсы и CRUD

### Карта ресурсов

| Область | Ресурсы |
|---|---|
| Школа и справочники | `academicyear`, `subjects`, `groups`, `building`, `room`, `typesmark`, `periodscheme`, `periodschemeitem`, `period` |
| Люди и связи | `user`, `students`, `parentlink`, `agent`, `workload` |
| Расписание | `callscheduletemplate`, `callscheduleentry`, `scheduletemplate`, `scheduletemplateinfo`, `schedule` |
| Оценки и обучение | `typemark`, `marks`, `totalmarks`, `yearfinalmark`, `ktptemplate`, `ktptemplateitem`, `ktptemplategroup`, `curriculumplan`, `curriculumitem`, `debtretake` |
| Посещаемость и задания | `attendance`, `homework`, `homeworktarget`, `homeworkattachment` |
| Документы и поддержка | `schooldocument`, `supportticket`, `supportmessage`, `supportattachment` |
| Прочее | `studentimportjob`, `logs`, `kugschedule`, `staymode`, `literequests`, `litereportselection`, `payment` |

### Формат полей и правила передачи

Ниже приведён фактический контракт универсального CRUD. Поля с пометкой
`FK` передаются как целочисленный `id` связанной записи, а не как вложенный
JSON-объект. Поля `JSON` передаются обычным JSON-значением (объектом, массивом,
строкой, числом или `null` — если поле допускает `null`). Даты имеют формат
`YYYY-MM-DD`, время — `HH:MM[:ss]`, дата-время — ISO 8601, например
`2026-08-20T12:30:00+10:00`.

`id` генерируется сервером и не передаётся при создании. Поля `created_at`,
`updated_at`, `uploaded_at`, `date_joined`, `reg_date` и аналогичные служебные
поля следует считать возвращаемыми полями; их не нужно включать в обычные
`POST`/`PATCH`. Поля-связи, помеченные `school`, для моделей с прямой связью
со школой при `POST` принудительно заменяются на `{school_id}` из URL. При
`PATCH` изменить принадлежность записи другой школе нельзя.

Обозначения в таблицах: `обяз.` — нужно передать при создании (если поле не
имеет серверного значения), `?` — допускается `null`/пустое значение, `FK:X` —
ссылка на ресурс `X`.

#### Школа

`GET /schools/{school_id}/` возвращает: `id`, `short_name`, `full_name`,
`director`, `email`, `phone?`, `legal_address?`, `actual_address?`, `inn?`,
`ogrn?`, `kpp?`, `license_no?`, `license_date?`, `license_authority?`,
`accreditation_no?`, `accreditation_date?`, `info_published`, `reg_date`,
`tarrif`, `subscription_modules?` (JSON), `subscribeto?`, `next_tariff?`,
`next_tariff_from?`, `retention_until?`, `current_year?` (FK:academicyear).
В `PATCH` разрешены все эти поля, кроме `id` и `reg_date`; передавайте только
изменяемые поля. Пример: `{ "short_name": "Школа №1", "phone": "+7..." }`.

#### Справочники и базовые сущности

| Resource | Поля тела (`POST`/`PATCH`), кроме `id` |
|---|---|
| `academicyear` | `year` (обяз.), `school` (FK:schools, подставляется), `start_date` (обяз.), `end_date` (обяз.) |
| `subjects` | `name` (обяз.), `school` (подставляется), `year` (FK:academicyear, обяз.) |
| `groups` | `name` (обяз.), `school` (подставляется), `tutor?` (FK:user), `year` (FK:academicyear, обяз.) |
| `building` | `school` (подставляется), `name` (обяз.), `address?` |
| `room` | `school` (подставляется), `building?` (FK:building), `name` (обяз.), `capacity?` (целое) |
| `typesmark` | `short` (обяз.), `full` (обяз.), `school` (подставляется), `year` (FK:academicyear, обяз.), `weight` (число, обяз.) |
| `periodscheme` | `school` (подставляется), `name` (обяз.), `is_published` (bool) |
| `periodschemeitem` | `scheme` (FK:periodscheme, обяз.), `name` (обяз.), `order` (целое), `start_date`, `end_date` |
| `period` | `year` (FK:academicyear, обяз.), `name` (обяз.), `start_date`, `end_date` |
| `user` | `username` (обяз.), `first_name?`, `last_name?`, `email?`, `is_active` (bool), `role` (обяз.), `school?` (FK:schools), `blocked` (bool), `birth_date?`, `gender?`, `snils?`, `identity_document?`, `phone?`, `position?` |
| `students` | `user` (FK:user, обяз.), `group` (FK:groups, обяз.), `school_at` (дата, обяз.), `admitted_note?`, `graduated` (bool), `left_at?`, `left_reason?`, `left_note?` |
| `agent` | `user` (FK:user, обяз.), `balance` (число), `rating` (число) |
| `parentlink` | `parent` (FK:user, обяз.), `student` (FK:user, обяз.), `relation` (обяз.) |
| `workload` | `teacher` (FK:user, обяз.), `subject` (FK:subjects, обяз.), `group` (FK:groups, обяз.), `room?` (FK:room) |

#### Расписание, оценки и учебные планы

| Resource | Поля тела (`POST`/`PATCH`), кроме `id` |
|---|---|
| `callscheduletemplate` | `school` (подставляется), `year` (FK:academicyear), `name`, `is_published` |
| `callscheduleentry` | `template` (FK:callscheduletemplate), `start_time`, `end_time`, `order`, `day_of_week?` |
| `scheduletemplate` | `school` (подставляется), `year` (FK:academicyear), `group` (FK:groups), `template_name`, `is_published`, `published_scopes?` (JSON), `published_week_dates?` (JSON) |
| `scheduletemplateinfo` | `school` (подставляется), `year` (FK:academicyear), `group` (FK:groups), `subject` (FK:subjects), `teacher` (FK:user), `period` (FK:period), `order`, `day_of_week`, `template` (FK:scheduletemplate) |
| `schedule` | `school` (подставляется), `year` (FK:academicyear), `group` (FK:groups), `subject` (FK:subjects), `teacher` (FK:user), `period` (FK:period), `date`, `order`, `day_of_week`, `template?` (FK:scheduletemplate), `room?` (FK:room), `status`, `original_teacher?` (FK:user), `original_subject?` (FK:subjects), `topic?`, `topic_color?`, `homework?`, `homework_next` (bool), `work_types?` (JSON) |
| `typemark` | `typemark` (FK:typesmark), `lesson` (FK:schedule) |
| `marks` | `student` (FK:students), `schedule` (FK:schedule), `mark` (JSON), `comment?`, `work_type?`, `typemark?` (FK:typemark) |
| `totalmarks` | `student` (FK:students), `subject` (FK:subjects), `period` (FK:period), `total_mark`, `is_manual` (bool) |
| `yearfinalmark` | `student` (FK:students), `subject` (FK:subjects), `year` (FK:academicyear), `mark`, `exam_mark?`, `cert_mark?` |
| `ktptemplate` | `school` (подставляется), `year` (FK:academicyear), `grade`, `subject` (FK:subjects), `name?`, `weekly_hours`, `level`, `status`, `author?` (FK:user) |
| `ktptemplateitem` | `template` (FK:ktptemplate), `lesson_number`, `topic`, `homework?`, `theme?`, `is_control` (bool) |
| `ktptemplategroup` | `template` (FK:ktptemplate), `group` (FK:groups) |
| `curriculumplan` | `school` (подставляется), `year` (FK:academicyear), `level`, `week_days`, `study_weeks`, `is_approved` (bool), `approved_note?` |
| `curriculumitem` | `plan` (FK:curriculumplan), `part`, `subject_area?`, `subject` (FK:subjects), `grade`, `hours_per_week`, `attestation_form?`, `order` |
| `debtretake` | `school` (подставляется), `year` (FK:academicyear), `student` (FK:students), `subject` (FK:subjects), `period?` (FK:period), `attempt`, `date`, `form?`, `commission?`, `status`, `new_mark?`, `note?`, `created_by?` (FK:user) |

#### Документы, посещаемость, поддержка и задания

| Resource | Поля тела (`POST`/`PATCH`), кроме `id` |
|---|---|
| `schooldocument` | `school` (подставляется), `title`, `category`, `file`, `is_published` (bool), `uploaded_by?` (FK:user). Файл требует multipart-запроса; JSON-строка не является загрузкой файла. |
| `attendance` | `student` (FK:students), `school` (подставляется), `year` (FK:academicyear), `date`, `mark`, `is_full_day` (bool), `lesson_numbers?` (JSON), `comment?`, `created_by?` (FK:user) |
| `supportticket` | `source`, `author?` (FK:user), `author_label?`, `author_email?`, `author_role?`, `school?` (FK:schools), `school_label?`, `subject`, `category`, `priority`, `status`, `assigned_agent?` (FK:user), `closed_at?`, `rating?`, `rating_comment?` |
| `supportmessage` | `ticket` (FK:supportticket), `author?` (FK:user), `author_label?`, `is_agent` (bool), `is_system` (bool), `body?` |
| `supportattachment` | `message` (FK:supportmessage), `file`, `original_name?`, `size` (целое). Файл требует multipart-запроса. |
| `homework` | `lesson` (FK:schedule), `order`, `description?`, `check_date?`, `check_lesson_order?`, `audience`, `created_by?` (FK:user) |
| `homeworktarget` | `homework` (FK:homework), `student` (FK:students) |
| `homeworkattachment` | `homework` (FK:homework), `file`, `original_name?`, `size` (целое). Файл требует multipart-запроса. |
| `kugschedule` | `school` (подставляется), `academic_year?` (FK:academicyear), `name`, `comment?`, `periods?`, `year_label?`, `is_published` (bool) |
| `staymode` | `school` (подставляется), `name`, `building?`, `bells?`, `events?`, `legacy_classes?`, `period_from?`, `period_to?`, `status` |

#### Журнал и импорт

| Resource | Поля тела (`POST`/`PATCH`), кроме `id` |
|---|---|
| `studentimportjob` | `school` (подставляется), `created_by?` (FK:user), `status`, `total`, `cursor`, `created_students`, `parents_created`, `parents_linked`, `skipped`, `rows?` (JSON), `errors?` (JSON), `creds?` (JSON). `creds` содержит чувствительные данные и не должен передаваться без отдельной необходимости. |
| `logs` | `action_type`, `description`, `user?` (FK:user), `user_label?`, `school?` (FK:schools), `ip?`, `user_agent?`, `date`. Обычно записи журнала создаются сервером; внешней программе рекомендуется только чтение. |

У `supportticket` и связанных `supportmessage`/`supportattachment` нет
специального режима «только чтение»: технически они подчиняются обычному
`can_write`. Если интеграции нужно сохранять обращения при очистке данных
школы, не удаляйте эти три ресурса без отдельного согласования.

`literequests`, `litereportselection` и `payment` также присутствуют в
каталоге. Это служебные сущности тарификации: их поля возвращаются в том же
формате, но создавать или изменять платежные статусы, идентификаторы YooKassa,
сырые ответы провайдера и даты применения тарифа через внешний API не следует.
Для `litereportselection` используются `school` (FK:schools), `month` и
`report_key`; для `literequests` — `short_name`, `full_name`, `director`,
`email`, `tariff`, `request_date`, `status`; для `payment` — `lite_request?`,
`school?`, `tariff`, `modules` (JSON), `amount`, `currency`, `status` и поля
платёжного провайдера.

`externalauthorizationcode` может попасть в автоматически сформированный
каталог из-за транзитивной связи с пользователем, но это внутренняя OAuth-сущность.
Не используйте её для синхронизации и не передавайте её поля во внешний API.

### Список и получение

- `GET /api/v1/school/resources/{resource}/` — список записей текущей школы;
- `GET /api/v1/school/resources/{resource}/{id}/` — одна запись текущей школы;
- для серверной интеграции замените `/school/` на
  `/schools/{school_id}/`.

Формат списка:

```json
{
  "resource": "subjects",
  "count": 2,
  "filters": {},
  "results": [
    {"id": 4, "school": 10, "name": "Математика"},
    {"id": 5, "school": 10, "name": "Физика"}
  ]
}
```

### Фильтрация списков

Без параметров список содержит все записи ресурса по школе (за все учебные
годы). Чтобы забрать только нужный срез, передайте фильтры в строке запроса:

- `?field=value` — точное совпадение (`?group=5&period=3`);
- `?field__in=1,2,3` — одно из значений, не больше 500;
- `?fk__field=value` — один переход по связи, например
  `marks?schedule__group=5&schedule__period=3` (отметки класса за период);
- `?field=null` — пустое значение поля, если поле допускает `null`;
- `id` / `id__in` — по идентификаторам записей.

Фильтровать можно по числовым, строковым, логическим полям, датам и связям
(`FK` — по `id`). JSON- и текстовые поля, переход через пользователя
(`student__user__...`) и служебные поля пользователя (`password`,
`last_login`, `is_staff`, `is_superuser`) не поддерживаются. Неизвестный или
неподдерживаемый фильтр возвращает `400`, а не весь список. Применённые
фильтры возвращаются в поле `filters`.

```bash
curl "https://ais-school.ru/api/v1/school/resources/schedule/?group=5&subject=4&period=3" \
  -H "Authorization: Bearer $TOKEN"
```

### Создание

`POST /api/v1/school/resources/{resource}/` или серверный вариант
`POST /api/v1/schools/{school_id}/resources/{resource}/`.

Для моделей с прямой связью со школой поле `school` подставляется сервером.

```bash
curl -X POST "https://ais-school.ru/api/v1/schools/10/resources/subjects/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Информатика","year":12}'
```

Ответ — `201` и созданная запись.

### Изменение и удаление

- `PATCH /api/v1/school/resources/{resource}/{id}/` — изменяет только
  переданные поля;
- `DELETE /api/v1/school/resources/{resource}/{id}/` — удаляет запись;
- для серверной интеграции используйте соответствующие адреса
  `/api/v1/schools/{school_id}/resources/...`.

Обе операции требуют `can_write=true`.

> Удаление может затронуть зависимые записи по правилам базы данных. Перед
> `DELETE` убедитесь, что интеграция понимает связи между ресурсами.

```bash
curl -X PATCH "https://ais-school.ru/api/v1/schools/10/resources/subjects/4/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Алгебра"}'
```

## 4. Отчёты

Отчёты АИС «Школа» являются вычисляемыми представлениями, а не отдельными
таблицами. Их исходные данные доступны через `/all/` и ресурсы `marks`,
`attendance`, `schedule`, `homework`, `students`, `groups` и другие. После
изменения данных внешний сайт может пересчитать отчёт у себя.

## 5. Ошибки

| Код | Причина |
|---:|---|
| `400` | Неверные поля или значения запроса |
| `401` | Нет токена, неверные реквизиты программы или токен истёк |
| `403` | Нет гранта либо недостаточно прав `can_read`/`can_write` |
| `404` | Школа, ресурс или запись не найдены |

Тело ошибки обычно содержит понятное поле `detail`, а для части ошибок — ещё и
`fields`. Пример:

```json
{"detail":"Программе не выдано требуемое право для этой школы."}
```

## 6. Безопасность

1. Храните `client_secret` и `access_token` только на сервере интеграции.
2. Используйте HTTPS и не передавайте токены в URL или логах.
3. Для чтения выдавайте только `can_read`; `can_write` включайте отдельно.
4. При компрометации токена отзовите его через `/auth/revoke/` и напишите на
   `director@ais-school.ru`, чтобы отключить доступ программы.
5. Не записывайте `Authorization`, `client_secret` и одноразовые коды в логи.
6. Для регулярной синхронизации запрашивайте только нужные ресурсы и срезы,
   вместо частого получения полного `/all/`.

## 7. Опросы и отзывы

Публичная страница текущего опроса: `GET /review`. Опросы являются версиями,
различающимися по дате (`survey_date`). Первая версия создаётся автоматически
с датой `2026-08-26`.

Пользователь, вошедший через OAuth-токен, может отправить ответы:

```http
POST /api/v1/reviews/
Authorization: Bearer <access_token>
Content-Type: application/json
```

Тело запроса содержит `answers` с полями зарегистрированного сценария:
`rating` (`1`–`5`), `missing_modules`, `disliked_modules`, `liked_modules`,
`price_satisfied` (`yes`/`no`), `change_tariff` (`yes`/`no`) и условное
`change_tariff_reason`. Необязательное поле `survey_date` выбирает активную
версию опроса, например `2026-08-26`. Ответ привязывается к пользователю и
его школе; повторная отправка обновляет его ответ для этой даты.

Просмотр ответов доступен только активным специалистам поддержки:

```http
GET /api/v1/agent/reviews/?survey_date=2026-08-26
Authorization: Token <agent_token>
```
