D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор hackeryaroslav
Источник xss.is
Создание платформы вопросов и ответов для сообщества разработчиков, подобной Stack Overflow, с интеграцией ИИ — увлекательно, все как мы любим. В данном проекте мы реализуем функционал, который позволяет пользователям задавать вопросы, получать ответы от ИИ, автоматически подбирать теги, а также анализировать и суммировать вопросы. В дополнение, система включает админ-панель и панель модератора, профили пользователей, и обладает современным дизайном. Все это будет реализовано с использованием Flask, снова.
Начнем с разбора основного кода, который отвечает за обработку вопросов и ответов. Эти модели являются ключевыми компонентами платформы, так как именно они представляют основную сущность любого Q&A ресурса. За картинку сразу извиняюсь, игрался с ИИ
( надо ник бы поменять
, я не хакер и имя даже не мое
)
Python: Скопировать в буфер обмена
Сначала импортируются необходимые модули. Мы подключаем
Python: Скопировать в буфер обмена
Модель
Теперь, когда у нас есть базовое понимание модели ответа, перейдем к следующему файлу, который описывает модель вопроса.
Python: Скопировать в буфер обмена
Здесь мы создаем вспомогательную таблицу
Python: Скопировать в буфер обмена
Модель
Python: Скопировать в буфер обмена
Далее, в модели
Python: Скопировать в буфер обмена
Здесь модель
Python: Скопировать в буфер обмена
Мы импортируем
Python: Скопировать в буфер обмена
Модель
Python: Скопировать в буфер обмена
Эти методы позволяют устанавливать и проверять пароли пользователей с использованием безопасных хешей, что важно для обеспечения безопасности аккаунтов.
Python: Скопировать в буфер обмена
Эта функция используется Flask-Login для загрузки пользователя по его идентификатору.
Python: Скопировать в буфер обмена
Модель
Python: Скопировать в буфер обмена
Эта строка добавляет ограничение, которое гарантирует, что голос может быть связан либо с вопросом, либо с ответом, но не с обоими одновременно.
Первоначально, давайте взглянем на необходимые импорты и инициализацию:
Python: Скопировать в буфер обмена
Здесь импортируются модули Flask, которые позволяют управлять маршрутизацией и взаимодействовать с шаблонами. Blueprint используется для группировки маршрутов под префиксом
Python: Скопировать в буфер обмена
Этот маршрут позволяет администратору редактировать информацию о пользователе. Сначала происходит поиск пользователя по
Python: Скопировать в буфер обмена
Здесь мы также используем
Python: Скопировать в буфер обмена
В этих маршрутах используется схожая логика: поиск записи в базе данных по id, удаление, и обработка возможных ошибок с откатом транзакции. После успешного удаления пользователь перенаправляется на соответствующую страницу с уведомлением.
Сначала рассмотрим импорты и инициализацию:
Python: Скопировать в буфер обмена
В этом файле используется Blueprint для создания группы маршрутов, связанных с ответами.
Python: Скопировать в буфер обмена
Этот маршрут позволяет пользователю добавлять ответы на вопросы. Сначала выполняется поиск вопроса по
Python: Скопировать в буфер обмена
Этот маршрут позволяет пользователям голосовать за ответы, улучшая качество контента на платформе. После нахождения ответа по
Python: Скопировать в буфер обмена
Этот маршрут позволяет автору вопроса принять один из предложенных ответов. После нахождения ответа по
Здесь импортируются необходимые модули и библиотеки. Blueprint используется для создания группы маршрутов, связанных с аутентификацией.
Python: Скопировать в буфер обмена
Этот маршрут обрабатывает регистрацию нового пользователя. При отправке POST-запроса из формы регистрации извлекаются
Python: Скопировать в буфер обмена
Этот маршрут обрабатывает вход пользователя в систему. При отправке POST-запроса из формы извлекаются
Python: Скопировать в буфер обмена
Этот маршрут позволяет пользователю выйти из системы. После выхода из системы происходит перенаправление на главную страницу платформы.
Python: Скопировать в буфер обмена
Этот маршрут доступен только администраторам, и предоставляет им доступ к информации о всех пользователях, вопросах и тегах на платформе. Данные извлекаются из базы данных и передаются в шаблон
Python: Скопировать в буфер обмена
Этот маршрут доступен только модераторам и отображает все вопросы, которые были помечены как нарушающие правила. Извлеченные данные передаются в шаблон
Python: Скопировать в буфер обмена
Этот маршрут позволяет пользователям сообщать о вопросах, которые они считают нарушающими правила. Если вопрос еще не был отмечен, он помечается как нарушающий, и сохраняется информация о пользователе, который сделал сообщение. Если возникает ошибка при сохранении изменений, транзакция откатывается, и пользователю показывается сообщение об ошибке. В случае успешного сообщения отображается сообщение о том, что вопрос был отправлен модераторам.
Python: Скопировать в буфер обмена
Здесь импортируются необходимые библиотеки и модули. Blueprint используется для создания маршрутов модератора. Модули из flask_login управляют доступом к маршрутам только для вошедших в систему пользователей. Модели
Python: Скопировать в буфер обмена
Этот маршрут позволяет модераторам просматривать все вопросы, которые были помечены пользователями как нарушающие правила. Используется фильтрация по полю
Python: Скопировать в буфер обмена
Этот маршрут позволяет модераторам одобрить вопрос. При отправке POST-запроса из формы, вопрос, который был помечен как нарушающий правила, обновляется: флаг
Python: Скопировать в буфер обмена
Этот маршрут позволяет модераторам удалить вопрос из системы. При отправке POST-запроса осуществляется поиск вопроса по
Здесь импортируются необходимые модули и функции. Blueprint используется для создания маршрутов для работы с вопросами. flask_login управляет доступом к маршрутам. Модели
Этот маршрут позволяет пользователям создавать новые вопросы. При отправке формы POST данные извлекаются из полей
Этот маршрут обрабатывает голосование за вопрос. Если пользователь уже голосовал за вопрос, проверяется тип предыдущего голоса и обновляется количество голосов в зависимости от нового типа голосования. Если голос отсутствует, создается новый объект
Этот маршрут отображает детали вопроса, включая количество просмотров, ИИ-инсайты, ранжированные ответы и подобные вопросы. Если краткое содержание вопроса отсутствует, оно генерируется и сохраняется в базе данных. Также создается список ответов, ранжированных с помощью ИИ, и находят похожие вопросы.
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
HTML: Скопировать в буфер обмена
Эти метатеги обеспечивают правильное отображение на различных устройствах и задают кодировку документа.
HTML: Скопировать в буфер обмена
HTML: Скопировать в буфер обмена
Меню навигации меняется в зависимости от статуса аутентификации пользователя. Если пользователь вошел в систему, отображаются ссылки на страницы "Задать вопрос", "Профиль" и "Выход". Если нет, отображаются ссылки на "Вход" и "Регистрацию". Форма поиска позволяет пользователям искать вопросы по ключевым словам.
HTML: Скопировать в буфер обмена
Функция
HTML: Скопировать в буфер обмена
Этот раздел предоставляет пользователям дополнительную информацию о сайте и ссылки на страницы, такие как "О нас", "Контакт" и "Часто задаваемые вопросы". Иконки социальных сетей позволяют пользователям быстро перейти на профили компании в социальных сетях.
HTML: Скопировать в буфер обмена
Этот файл добавляет функциональность, такую как прокрутка до верхней части страницы, анимация элементов при прокрутке и обработка форм.
HTML: Скопировать в буфер обмена
HTML: Скопировать в буфер обмена
Каждая строка таблицы представляет одного пользователя. Кнопка "Edit" перенаправляет на страницу редактирования пользователя, а форма с кнопкой "Delete" отправляет запрос на удаление пользователя после подтверждения действия.
HTML: Скопировать в буфер обмена
Таблица вопросов аналогична таблице пользователей, где каждое действие сопровождается кнопкой удаления, требующей подтверждения от администратора.
HTML: Скопировать в буфер обмена
Таблица тегов позволяет администратору удалять теги с помощью кнопки "Delete", также требующей подтверждения.
HTML: Скопировать в буфер обмена
Опять же, форма включает скрытое поле для CSRF-токена, что защищает от атак межсайтовой подделки запросов. Поля формы включают ввод для имени пользователя, электронной почты и выбора роли (пользователь, модератор или администратор). Эти данные будут обновлены в базе данных при отправке формы. Форма завершается кнопкой "Update User" для отправки данных и ссылкой для возврата на панель управления администратора.
HTML: Скопировать в буфер обмена
На главной странице представлена секция "hero" с приветственным сообщением и кнопкой для создания нового вопроса. Ниже идет список последних вопросов, каждый из которых отображается в виде краткого резюме. Включены ссылки на детали каждого вопроса, информация о пользователе, который задал вопрос, метка времени, количество просмотров и голосов. Теги, связанные с каждым вопросом, также отображаются, что помогает пользователям быстро понять тему вопроса и найти вопросы по интересующим их темам.
HTML: Скопировать в буфер обмена
И снова, форма включает CSRF-токен для безопасности и кнопку "Login" для отправки данных. Внизу страницы размещена ссылка для регистрации, если у пользователя еще нет аккаунта. Маленький трюк, который помогает новым пользователям быстро найти способ создания аккаунта, или просто улучшает UX.
HTML: Скопировать в буфер обмена
HTML: Скопировать в буфер обмена
В содержимом шаблона сначала отображается заголовок "Moderation Panel" и таблица с сообщениями, которые были пожалованы. Таблица содержит такие колонки, как заголовок вопроса, автор, кто пожаловался и действия. В каждой строке таблицы указана информация о вопросе и пользователе, который его пожаловался.
Кнопки для действий включают "Approve" и "Delete". Эти кнопки представлены в виде форм, отправляющих POST-запросы к соответствующим маршрутам для утверждения или удаления вопроса.
HTML: Скопировать в буфер обмена
Особое внимание уделяется разделу "AI Insights", который отображает результаты анализа вопроса с использованием ИИ. Этот раздел может содержать полезные замечания или рекомендации, предоставляемые ИИ на основе анализа вопроса.
Раздел "AI-Generated Summary" показывает автоматически сгенерированное резюме вопроса, если таковое имеется. В блоке "Similar Questions" представлены вопросы, похожие на текущий, что позволяет пользователям находить связанные темы и расширяет возможности поиска.
Кроме того, пользователи могут просматривать и голосовать за ответы на вопрос. Под каждым ответом представлены кнопки для голосования и возможностью отметить ответ как принятый, если пользователь, задавший вопрос, его одобрил. Форма для добавления собственного ответа также включена в этот шаблон.
HTML: Скопировать в буфер обмена
Каждый вопрос представлен в виде ссылки, ведущей к подробной странице вопроса. Этот шаблон предоставляет пользователю возможность быстро просмотреть результаты поиска и выбрать интересующие вопросы. Если результаты поиска отсутствуют, пользователю будет показано сообщение об этом.
HTML: Скопировать в буфер обмена
Далее представлены разделы с вопросами и ответами пользователя. Каждый вопрос и ответ отображаются в виде краткого описания с ссылкой на полное содержание. В блоке "user-questions" показываются вопросы, заданные пользователем, а в блоке "user-answers" — ответы, которые он оставил. Если у пользователя нет вопросов или ответов, отображаются соответствующие сообщения.
Заключение
Друзья, огромное спасибо всем, кто дочитал статью до конца! Тема ИИ и веб-разработки действительно интересна, но написание проектов и, тем более, качественно оформленных статей требует ещё больше времени. Не забудьте также создать вашу дб и таблицы, перед тем как запускать код. Самые внимательные могли заметить, что не каждая функция реализована на клиенте, я лишь создал простой и приятный шаблон, вы сами можете "закончить" добавив это. Буду благодарен за фидбек. Если обнаружите баги или недочёты, пожалуйста, сообщите, и я постараюсь их исправить. Всем добра!
Источник xss.is
Создание платформы вопросов и ответов для сообщества разработчиков, подобной Stack Overflow, с интеграцией ИИ — увлекательно, все как мы любим. В данном проекте мы реализуем функционал, который позволяет пользователям задавать вопросы, получать ответы от ИИ, автоматически подбирать теги, а также анализировать и суммировать вопросы. В дополнение, система включает админ-панель и панель модератора, профили пользователей, и обладает современным дизайном. Все это будет реализовано с использованием Flask, снова.
Начнем с разбора основного кода, который отвечает за обработку вопросов и ответов. Эти модели являются ключевыми компонентами платформы, так как именно они представляют основную сущность любого Q&A ресурса. За картинку сразу извиняюсь, игрался с ИИ


Разбор файла answer.py
Файлanswer.py
содержит модель Answer
, которая отвечает за хранение и обработку данных, связанных с ответами пользователей на вопросы. Рассмотрим каждую часть этого файла более подробно.Python: Скопировать в буфер обмена
Код:
from app import db
from datetime import datetime
Сначала импортируются необходимые модули. Мы подключаем
db
из Flask приложения, чтобы работать с базой данных, а также datetime
для управления временными метками.Python: Скопировать в буфер обмена
Код:
class Answer(db.Model):
__tablename__ = "answers"
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
question_id = db.Column(db.Integer, db.ForeignKey("questions.id"))
votes = db.Column(db.Integer, default=0)
is_accepted = db.Column(db.Boolean, default=False)
reported = db.Column(db.Boolean, default=False)
Модель
Answer
описывает структуру таблицы answers
в базе данных. Каждый ответ имеет уникальный идентификатор id
, текстовое поле body
, временную метку создания timestamp
, а также связи с таблицами пользователей (user_id) и вопросов (question_id). Поле votes
хранит количество голосов за ответ, is_accepted
указывает, был ли ответ принят как лучший, а reported
позволяет отметить ответ как неподобающий или спам.Теперь, когда у нас есть базовое понимание модели ответа, перейдем к следующему файлу, который описывает модель вопроса.
Разбор файла question.py
МодельQuestion
определяет структуру таблицы questions
и описывает основные свойства и связи, присущие каждому вопросу на платформе.Python: Скопировать в буфер обмена
Код:
from app import db
from datetime import datetime
question_tags = db.Table(
"question_tags",
db.Column(
"question_id", db.Integer, db.ForeignKey("questions.id"), primary_key=True
),
db.Column("tag_id", db.Integer, db.ForeignKey("tags.id"), primary_key=True),
)
Здесь мы создаем вспомогательную таблицу
question_tags
для реализации связи многие-ко-многим между вопросами и тегами. Эта таблица поможет нам хранить информацию о том, какие теги привязаны к каждому вопросу.Python: Скопировать в буфер обмена
Код:
class Question(db.Model):
__tablename__ = "questions"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
body = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
answers = db.relationship("Answer", backref="question", lazy="dynamic")
tags = db.relationship(
"Tag", secondary=question_tags, backref=db.backref("questions", lazy="dynamic")
)
views = db.Column(db.Integer, default=0)
votes = db.Column(db.Integer, default=0)
reported = db.Column(db.Boolean, default=False)
summary = db.Column(db.Text)
Модель
Question
описывает основную сущность вопроса. Здесь присутствуют поля для хранения идентификатора id
, заголовка title
, содержания body
, временной метки создания timestamp
, а также идентификатора пользователя user_id
, который задал вопрос. Поле answers
устанавливает связь один-ко-многим с моделью Answer
, что позволяет привязывать к вопросу множество ответов. Поле tags
связано с моделью Tag
через таблицу question_tags
, что позволяет назначать теги к каждому вопросу.Python: Скопировать в буфер обмена
Код:
user = db.relationship("User", foreign_keys=[user_id], back_populates="questions")
reported_by_id = db.Column(db.Integer, db.ForeignKey("users.id"))
reported_by_user = db.relationship(
"User", foreign_keys=[reported_by_id], back_populates="reported_questions"
)
Далее, в модели
Question
, мы добавляем связь с моделью User
для хранения информации о том, кто задал вопрос, а также кто его пожаловался.Разбор файла tag.py
Файлtag.py
описывает модель Tag
, которая представляет теги, используемые для классификации вопросов.Python: Скопировать в буфер обмена
Код:
from app import db
class Tag(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
Здесь модель
Tag
содержит два поля: id
, который уникально идентифицирует каждый тег, и name
, который хранит название тега.Разбор файла user.py
Файлuser.py
включает в себя модель User
, которая описывает пользователей платформы, а также их аутентификацию и авторизацию.Python: Скопировать в буфер обмена
Код:
from app import db, login_manager
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
Мы импортируем
db
и login_manager
из нашего приложения, а также используем UserMixin
из Flask-Login для упрощения работы с пользователями и модули из Werkzeug для управления паролями.Python: Скопировать в буфер обмена
Код:
class User(UserMixin, db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(120), unique=True, index=True)
password_hash = db.Column(db.String(128))
reputation = db.Column(db.Integer, default=0)
questions = db.relationship(
"Question",
foreign_keys="Question.user_id",
back_populates="user",
lazy="dynamic",
)
reported_questions = db.relationship(
"Question",
foreign_keys="Question.reported_by_id",
back_populates="reported_by_user",
lazy="dynamic",
)
answers = db.relationship("Answer", backref="author", lazy="dynamic")
is_admin = db.Column(db.Boolean, default=False)
is_moderator = db.Column(db.Boolean, default=False)
Модель
User
включает основные атрибуты пользователя, такие как username
, email
, password_hash
, и reputation
. Она также определяет связи с вопросами и ответами, позволяя хранить информацию о том, какие вопросы и ответы связаны с конкретным пользователем. Поля is_admin
и is_moderator
определяют, является ли пользователь администратором или модератором.Python: Скопировать в буфер обмена
Код:
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Эти методы позволяют устанавливать и проверять пароли пользователей с использованием безопасных хешей, что важно для обеспечения безопасности аккаунтов.
Python: Скопировать в буфер обмена
Код:
@login_manager.user_loader
def load_user(id):
return User.query.get(int(id))
Эта функция используется Flask-Login для загрузки пользователя по его идентификатору.
Разбор файла vote.py
Последний файл в этом наборе модел —vote.py
, который отвечает за голосование за вопросы и ответы.Python: Скопировать в буфер обмена
Код:
from app import db
class Vote(db.Model):
__tablename__ = "votes"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
question_id = db.Column(db.Integer, db.ForeignKey("questions.id"))
answer_id = db.Column(db.Integer, db.ForeignKey("answers.id"))
vote_type = db.Column(db.String(4), nullable=False)
Модель
Vote
содержит поля user_id
, question_id
, и answer_id
, что позволяет связывать голос с конкретным пользователем, вопросом или ответом. Поле vote_type
хранит тип голоса, который может быть положительным (up) или отрицательным (down).Python: Скопировать в буфер обмена
Код:
__table_args__ = (
db.CheckConstraint("(question_id IS NULL) != (answer_id IS NULL)"),
)
Эта строка добавляет ограничение, которое гарантирует, что голос может быть связан либо с вопросом, либо с ответом, но не с обоими одновременно.
Файл admin.py
Файлadmin.py
отвечает за управление пользователями и контентом через административную панель. Этот файл содержит маршруты для редактирования и удаления пользователей, вопросов и тегов, а также предоставляет интерфейс для выполнения этих задач.Первоначально, давайте взглянем на необходимые импорты и инициализацию:
Python: Скопировать в буфер обмена
Код:
from flask import Blueprint, render_template, request, redirect, url_for, flash
from app import db
from app.models.user import User
from app.models.tag import Tag
from app.models.question import Question
Здесь импортируются модули Flask, которые позволяют управлять маршрутизацией и взаимодействовать с шаблонами. Blueprint используется для группировки маршрутов под префиксом
/admin
, что упрощает организацию кода. Также мы импортируем объекты моделей для работы с пользователями, тегами и вопросами, что позволит нам выполнять операции с этими сущностями в базе данных.Редактирование пользователя
Маршрут для редактирования пользователя выглядит следующим образом:Python: Скопировать в буфер обмена
Код:
@bp.route("/edit_user/<int:user_id>", methods=["GET", "POST"])
def edit_user(user_id):
user = User.query.get_or_404(user_id)
if request.method == "POST":
user.username = request.form["username"]
user.email = request.form["email"]
user.role = request.form["role"]
try:
db.session.commit()
flash("User details updated successfully!", "success")
return redirect(url_for("admin.admin_dashboard"))
except Exception as e:
db.session.rollback()
flash("An error occurred while updating the user.", "error")
return render_template("edit_user.html", user=user)
Этот маршрут позволяет администратору редактировать информацию о пользователе. Сначала происходит поиск пользователя по
user_id
. Если запрос является POST, данные из формы передаются в соответствующие поля пользователя: имя, email и роль. После этого мы пытаемся сохранить изменения в базе данных с помощью db.session.commit()
. Если возникает ошибка, транзакция откатывается, и пользователю показывается сообщение об ошибке.Удаление пользователя
Следующий маршрут отвечает за удаление пользователя:Python: Скопировать в буфер обмена
Код:
@bp.route("/delete_user/<int:user_id>", methods=["POST"])
def delete_user(user_id):
user = User.query.get_or_404(user_id)
try:
db.session.delete(user)
db.session.commit()
flash("User deleted successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while deleting the user.", "error")
return redirect(url_for("main.admin_dashboard"))
Здесь мы также используем
user_id
для поиска пользователя. Если пользователь найден, он удаляется из базы данных с помощью db.session.delete(user)
. В случае успешного удаления происходит перенаправление на административную панель с соответствующим сообщением.Удаление вопросов и тегов
Аналогично пользователям, маршруты для удаления вопросов и тегов работают по тому же принципу:Python: Скопировать в буфер обмена
Код:
@bp.route("/delete_question/<int:question_id>", methods=["POST"])
def delete_question(question_id):
question = Question.query.get_or_404(question_id)
try:
db.session.delete(question)
db.session.commit()
flash("Question deleted successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while deleting the question.", "error")
return redirect(url_for("main.admin_dashboard"))
@bp.route("/delete_tag/<int:tag_id>", methods=["POST"])
def delete_tag(tag_id):
tag = Tag.query.get_or_404(tag_id)
try:
db.session.delete(tag)
db.session.commit()
flash("Tag deleted successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while deleting the tag.", "error")
return redirect(url_for("auth.admin_dashboard"))
В этих маршрутах используется схожая логика: поиск записи в базе данных по id, удаление, и обработка возможных ошибок с откатом транзакции. После успешного удаления пользователь перенаправляется на соответствующую страницу с уведомлением.
Файл answers.py
Файлanswers.py
отвечает за функциональность, связанную с добавлением, голосованием и принятием ответов на вопросы. Здесь определены маршруты, которые позволяют пользователям взаимодействовать с ответами на платформе.Сначала рассмотрим импорты и инициализацию:
Python: Скопировать в буфер обмена
Код:
from flask import Blueprint, redirect, url_for, request, flash
from flask_login import login_required, current_user
from app import db
from app.models.question import Question
from app.models.answer import Answer
from app.models.vote import Vote
from app.utils.ai_helper import get_ai_answer_review
В этом файле используется Blueprint для создания группы маршрутов, связанных с ответами.
login_required
декоратор из flask_login гарантирует, что только авторизованные пользователи могут добавлять и голосовать за ответы. Модели Question
, Answer
, и Vote
импортируются для работы с соответствующими данными в базе, а функция get_ai_answer_review
из ai_helper
используется для получения анализа ответа с помощью ИИ.Добавление ответа
Маршрут для добавления ответа на вопрос выглядит следующим образом:Python: Скопировать в буфер обмена
Код:
@bp.route("/question/<int:question_id>/answer", methods=["POST"])
@login_required
def post_answer(question_id):
question = Question.query.get_or_404(question_id)
answer_body = request.form["answer_body"]
answer = Answer(body=answer_body, author=current_user, question=question)
db.session.add(answer)
db.session.commit()
ai_review = get_ai_answer_review(question.body, answer_body)
flash(f"Your answer has been posted! AI Review: {ai_review}")
return redirect(url_for("questions.question_detail", question_id=question_id))
Этот маршрут позволяет пользователю добавлять ответы на вопросы. Сначала выполняется поиск вопроса по
question_id
, а затем из формы извлекается текст ответа. Создается объект Answer
, который привязывается к текущему пользователю (author=current_user) и вопросу. После сохранения ответа в базе данных с помощью db.session.commit()
, вызывается функция get_ai_answer_review
, которая анализирует ответ с помощью ИИ. Результат анализа отображается пользователю через flash-сообщение, после чего происходит перенаправление обратно на страницу вопроса.Голосование за ответ
Следующий маршрут отвечает за голосование за ответ:Python: Скопировать в буфер обмена
Код:
@bp.route("/answer/<int:answer_id>/vote", methods=["POST"])
@login_required
def vote_answer(answer_id):
answer = Answer.query.get_or_404(answer_id)
vote_type = request.form["vote_type"]
existing_vote = Vote.query.filter_by(
user_id=current_user.id, answer_id=answer_id
).first()
if existing_vote:
if existing_vote.vote_type != vote_type:
answer.votes += 2 if vote_type == "up" else -2
existing_vote.vote_type = vote_type
else:
answer.votes += -1 if vote_type == "up" else 1
db.session.delete(existing_vote)
else:
new_vote = Vote(
user_id=current_user.id, answer_id=answer_id, vote_type=vote_type
)
db.session.add(new_vote)
answer.votes += 1 if vote_type == "up" else -1
db.session.commit()
return redirect(
url_for("questions.question_detail", question_id=answer.question_id)
)
Этот маршрут позволяет пользователям голосовать за ответы, улучшая качество контента на платформе. После нахождения ответа по
answer_id
, проверяется, существует ли уже голос от текущего пользователя за данный ответ. Если голос существует и его тип отличается от нового голоса, то счетчик голосов ответа корректируется, и тип голоса обновляется. Если голос уже совпадает с новым, то голос удаляется, уменьшая счетчик голосов. Если голос отсутствует, создается новый объект Vote
, который добавляется в базу данных. После всех изменений, результат сохраняется в базе данных, и пользователь перенаправляется обратно на страницу вопроса. Не лучшее решение, ведь это занимает какое-то время чтобы сохранится.Принятие ответа
Последний маршрут в этом файле позволяет автору вопроса принять ответ:Python: Скопировать в буфер обмена
Код:
@bp.route("/answer/<int:answer_id>/accept", methods=["POST"])
@login_required
def accept_answer(answer_id):
answer = Answer.query.get_or_404(answer_id)
question = answer.question
if current_user != question.author:
flash("You can only accept answers to your own questions.")
return redirect(url_for("questions.question_detail", question_id=question.id))
for a in question.answers:
a.is_accepted = False
answer.is_accepted = True
db.session.commit()
flash("Answer has been accepted.")
return redirect(url_for("questions.question_detail", question_id=question.id))
Этот маршрут позволяет автору вопроса принять один из предложенных ответов. После нахождения ответа по
answer_id
, проверяется, является ли текущий пользователь автором вопроса. Если нет, пользователю показывается сообщение об ошибке. Если да, то все ответы на вопрос помечаются как непринятые, а выбранный ответ отмечается как принятый. Изменения сохраняются в базе данных, и пользователь перенаправляется обратно на страницу вопроса. Функцию можно в дальнейшем применить на стороне клиента (но пока это решает админ).Файл auth.py
Файл auth.py отвечает за реализацию системы аутентификации пользователей на платформе. В нем определены маршруты для регистрации, входа в систему, выхода, а также для доступа к административной и модераторской панелям. Давайте разберем каждую часть этого файла более подробно.Импорт библиотек и инициализация
Python: Скопировать в буфер обмена
Код:
import os
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash, generate_password_hash
from app.models.user import User
from app import db
from app.models.question import Question
from app.models.tag import Tag
from app.utils.decorators import admin_required, moderator_required
Здесь импортируются необходимые модули и библиотеки. Blueprint используется для создания группы маршрутов, связанных с аутентификацией.
login_user
, logout_user
, и login_required
из flask_login управляют процессом аутентификации и авторизации. check_password_hash
и generate_password_hash
из werkzeug.security предназначены для проверки и создания хэшей паролей. Импортируются модели User
, Question
, и Tag
, а также декораторы admin_required
и moderator_required
для контроля доступа.Регистрация пользователя
Маршрут для регистрации нового пользователя:Python: Скопировать в буфер обмена
Код:
@bp.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form["username"]
email = request.form["email"]
password = request.form["password"]
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash("Registration successful. Please log in.")
return redirect(url_for("auth.login"))
return render_template("register.html")
Этот маршрут обрабатывает регистрацию нового пользователя. При отправке POST-запроса из формы регистрации извлекаются
username
, email
, и password
. Создается новый объект User
, и пароль хэшируется с помощью метода set_password
. После добавления пользователя в базу данных и сохранения изменений, пользователю показывается сообщение о успешной регистрации и происходит перенаправление на страницу входа.Вход в систему
Маршрут для входа в систему:Python: Скопировать в буфер обмена
Код:
@bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
login_type = request.form["login_type"]
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
if login_type == "admin":
if user.is_admin:
return redirect(url_for("auth.admin_dashboard"))
else:
flash("You do not have admin access.")
elif login_type == "moderator":
if user.is_moderator:
return redirect(url_for("auth.moderate"))
else:
flash("You do not have moderator access.")
else:
return redirect(url_for("main.index"))
else:
flash("Invalid username or password.")
return render_template("login.html")
Этот маршрут обрабатывает вход пользователя в систему. При отправке POST-запроса из формы извлекаются
username
, password
, и login_type
. Проверяется, существует ли пользователь с таким именем и правильность пароля. Если проверка проходит успешно, пользователь аутентифицируется с помощью login_user
. В зависимости от типа входа (admin или moderator), происходит перенаправление на соответствующую панель управления. Если пользователь не имеет соответствующих прав, отображается сообщение о недостатке доступа. Если данные неверны, выводится сообщение об ошибке.Выход из системы
Маршрут для выхода из системы:Python: Скопировать в буфер обмена
Код:
@bp.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("main.index"))
Этот маршрут позволяет пользователю выйти из системы. После выхода из системы происходит перенаправление на главную страницу платформы.
Административная панель
Маршрут для доступа к административной панели:Python: Скопировать в буфер обмена
Код:
@bp.route("/dashboard")
@login_required
@admin_required
def admin_dashboard():
users = User.query.all()
questions = Question.query.all()
tags = Tag.query.all()
return render_template(
"admin_dashboard.html", users=users, questions=questions, tags=tags
)
Этот маршрут доступен только администраторам, и предоставляет им доступ к информации о всех пользователях, вопросах и тегах на платформе. Данные извлекаются из базы данных и передаются в шаблон
admin_dashboard.html
, который отображает административную панель.Панель модератора
Маршрут для доступа к панели модератора:Python: Скопировать в буфер обмена
Код:
@bp.route("/moderate")
@login_required
@moderator_required
def moderate():
reported_questions = Question.query.filter(Question.reported == True).all()
return render_template(
"moderate.html",
reported_questions=reported_questions,
)
Этот маршрут доступен только модераторам и отображает все вопросы, которые были помечены как нарушающие правила. Извлеченные данные передаются в шаблон
moderate.html
, где модераторы могут просматривать и принимать меры по этим вопросам.Сообщение о нарушении
Маршрут для сообщения о нарушении:Python: Скопировать в буфер обмена
Код:
@bp.route("/report_question/<int:question_id>", methods=["POST"])
@login_required
def report_question(question_id):
question = Question.query.get_or_404(question_id)
if not question.reported:
question.reported = True
question.reported_by_id = current_user.id
try:
db.session.commit()
flash("The question has been reported to the moderators.", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while reporting the question.", "error")
else:
flash("This question has already been reported.", "warning")
return redirect(url_for("main.index"))
Этот маршрут позволяет пользователям сообщать о вопросах, которые они считают нарушающими правила. Если вопрос еще не был отмечен, он помечается как нарушающий, и сохраняется информация о пользователе, который сделал сообщение. Если возникает ошибка при сохранении изменений, транзакция откатывается, и пользователю показывается сообщение об ошибке. В случае успешного сообщения отображается сообщение о том, что вопрос был отправлен модераторам.
Файл moderator.py
Файлmoderator.py
реализует функциональность, необходимую модераторам для управления вопросами, которые были отмечены пользователями. Он предоставляет маршруты для просмотра, одобрения и удаления вопросов. Рассмотрим подробнее, как организована эта часть системы.Импорт библиотек и инициализация
Python: Скопировать в буфер обмена
Код:
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from app import db
from app.models.question import Question
from app.models.answer import Answer
from app.utils.decorators import moderator_required
Здесь импортируются необходимые библиотеки и модули. Blueprint используется для создания маршрутов модератора. Модули из flask_login управляют доступом к маршрутам только для вошедших в систему пользователей. Модели
Question
и Answer
из app.models позволяют взаимодействовать с данными о вопросах и ответах в базе данных. Декоратор moderator_required
используется для ограничения доступа к определенным маршрутам только для модераторов.Просмотр вопросов, требующих модерации
Python: Скопировать в буфер обмена
Код:
@bp.route("/moderate")
@login_required
@moderator_required
def moderate():
reported_questions = Question.query.filter(Question.reported == True).all()
return render_template(
"moderate.html",
reported_questions=reported_questions,
)
Этот маршрут позволяет модераторам просматривать все вопросы, которые были помечены пользователями как нарушающие правила. Используется фильтрация по полю
reported
, чтобы получить список таких вопросов. Данные передаются в шаблон moderate.html
, где модераторы могут просматривать и обрабатывать эти вопросы.Одобрение вопроса
Python: Скопировать в буфер обмена
Код:
@bp.route("/approve_question/<int:question_id>", methods=["POST"])
@moderator_required
def approve_question(question_id):
question = Question.query.get_or_404(question_id)
question.reported = False
question.reported_by = None
try:
db.session.commit()
flash("Question approved successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while approving the question.", "error")
return redirect(url_for("moderator.moderate"))
Этот маршрут позволяет модераторам одобрить вопрос. При отправке POST-запроса из формы, вопрос, который был помечен как нарушающий правила, обновляется: флаг
reported
сбрасывается, и поле reported_by
очищается. Изменения сохраняются в базе данных. Если процесс проходит успешно, пользователю показывается сообщение об успешном одобрении. В случае ошибки транзакция откатывается, и отображается сообщение об ошибке. После обработки вопроса происходит перенаправление на страницу модерации.Удаление вопроса
Python: Скопировать в буфер обмена
Код:
@bp.route("/delete_question/<int:question_id>", methods=["POST"])
@moderator_required
def delete_question(question_id):
question = Question.query.get_or_404(question_id)
try:
db.session.delete(question)
db.session.commit()
flash("Question deleted successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while deleting the question.", "error")
return redirect(url_for("moderator.moderate"))
Этот маршрут позволяет модераторам удалить вопрос из системы. При отправке POST-запроса осуществляется поиск вопроса по
question_id
. Если вопрос найден, он удаляется из базы данных. Если операция проходит успешно, пользователю показывается сообщение об успешном удалении. В случае ошибки транзакция откатывается, и выводится сообщение об ошибке. После удаления вопроса происходит перенаправление на страницу модерации.Файл questions.py
Файлquestions.py
реализует маршруты для работы с вопросами на платформе. Этот файл охватывает функционал для работы и отображения деталей вопроса. Давайте разберем его более подробно.Импорт библиотек и инициализация
Python: Скопировать в буфер обмена
Код:
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_required, current_user
from app import db
from app.models.question import Question
from app.models.tag import Tag
from app.models.vote import Vote
from app.utils.ai_helper import (
get_ai_insights,
get_ai_suggested_tags,
get_ai_question_summary,
get_ai_answer_ranking,
get_ai_similar_questions,
)
Здесь импортируются необходимые модули и функции. Blueprint используется для создания маршрутов для работы с вопросами. flask_login управляет доступом к маршрутам. Модели
Question
, Tag
, и Vote
из app.models используются для работы с данными вопросов, тегов и голосов. Функции из app.utils.ai_helper
предоставляют ИИ-функционал, такой как генерация тегов и ранжирование ответов.Создание нового вопроса
Python: Скопировать в буфер обмена
Код:
@bp.route("/ask", methods=["GET", "POST"])
@login_required
def ask_question():
if request.method == "POST":
title = request.form["title"]
body = request.form["body"]
suggested_tags = get_ai_suggested_tags(title, body)
summary = get_ai_question_summary(title, body)
if summary is None:
summary = "Unable to generate summary at this time."
question = Question(
title=title, body=body, user_id=current_user.id, summary=summary
)
tags = (
request.form["tags"].split(",") if request.form["tags"] else suggested_tags
)
for tag_name in tags:
tag = Tag.query.filter_by(name=tag_name.strip()).first()
if not tag:
tag = Tag(name=tag_name.strip())
question.tags.append(tag)
db.session.add(question)
db.session.commit()
flash("Your question has been posted!")
return redirect(url_for("questions.question_detail", question_id=question.id))
return render_template("ask_question.html")
Этот маршрут позволяет пользователям создавать новые вопросы. При отправке формы POST данные извлекаются из полей
title
и body
. AI-сервисы генерируют предложенные теги и краткое содержание вопроса. Вопрос добавляется в базу данных, а теги либо предоставляются пользователем, либо сгенерированы AI. После сохранения пользователю показывается сообщение об успешной публикации вопроса.Голосование за вопрос
Python: Скопировать в буфер обмена
Код:
@bp.route("/question/<int:question_id>/vote", methods=["POST"])
@login_required
def vote_question(question_id):
question = Question.query.get_or_404(question_id)
vote_type = request.form["vote_type"]
existing_vote = Vote.query.filter_by(
user_id=current_user.id, question_id=question_id
).first()
if existing_vote:
if existing_vote.vote_type != vote_type:
question.votes += 2 if vote_type == "up" else -2
existing_vote.vote_type = vote_type
else:
question.votes += -1 if vote_type == "up" else 1
db.session.delete(existing_vote)
else:
new_vote = Vote(
user_id=current_user.id, question_id=question_id, vote_type=vote_type
)
db.session.add(new_vote)
question.votes += 1 if vote_type == "up" else -1
db.session.commit()
return redirect(url_for("questions.question_detail", question_id=question.id))
Этот маршрут обрабатывает голосование за вопрос. Если пользователь уже голосовал за вопрос, проверяется тип предыдущего голоса и обновляется количество голосов в зависимости от нового типа голосования. Если голос отсутствует, создается новый объект
Vote
. После обработки голосования количество голосов в вопросе обновляется и данные сохраняются в базе данных. Опять же, не лучшее решение, ведь это занимает какое-то время чтобы сохранится.Просмотр деталей вопроса
Python: Скопировать в буфер обмена
Код:
@bp.route("/question/<int:question_id>")
def question_detail(question_id):
question = Question.query.get_or_404(question_id)
question.views += 1
db.session.commit()
ai_insights = get_ai_insights(question.title, question.body)
if ai_insights is None:
ai_insights = "Unable to generate AI insights at this time."
if question.summary is None:
question.summary = get_ai_question_summary(question.title, question.body)
if question.summary is None:
question.summary = "Unable to generate summary at this time."
db.session.commit()
answers_list = list(question.answers)
ranking = get_ai_answer_ranking(question.body, answers_list)
ranked_answers = [
answers_list[i - 1] for i in ranking if 1 <= i <= len(answers_list)
]
similar_question_ids = get_ai_similar_questions(
question.title, question.body, Question.query.all()
)
similar_questions = Question.query.filter(
Question.id.in_(similar_question_ids)
).all()
return render_template(
"question_detail.html",
question=question,
ai_insights=ai_insights,
ranked_answers=ranked_answers,
similar_questions=similar_questions,
)
Этот маршрут отображает детали вопроса, включая количество просмотров, ИИ-инсайты, ранжированные ответы и подобные вопросы. Если краткое содержание вопроса отсутствует, оно генерируется и сохраняется в базе данных. Также создается список ответов, ранжированных с помощью ИИ, и находят похожие вопросы.
Файл main.js
Файлmain.js
включает в себя функциональные возможности, такие как плавная прокрутка, динамическое отображение кнопки "Назад вверх", анимации на прокрутку, валидацию форм и управление навигационным меню. Также реализована функциональность для предложения тегов и выделения лучших ответов. Рассмотрим подробно каждую часть этого файла.Плавная прокрутка
Первый блок кода добавляет плавную прокрутку к якорным ссылкам. После загрузки документа (DOMContentLoaded), скрипт находит все ссылки, начинающиеся с#
, и добавляет к ним обработчик клика. При клике на такую ссылку, прокрутка страницы к указанному элементу происходит плавно.JavaScript: Скопировать в буфер обмена
Код:
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
document.querySelector(this.getAttribute("href")).scrollIntoView({
behavior: "smooth",
});
});
});
});
Кнопка "Назад вверх"
Следующий функционал добавляет кнопку "Назад вверх", которая появляется при прокрутке страницы вниз и исчезает, когда пользователь возвращается вверх. Кнопка создается и добавляется в документ, а также настроен обработчик событий для управления её видимостью на основе положения прокрутки страницы. При клике на кнопку происходит плавный скролл к верху страницы.JavaScript: Скопировать в буфер обмена
Код:
const backToTopButton = document.createElement("button");
backToTopButton.textContent = "↑";
backToTopButton.classList.add("back-to-top");
backToTopButton.style.display = "none";
document.body.appendChild(backToTopButton);
window.addEventListener("scroll", () => {
if (window.pageYOffset > 100) {
backToTopButton.style.display = "block";
} else {
backToTopButton.style.display = "none";
}
});
backToTopButton.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
Анимации при прокрутке
Код также включает в себя анимации элементов при прокрутке страницы. ФункцияanimateOnScroll
проверяет видимость элементов с классами .ai-insights
. Если элемент виден на экране, к нему добавляется класс animate-in
, который управляет анимацией. Эта функция вызывается при прокрутке страницы и при загрузке страницы, чтобы обеспечить анимацию уже видимых элементов.JavaScript: Скопировать в буфер обмена
Код:
const animateOnScroll = () => {
const elements = document.querySelectorAll(" .ai-insights");
elements.forEach((element) => {
const elementTop = element.getBoundingClientRect().top;
const elementBottom = element.getBoundingClientRect().bottom;
if (elementTop < window.innerHeight && elementBottom > 0) {
element.classList.add("animate-in");
}
});
};
window.addEventListener("scroll", animateOnScroll);
animateOnScroll();
Валидация форм
Код также включает валидацию форм. При отправке формы скрипт проверяет все обязательные поля и добавляет класс error к тем полям, которые не заполнены. Это позволяет пользователю легко увидеть, какие поля требуют заполнения.JavaScript: Скопировать в буфер обмена
Код:
const forms = document.querySelectorAll("form");
forms.forEach((form) => {
form.addEventListener("submit", (e) => {
const requiredFields = form.querySelectorAll("[required]");
requiredFields.forEach((field) => {
if (!field.value) {
e.preventDefault();
field.classList.add("error");
} else {
field.classList.remove("error");
}
});
});
});
Управление навигационным меню
Для управления навигационным меню добавлен переключатель меню. Кнопка создается динамически и добавляется в DOM перед элементом списка навигации. При клике на эту кнопку меню переключается между показанным и скрытым состоянием. Срабатывает когда девайс мобильный телефон.JavaScript: Скопировать в буфер обмена
Код:
const menuToggle = document.createElement("button");
menuToggle.textContent = "☰";
menuToggle.classList.add("menu-toggle");
const nav = document.querySelector("nav ul");
nav.parentNode.insertBefore(menuToggle, nav);
menuToggle.addEventListener("click", () => {
nav.classList.toggle("show");
});
Предложение тегов
В конце файла добавляется функциональность для предложения тегов. К кнопке "Suggest Tags" привязан обработчик событий, который при нажатии отправляет запрос на сервер с заголовком и текстом вопроса. Полученные теги отображаются в поле ввода тегов.JavaScript: Скопировать в буфер обмена
Код:
const tagInput = document.getElementById("tags");
const suggestTagsButton = document.createElement("button");
suggestTagsButton.textContent = "Suggest Tags";
suggestTagsButton.classList.add("btn", "btn-secondary", "mt-2");
tagInput.parentNode.insertBefore(suggestTagsButton, tagInput.nextSibling);
suggestTagsButton.addEventListener("click", async (e) => {
e.preventDefault();
const title = document.getElementById("title").value;
const body = document.getElementById("body").value;
try {
const response = await fetch("/api/suggest_tags", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title, body }),
});
const data = await response.json();
tagInput.value = data.tags.join(", ");
} catch (error) {
console.error("Error suggesting tags:", error);
}
});
Выделение лучших ответов
Последний блок кода выделяет лучший ответ на вопрос, если он присутствует. Он ищет ответ с атрибутомdata-rank="1"
(декоративный) и добавляет к нему класс top-answer
, который может использоваться для особого оформления.JavaScript: Скопировать в буфер обмена
Код:
const topAnswer = document.querySelector('.answer[data-rank="1"]');
if (topAnswer) {
topAnswer.classList.add("top-answer");
}
Файл base.html
Файлbase.html
представляет собой основной шаблон для веб-приложения на платформе вопросов и ответов, построенной с использованием Flask. Он задает общий макет и стили для всех страниц приложения и обеспечивает единый вид и функциональность для всего сайта. Давайте подробно рассмотрим структуру и содержание этого шаблона. Разбор этого и следующего файла будут подробно объяснены, а по следующем будет краткий взгляд . Основные элементы шаблона
Шаблон начинается с объявления типа документа и мета-информации. Он определяет язык документа как английский и задает кодировку и масштабирование для мобильных устройств.HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Dev Q&A Platform{% endblock %}</title>
Эти метатеги обеспечивают правильное отображение на различных устройствах и задают кодировку документа.
Подключение стилей
В разделе<head>
подключаются CSS-файлы, которые определяют внешний вид различных компонентов и макета сайта. Используются как общие стили, так и более специализированные для различных элементов интерфейса.HTML: Скопировать в буфер обмена
Код:
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/base.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/components/_buttons.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/components/_forms.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/components/_navigation.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/components/_tags.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/components/_typography.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/components/_utilities.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/layout/_footer.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/layout/_header.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/layout/_main.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/utilities/_animations.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/utilities/_breakpoints.css') }}"
/>
</head>
Заголовок и навигация
В разделе<body>
определены элементы заголовка и навигации. В <header>
содержится навигационное меню и форма поиска.HTML: Скопировать в буфер обмена
Код:
<header>
<nav>
<ul>
<li><a href="{{ url_for('main.index') }}">Home</a></li>
{% if current_user.is_authenticated %}
<li>
<a href="{{ url_for('questions.ask_question') }}">Ask Question</a>
</li>
<li>
<a
href="{{ url_for('main.user_profile', username=current_user.username) }}"
>Profile</a
>
</li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
{% endif %}
</ul>
</nav>
<form
action="{{ url_for('main.search') }}"
method="get"
class="search-form"
>
<input type="text" name="q" placeholder="Search questions..." />
<button type="submit">Search</button>
</form>
</header>
Меню навигации меняется в зависимости от статуса аутентификации пользователя. Если пользователь вошел в систему, отображаются ссылки на страницы "Задать вопрос", "Профиль" и "Выход". Если нет, отображаются ссылки на "Вход" и "Регистрацию". Форма поиска позволяет пользователям искать вопросы по ключевым словам.
Основное содержимое
Основное содержимое страницы определяется в блоке{% block content %}
, который будет переопределен в других шаблонах, расширяющих этот базовый шаблон.HTML: Скопировать в буфер обмена
Код:
<main class="container">
{% with messages = get_flashed_messages() %} {% if messages %}
<div class="flashes">
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
</div>
{% endif %} {% endwith %} {% block content %}{% endblock %}
</main>
Функция
get_flashed_messages()
используется для отображения временных сообщений, таких как сообщения об успешных действиях или ошибках. Можно также украсить стилями, я решил оставить это как есть.Подвал сайта
В подвале (<footer>
) отображается навигационное меню с дополнительными ссылками и иконками социальных сетей.HTML: Скопировать в буфер обмена
Код:
<footer>
<div class="container">
<div class="footer-nav">
<ul>
<li><a href="{{ url_for('main.index') }}">Home</a></li>
<li><a href="#">About Us</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">FAQ</a></li>
</ul>
</div>
<div class="social-icons">
<a href="#"><i class="fab fa-facebook-f"></i></a>
<a href="#"><i class="fab fa-twitter"></i></a>
<a href="#"><i class="fab fa-linkedin-in"></i></a>
</div>
<p>© 2024 Dev Q&A Platform. All rights reserved.</p>
</div>
</footer>
Этот раздел предоставляет пользователям дополнительную информацию о сайте и ссылки на страницы, такие как "О нас", "Контакт" и "Часто задаваемые вопросы". Иконки социальных сетей позволяют пользователям быстро перейти на профили компании в социальных сетях.
Подключение скриптов
Наконец, перед закрывающим тегом</body>
подключается JavaScript-файл main.js
, который содержит клиентский код для взаимодействия с пользовательским интерфейсом. Разобрали его выше.HTML: Скопировать в буфер обмена
Код:
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
Этот файл добавляет функциональность, такую как прокрутка до верхней части страницы, анимация элементов при прокрутке и обработка форм.
Файл admin_dashboard.html
Файлadmin_dashboard.html
представляет собой шаблон для административной панели управления на платформе вопросов и ответов.. Этот шаблон расширяет базовый шаблон base.html
и отображает различные разделы административного интерфейса, включая управление пользователями, вопросами и тегами. Рассмотрим подробнее, как реализованы основные функции этого шаблона.Расширение базового шаблона
Шаблон начинается с расширения базового шаблонаbase.html
, который содержит общую структуру и стили для всего сайта. В блоке content определяется основное содержимое страницы административной панели.HTML: Скопировать в буфер обмена
Код:
{% extends "base.html" %}
{% block content %}
Управление пользователями
Первый раздел административной панели посвящен управлению пользователями. Здесь отображается таблица с пользователями, где указаны их имена, электронные адреса и роли. В колонке действий предоставлены кнопки для редактирования и удаления пользователей.HTML: Скопировать в буфер обмена
Код:
<h2>Users</h2>
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
<td>
<a
href="{{ url_for('admin.edit_user', user_id=user.id) }}"
class="btn btn-secondary"
>Edit</a
>
|
<form
action="{{ url_for('admin.delete_user', user_id=user.id) }}"
method="post"
style="display: inline"
>
<button
type="submit"
class="btn btn-danger"
onclick="return confirm('Are you sure you want to delete this user?')"
>
Delete
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Каждая строка таблицы представляет одного пользователя. Кнопка "Edit" перенаправляет на страницу редактирования пользователя, а форма с кнопкой "Delete" отправляет запрос на удаление пользователя после подтверждения действия.
Управление вопросами
Следующий раздел посвящен управлению вопросами. В этой таблице отображаются заголовки вопросов и авторы. Также предусмотрена форма для удаления вопросов.HTML: Скопировать в буфер обмена
Код:
<h2>Questions</h2>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for question in questions %}
<tr>
<td>{{ question.title }}</td>
<td>{{ question.user.username }}</td>
<td>
<form
action="{{ url_for('admin.delete_question', question_id=question.id) }}"
method="post"
style="display: inline"
>
<button
type="submit"
class="btn btn-danger"
onclick="return confirm('Are you sure you want to delete this question?')"
>
Delete
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Таблица вопросов аналогична таблице пользователей, где каждое действие сопровождается кнопкой удаления, требующей подтверждения от администратора.
Управление тегами
Последний раздел шаблона посвящен тегам. Здесь отображаются имена тегов и предоставлена возможность их удаления. В этом разделе также используется форма для отправки запроса на удаление, в которой добавлен CSRF-токен для защиты от подделки запросов.HTML: Скопировать в буфер обмена
Код:
<h2>Tags</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr>
<td>{{ tag.name }}</td>
<td>
<form
action="{{ url_for('admin.delete_tag', tag_id=tag.id) }}"
method="POST"
style="display: inline"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button
type="submit"
class="btn btn-danger"
onclick="return confirm('Are you sure?')"
>
Delete
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Таблица тегов позволяет администратору удалять теги с помощью кнопки "Delete", также требующей подтверждения.
Файл edit_user.html
Файлedit_user.html
используется для редактирования информации о пользователе. В блоке content отображается форма для обновления данных пользователя.HTML: Скопировать в буфер обмена
Код:
<!-- edit_user.html -->
{% extends "base.html" %}
{% block content %}
<h1>Edit User</h1>
<div class="form-container">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" value="{{ user.username }}" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" value="{{ user.email }}" required>
</div>
<div class="form-group">
<label for="role">Role:</label>
<select id="role" name="role">
<option value="user" {% if user.role == 'user' %}selected{% endif %}>User</option>
<option value="moderator" {% if user.role == 'moderator' %}selected{% endif %}>Moderator</option>
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
</select>
</div>
<button type="submit" class="btn">Update User</button>
</form>
<a href="{{ url_for('auth.admin_dashboard') }}" class="btn btn-secondary">Back to Dashboard</a>
</div>
{% endblock %}
Опять же, форма включает скрытое поле для CSRF-токена, что защищает от атак межсайтовой подделки запросов. Поля формы включают ввод для имени пользователя, электронной почты и выбора роли (пользователь, модератор или администратор). Эти данные будут обновлены в базе данных при отправке формы. Форма завершается кнопкой "Update User" для отправки данных и ссылкой для возврата на панель управления администратора.
Файл index.html
Файлindex.html
представляет собой главную страницу приложения, где пользователи могут увидеть последние вопросы.HTML: Скопировать в буфер обмена
Код:
<!-- index.html -->
{% extends "base.html" %} {% block content %}
<section class="hero">
<div class="container">
<h1>Find Solutions, Share Expertise</h1>
<p>
Connect with fellow developers, get answers, and level up your coding
skills.
</p>
<a href="{{ url_for('questions.ask_question') }}" class="btn"
>Ask a Question</a
>
</div>
</section>
<div class="container">
<h1>Latest Questions</h1>
<div class="question-list">
{% for question in questions %}
<div class="question-summary">
<h2>
<a
href="{{ url_for('questions.question_detail', question_id=question.id) }}"
>
{{ question.title }}
</a>
</h2>
<p>
{{ question.body[:200] }}{% if question.body|length > 200 %}...{% endif
%}
</p>
<div class="question-meta">
<span>Asked by {{ question.user.username }}</span>
<span>{{ question.timestamp.strftime('%Y-%m-%d %H:%M') }}</span>
<span>{{ question.views }} views</span>
<span>{{ question.votes }} votes</span>
</div>
<div class="question-tags">
{% for tag in question.tags %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
На главной странице представлена секция "hero" с приветственным сообщением и кнопкой для создания нового вопроса. Ниже идет список последних вопросов, каждый из которых отображается в виде краткого резюме. Включены ссылки на детали каждого вопроса, информация о пользователе, который задал вопрос, метка времени, количество просмотров и голосов. Теги, связанные с каждым вопросом, также отображаются, что помогает пользователям быстро понять тему вопроса и найти вопросы по интересующим их темам.
Файл login.html
Шаблонlogin.html
предназначен для входа в систему. Он содержит форму, в которую пользователи вводят свои учетные данные — имя пользователя и пароль. В форме также есть выпадающий список для выбора типа входа (пользователь, администратор или модератор), что позволяет системе идентифицировать и аутентифицировать пользователя.HTML: Скопировать в буфер обмена
Код:
<!-- login.html -->
{% extends "base.html" %} {% block content %}
<div class="container">
<div class="form-container">
<h1>Login</h1>
<form method="POST" action="{{ url_for('auth.login') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required />
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required />
</div>
<div class="form-group">
<label for="login_type">Login as:</label>
<select id="login_type" name="login_type" required>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="moderator">Moderator</option>
</select>
</div>
<button type="submit">Login</button>
</form>
<p class="text-center">
Don't have an account?
<a href="{{ url_for('auth.register') }}">Register here</a>
</p>
</div>
</div>
{% endblock %}
И снова, форма включает CSRF-токен для безопасности и кнопку "Login" для отправки данных. Внизу страницы размещена ссылка для регистрации, если у пользователя еще нет аккаунта. Маленький трюк, который помогает новым пользователям быстро найти способ создания аккаунта, или просто улучшает UX.
Файл register.html
Файлregister.html
предназначен для регистрации новых пользователей. Этот шаблон содержит форму для ввода имени пользователя, электронной почты, пароля и подтверждения пароля. Также включен CSRF-токен для защиты от атак. После успешной регистрации пользователь сможет войти в систему, используя свои учетные данные. Внизу страницы предоставлена ссылка для перехода на страницу входа, если у пользователя уже есть аккаунт.HTML: Скопировать в буфер обмена
Код:
<!-- register.html -->
{% extends "base.html" %} {% block content %}
<div class="container">
<div class="form-container">
<h1>Register</h1>
<form method="POST" action="{{ url_for('auth.register') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required />
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required />
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password:</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
required
/>
</div>
<button type="submit">Register</button>
</form>
<p class="text-center">
Already have an account?
<a href="{{ url_for('auth.login') }}">Login here</a>
</p>
</div>
</div>
{% endblock %}
Файл moderate.html
Файлmoderate.html
предназначен для панели модерации, где модераторы могут просматривать и управлять сообщениями, которые были пожалованы пользователями.HTML: Скопировать в буфер обмена
Код:
<!-- moderate.html -->
{% extends "base.html" %} {% block content %}
<h1>Moderation Panel</h1>
<h2>Reported Questions</h2>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Reported By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for question in reported_questions %}
<tr>
<td>{{ question.title }}</td>
<td>{{ question.user.username }}</td>
<td>
{% if question.reported_by_user %} {{ question.reported_by_user.username
}} {% else %} Not reported {% endif %}
</td>
<td>
<form
action="{{ url_for('moderator.approve_question', question_id=question.id) }}"
method="post"
style="display: inline"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-success">Approve</button>
</form>
|
<form
action="{{ url_for('moderator.delete_question', question_id=question.id) }}"
method="post"
style="display: inline"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button
type="submit"
class="btn btn-danger"
onclick="return confirm('Are you sure you want to delete this question?')"
>
Delete
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
В содержимом шаблона сначала отображается заголовок "Moderation Panel" и таблица с сообщениями, которые были пожалованы. Таблица содержит такие колонки, как заголовок вопроса, автор, кто пожаловался и действия. В каждой строке таблицы указана информация о вопросе и пользователе, который его пожаловался.
Кнопки для действий включают "Approve" и "Delete". Эти кнопки представлены в виде форм, отправляющих POST-запросы к соответствующим маршрутам для утверждения или удаления вопроса.
Файл question_detail.html
Шаблонquestion_detail.html
предназначен для отображения подробностей конкретного вопроса. Здесь представлены кнопки для голосования за вопрос, которые отправляют POST-запросы для увеличения или уменьшения рейтинга вопроса. Также отображается информация о вопросе, такая как заголовок, тело, теги и метаданные, включая автора и дату.HTML: Скопировать в буфер обмена
Код:
<!-- question_detail.html -->
{% extends "base.html" %} {% block content %}
<div class="container">
<div class="question-detail">
<div class="content-wrapper">
<div class="vote-buttons">
<form
action="{{ url_for('questions.vote_question', question_id=question.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" name="vote_type" value="up">+</button>
</form>
<span>{{ question.votes }}</span>
<form
action="{{ url_for('questions.vote_question', question_id=question.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" name="vote_type" value="down">-</button>
</form>
</div>
<div class="question-content">
<div class="question-header">
<h1>{{ question.title }}</h1>
<div class="question-meta">
<span>Asked by {{ question.user.username }}</span>
<span>{{ question.timestamp.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
</div>
<div class="question-body">{{ question.body }}</div>
<div class="question-tags">
{% for tag in question.tags %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
</div>
<form
action="{{ url_for('auth.report_question', question_id=question.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-warning">Report Question</button>
</form>
</div>
</div>
<div class="ai-insights">
<h2 class="section-title">AI Insights</h2>
<div class="insights-content">
{{ ai_insights|default('No AI insights available at the moment.', true)
}}
</div>
</div>
<div class="ai-summary">
<h2 class="section-title">AI-Generated Summary</h2>
<div class="summary-content">
{% if question.summary %} {{ question.summary }} {% else %} No
AI-generated summary available at the moment. {% endif %}
</div>
</div>
<div class="similar-questions">
<h2 class="section-title">Similar Questions</h2>
{% if similar_questions %}
<ul>
{% for similar_question in similar_questions %}
<li>
<a
href="{{ url_for('questions.question_detail', question_id=similar_question.id) }}"
>{{ similar_question.title }}</a
>
</li>
{% endfor %}
</ul>
{% else %}
<p>No similar questions found.</p>
{% endif %}
</div>
<div class="answers">
<h2 class="section-title">{{ question.answers.count() }} Answers</h2>
{% for answer in ranked_answers %}
<div class="answer">
<div class="vote-buttons">
<form
action="{{ url_for('answers.vote_answer', answer_id=answer.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" name="vote_type" value="up">+</button>
</form>
<span>{{ answer.votes }}</span>
<form
action="{{ url_for('answers.vote_answer', answer_id=answer.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" name="vote_type" value="down">-</button>
</form>
</div>
<div class="answer-body">{{ answer.body }}</div>
<div class="answer-meta">
{% if current_user == question.author and not answer.is_accepted %}
<form
action="{{ url_for('answers.accept_answer', answer_id=answer.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-success">Accept Answer</button>
</form>
{% endif %} {% if answer.is_accepted %}
<span class="accepted">Accepted Answer</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<div class="post-answer">
<h2 class="section-title">Your Answer</h2>
<form
action="{{ url_for('answers.post_answer', question_id=question.id) }}"
method="post"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<textarea
name="answer_body"
rows="6"
placeholder="Write your answer here..."
required
></textarea>
<button type="submit" class="btn btn-primary">Post Your Answer</button>
</form>
</div>
</div>
</div>
{% endblock %}
Особое внимание уделяется разделу "AI Insights", который отображает результаты анализа вопроса с использованием ИИ. Этот раздел может содержать полезные замечания или рекомендации, предоставляемые ИИ на основе анализа вопроса.
Раздел "AI-Generated Summary" показывает автоматически сгенерированное резюме вопроса, если таковое имеется. В блоке "Similar Questions" представлены вопросы, похожие на текущий, что позволяет пользователям находить связанные темы и расширяет возможности поиска.
Кроме того, пользователи могут просматривать и голосовать за ответы на вопрос. Под каждым ответом представлены кнопки для голосования и возможностью отметить ответ как принятый, если пользователь, задавший вопрос, его одобрил. Форма для добавления собственного ответа также включена в этот шаблон.
Файл search_results.html
Файлsearch_results.html
отображает результаты поиска по запросу. Он показывает заголовки вопросов, соответствующих запросу, вместе с кратким описанием и метаданными, такими как количество просмотров и голосов.HTML: Скопировать в буфер обмена
Код:
<!-- search_results.html -->
{% extends "base.html" %} {% block content %}
<h1>Search Results for "{{ query }}"</h1>
<div class="search-results">
{% if questions %} {% for question in questions %}
<div class="question-summary">
<h2>
<a
href="{{ url_for('questions.question_detail', question_id=question.id) }}"
>{{ question.title }}</a
>
</h2>
<p>
{{ question.body[:200] }}{% if question.body|length > 200 %}...{% endif %}
</p>
<div class="question-meta">
<span>Asked by {{ question.user.username }}</span>
<span>{{ question.timestamp.strftime('%Y-%m-%d %H:%M') }}</span>
<span>{{ question.views }} views</span>
<span>{{ question.votes }} votes</span>
</div>
<div class="question-tags">
{% for tag in question.tags %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
</div>
</div>
{% endfor %} {% else %}
<p>No results found for "{{ query }}".</p>
{% endif %}
</div>
{% endblock %}
Каждый вопрос представлен в виде ссылки, ведущей к подробной странице вопроса. Этот шаблон предоставляет пользователю возможность быстро просмотреть результаты поиска и выбрать интересующие вопросы. Если результаты поиска отсутствуют, пользователю будет показано сообщение об этом.
Файл user_profile.html
Шаблонuser_profile.html
предназначен для отображения профиля пользователя. Он включает аватар пользователя, основную информацию, такую как имя пользователя, email и репутацию.HTML: Скопировать в буфер обмена
Код:
<!-- user_profile.html -->
{% extends "base.html" %} {% block content %}
<div class="container">
<div class="profile-header">
<img
src="{{ url_for('static', filename='images/default-avatar.jpg') }}"
alt="{{ user.username }}'s Avatar"
class="avatar"
/>
<div class="user-info">
<h1>{{ user.username }}</h1>
<p>Email: {{ user.email }}</p>
<p class="reputation">Reputation: {{ user.reputation }}</p>
</div>
</div>
<div class="user-questions">
<h2 class="section-title">Questions ({{ questions.items|length }})</h2>
{% if questions.items %} {% for question in questions.items %}
<div class="question-summary">
<h3>
<a
href="{{ url_for('questions.question_detail', question_id=question.id) }}"
>{{ question.title }}</a
>
</h3>
<p>
{{ question.body[:200] }}{% if question.body|length > 200 %}...{% endif
%}
</p>
<div class="question-meta">
<span>{{ question.timestamp.strftime('%Y-%m-%d %H:%M') }}</span>
<span>{{ question.views }} views</span>
<span>{{ question.votes }} votes</span>
</div>
</div>
{% endfor %} {% else %}
<p class="no-contributions">This user hasn't asked any questions yet.</p>
{% endif %}
</div>
<div class="user-answers">
<h2 class="section-title">Answers ({{ answers.items|length }})</h2>
{% if answers.items %} {% for answer in answers.items %}
<div class="answer-summary">
<h3>
<a
href="{{ url_for('questions.question_detail', question_id=answer.question.id) }}"
>{{ answer.question.title }}</a
>
</h3>
<p>
{{ answer.body[:200] }}{% if answer.body|length > 200 %}...{% endif %}
</p>
<div class="answer-meta">
<span>{{ answer.timestamp.strftime('%Y-%m-%d %H:%M') }}</span>
<span>{{ answer.votes }} votes</span>
{% if answer.is_accepted %}
<span class="accepted">Accepted Answer</span>
{% endif %}
</div>
</div>
{% endfor %} {% else %}
<p class="no-contributions">This user hasn't posted any answers yet.</p>
{% endif %}
</div>
</div>
{% endblock %}
Далее представлены разделы с вопросами и ответами пользователя. Каждый вопрос и ответ отображаются в виде краткого описания с ссылкой на полное содержание. В блоке "user-questions" показываются вопросы, заданные пользователем, а в блоке "user-answers" — ответы, которые он оставил. Если у пользователя нет вопросов или ответов, отображаются соответствующие сообщения.
Заключение
Друзья, огромное спасибо всем, кто дочитал статью до конца! Тема ИИ и веб-разработки действительно интересна, но написание проектов и, тем более, качественно оформленных статей требует ещё больше времени. Не забудьте также создать вашу дб и таблицы, перед тем как запускать код. Самые внимательные могли заметить, что не каждая функция реализована на клиенте, я лишь создал простой и приятный шаблон, вы сами можете "закончить" добавив это. Буду благодарен за фидбек. Если обнаружите баги или недочёты, пожалуйста, сообщите, и я постараюсь их исправить. Всем добра!