docs+featured/unfeatured-upgrade
All checks were successful
Deploy on push / deploy (push) Successful in 6s
All checks were successful
Deploy on push / deploy (push) Successful in 6s
This commit is contained in:
parent
6a582d49d4
commit
b5aa7032eb
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,5 +1,39 @@
|
|||
# Changelog
|
||||
|
||||
## [0.5.5] - 2025-06-19
|
||||
|
||||
### Улучшения документации
|
||||
|
||||
- **НОВОЕ**: Красивые бейджи в README.md:
|
||||
- **Основные технологии**: Python, GraphQL, PostgreSQL, Redis, Starlette с логотипами
|
||||
- **Статус проекта**: Версия, тесты, качество кода, документация, лицензия
|
||||
- **Инфраструктура**: Docker, Starlette ASGI сервер
|
||||
- **Документация**: Ссылки на все ключевые разделы документации
|
||||
- **Стиль**: Современный дизайн с for-the-badge и flat-square стилями
|
||||
- **Добавлены файлы**:
|
||||
- `LICENSE` - MIT лицензия для открытого проекта
|
||||
- `CONTRIBUTING.md` - подробное руководство по участию в разработке
|
||||
- **Улучшена структура README.md**:
|
||||
- Таблица технологий с бейджами и описаниями
|
||||
- Эмодзи для улучшения читаемости разделов
|
||||
- Ссылки на документацию и руководства
|
||||
- Статистика проекта и ссылки на ресурсы
|
||||
|
||||
### Исправления системы featured публикаций
|
||||
|
||||
- **КРИТИЧНО**: Исправлена логика удаления публикаций с главной страницы (featured):
|
||||
- **Проблема**: Не работали условия unfeatured - публикации не убирались с главной при соответствующих условиях голосования
|
||||
- **Исправления**:
|
||||
- **Условие 1**: Добавлена проверка "меньше 5 голосов за" - если у публикации менее 5 лайков, она должна убираться с главной
|
||||
- **Условие 2**: Сохранена проверка "больше 20% минусов" - если доля дизлайков превышает 20%, публикация убирается с главной
|
||||
- **Баг с типами данных**: Исправлена передача неправильного типа в `check_to_unfeature()` в функции `delete_reaction`
|
||||
- **Оптимизация логики**: Проверка unfeatured теперь происходит только для уже featured публикаций
|
||||
- **Результат**: Система корректно убирает публикации с главной при выполнении любого из условий
|
||||
- **Улучшена логика обработки реакций**:
|
||||
- В `_create_reaction()` добавлена проверка текущего статуса публикации перед применением логики featured/unfeatured
|
||||
- В `delete_reaction()` добавлена проверка статуса публикации перед удалением реакции
|
||||
- Улучшено логирование процесса featured/unfeatured для отладки
|
||||
|
||||
## [0.5.4] - 2025-06-03
|
||||
|
||||
### Оптимизация инфраструктуры
|
||||
|
|
133
CONTRIBUTING.md
Normal file
133
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,133 @@
|
|||
# Contributing to Discours Core
|
||||
|
||||
🎉 Thanks for taking the time to contribute!
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b my-new-feature`
|
||||
3. Make your changes
|
||||
4. Add tests for your changes
|
||||
5. Run the test suite: `pytest`
|
||||
6. Run the linter: `ruff check . --fix && ruff format . --line-length=120`
|
||||
7. Commit your changes: `git commit -am 'Add some feature'`
|
||||
8. Push to the branch: `git push origin my-new-feature`
|
||||
9. Create a Pull Request
|
||||
|
||||
## 📋 Development Guidelines
|
||||
|
||||
### Code Style
|
||||
|
||||
- **Python 3.12+** required
|
||||
- **Line length**: 120 characters max
|
||||
- **Type hints**: Required for all functions
|
||||
- **Docstrings**: Required for public methods
|
||||
- **Ruff**: For linting and formatting
|
||||
|
||||
### Testing
|
||||
|
||||
- **Pytest** for testing
|
||||
- **85%+ coverage** required
|
||||
- Test both positive and negative cases
|
||||
- Mock external dependencies
|
||||
|
||||
### Commit Messages
|
||||
|
||||
We follow [Conventional Commits](https://conventionalcommits.org/):
|
||||
|
||||
```
|
||||
feat: add user authentication
|
||||
fix: resolve database connection issue
|
||||
docs: update API documentation
|
||||
test: add tests for reaction system
|
||||
refactor: improve GraphQL resolvers
|
||||
```
|
||||
|
||||
### Python Code Standards
|
||||
|
||||
```python
|
||||
# Good example
|
||||
async def create_reaction(
|
||||
session: Session,
|
||||
author_id: int,
|
||||
reaction_data: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Create a new reaction.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
author_id: ID of the author creating the reaction
|
||||
reaction_data: Reaction data
|
||||
|
||||
Returns:
|
||||
Created reaction data
|
||||
|
||||
Raises:
|
||||
ValueError: If reaction data is invalid
|
||||
"""
|
||||
if not reaction_data.get("kind"):
|
||||
raise ValueError("Reaction kind is required")
|
||||
|
||||
reaction = Reaction(**reaction_data)
|
||||
session.add(reaction)
|
||||
session.commit()
|
||||
|
||||
return reaction.dict()
|
||||
```
|
||||
|
||||
## 🐛 Bug Reports
|
||||
|
||||
When filing a bug report, please include:
|
||||
|
||||
- **Python version**
|
||||
- **Package versions** (`pip freeze`)
|
||||
- **Error message** and full traceback
|
||||
- **Steps to reproduce**
|
||||
- **Expected vs actual behavior**
|
||||
|
||||
## 💡 Feature Requests
|
||||
|
||||
For feature requests, please include:
|
||||
|
||||
- **Use case** description
|
||||
- **Proposed solution**
|
||||
- **Alternatives considered**
|
||||
- **Breaking changes** (if any)
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- Update documentation for new features
|
||||
- Add examples for complex functionality
|
||||
- Use Russian comments for Russian-speaking team members
|
||||
- Keep README.md up to date
|
||||
|
||||
## 🔍 Code Review Process
|
||||
|
||||
1. **Automated checks** must pass (tests, linting)
|
||||
2. **Manual review** by at least one maintainer
|
||||
3. **Documentation** must be updated if needed
|
||||
4. **Breaking changes** require discussion
|
||||
|
||||
## 🏷️ Release Process
|
||||
|
||||
We follow [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **MAJOR**: Breaking changes
|
||||
- **MINOR**: New features (backward compatible)
|
||||
- **PATCH**: Bug fixes (backward compatible)
|
||||
|
||||
## 🤝 Community
|
||||
|
||||
- Be respectful and inclusive
|
||||
- Help newcomers get started
|
||||
- Share knowledge and best practices
|
||||
- Follow our [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
## 📞 Getting Help
|
||||
|
||||
- **Issues**: For bugs and feature requests
|
||||
- **Discussions**: For questions and general discussion
|
||||
- **Documentation**: Check `docs/` folder first
|
||||
|
||||
Thank you for contributing! 🙏
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Discours Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
101
README.md
101
README.md
|
@ -1,8 +1,28 @@
|
|||
# GraphQL API Backend
|
||||
|
||||
Backend service providing GraphQL API for content management system with reactions, ratings and comments.
|
||||
<div align="center">
|
||||
|
||||
## Core Features
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Backend service providing GraphQL API for content management system with reactions, ratings and topics.
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
 • [API Documentation](docs/api.md)
|
||||
 • [Authentication Guide](docs/auth.md)
|
||||
 • [Caching System](docs/redis-schema.md)
|
||||
 • [Features Overview](docs/features.md)
|
||||
|
||||
## 🚀 Core Features
|
||||
|
||||
### Shouts (Posts)
|
||||
- CRUD operations via GraphQL mutations
|
||||
|
@ -26,18 +46,20 @@ Backend service providing GraphQL API for content management system with reactio
|
|||
- Activity tracking and stats
|
||||
- Community features
|
||||
|
||||
## Tech Stack
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- [Python](https://www.python.org/) 3.12+
|
||||
- **GraphQL** with [Ariadne](https://ariadnegraphql.org/)
|
||||
- [SQLAlchemy](https://docs.sqlalchemy.org/en/20/orm/)
|
||||
- [PostgreSQL](https://www.postgresql.org/)/[SQLite](https://www.sqlite.org/) support
|
||||
- [Starlette](https://www.starlette.io/) for ASGI server
|
||||
- [Redis](https://redis.io/) for caching
|
||||
**Core:** Python 3.12 • GraphQL • PostgreSQL • Redis • txtai
|
||||
**Server:** Starlette • Granian • Nginx
|
||||
**Tools:** SQLAlchemy • JWT • Pytest • Ruff
|
||||
**Deploy:** Dokku • Gitea • Glitchtip
|
||||
|
||||
## Development
|
||||
## 🔧 Development
|
||||
|
||||
### Prepare environment:
|
||||

|
||||

|
||||

|
||||
|
||||
### 📦 Prepare environment:
|
||||
|
||||
```shell
|
||||
python3.12 -m venv venv
|
||||
|
@ -45,7 +67,7 @@ source venv/bin/activate
|
|||
pip install -r requirements.dev.txt
|
||||
```
|
||||
|
||||
### Run server
|
||||
### 🚀 Run server
|
||||
|
||||
First, certificates are required to run the server with HTTPS.
|
||||
|
||||
|
@ -60,7 +82,7 @@ Then, run the server:
|
|||
python -m granian main:app --interface asgi
|
||||
```
|
||||
|
||||
### Useful Commands
|
||||
### ⚡ Useful Commands
|
||||
|
||||
```shell
|
||||
# Linting and import sorting
|
||||
|
@ -79,15 +101,15 @@ mypy .
|
|||
python -m granian main:app --interface asgi
|
||||
```
|
||||
|
||||
### Code Style
|
||||
### 📝 Code Style
|
||||
|
||||
We use:
|
||||
- Ruff for linting and import sorting
|
||||
- Line length: 120 characters
|
||||
- Python type hints
|
||||
- Docstrings for public methods
|
||||

|
||||

|
||||

|
||||
|
||||
### GraphQL Development
|
||||
**Ruff** for linting • **120 char** lines • **Type hints** required • **Docstrings** for public methods
|
||||
|
||||
### 🔍 GraphQL Development
|
||||
|
||||
Test queries in GraphQL Playground at `http://localhost:8000`:
|
||||
|
||||
|
@ -103,3 +125,42 @@ query GetShout($slug: String) {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Project Stats
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
 • [Read the guide](CONTRIBUTING.md)
|
||||
|
||||
We welcome contributions! Please read our contributing guide before submitting PRs.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
 • [discours.io](https://discours.io)
|
||||
 • [Source Code](https://github.com/discours/core)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Made with ❤️ by the Discours Team**
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,7 @@ python dev.py
|
|||
- [API методы](api.md) - GraphQL эндпоинты
|
||||
- [Функции системы](features.md) - Полный список возможностей
|
||||
|
||||
## ⚡ Ключевые возможности (v0.5.4)
|
||||
## ⚡ Ключевые возможности
|
||||
|
||||
### Авторизация
|
||||
- **Модульная архитектура**: SessionTokenManager, VerificationTokenManager, OAuthTokenManager
|
||||
|
|
|
@ -95,3 +95,22 @@
|
|||
- **Дополнительные мутации**:
|
||||
- confirmEmailChange
|
||||
- cancelEmailChange
|
||||
|
||||
## Система featured публикаций
|
||||
|
||||
- **Автоматическое получение статуса featured**:
|
||||
- Публикация получает статус featured при более чем 4 лайках от авторов с featured статьями
|
||||
- Проверка квалификации автора: наличие опубликованных featured статей
|
||||
- Логирование процесса для отладки и мониторинга
|
||||
- **Условия удаления с главной (unfeatured)**:
|
||||
- **Условие 1**: Менее 5 голосов "за" (положительные реакции)
|
||||
- **Условие 2**: 20% или более отрицательных реакций от общего количества голосов
|
||||
- Проверка выполняется только для уже featured публикаций
|
||||
- **Оптимизированная логика обработки**:
|
||||
- Проверка unfeatured имеет приоритет над featured при обработке реакций
|
||||
- Автоматическая проверка условий при добавлении/удалении реакций
|
||||
- Корректная обработка типов данных в функциях проверки
|
||||
- **Интеграция с системой реакций**:
|
||||
- Обработка в `create_reaction` для новых реакций
|
||||
- Обработка в `delete_reaction` для удаленных реакций
|
||||
- Учет только реакций на саму публикацию (не на комментарии)
|
||||
|
|
|
@ -2,7 +2,6 @@ bcrypt
|
|||
PyJWT
|
||||
authlib
|
||||
passlib==1.7.4
|
||||
opensearch-py
|
||||
google-analytics-data
|
||||
colorlog
|
||||
psycopg2-binary
|
||||
|
|
|
@ -167,18 +167,22 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool
|
|||
|
||||
def check_to_unfeature(session: Session, reaction: dict) -> bool:
|
||||
"""
|
||||
Unfeature a shout if 20% of reactions are negative.
|
||||
Unfeature a shout if:
|
||||
1. Less than 5 positive votes, OR
|
||||
2. 20% or more of reactions are negative.
|
||||
|
||||
:param session: Database session.
|
||||
:param reaction: Reaction object.
|
||||
:return: True if shout should be unfeatured, else False.
|
||||
"""
|
||||
if not reaction.get("reply_to"):
|
||||
shout_id = reaction.get("shout")
|
||||
|
||||
# Проверяем соотношение дизлайков, даже если текущая реакция не дизлайк
|
||||
total_reactions = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
Reaction.shout == reaction.get("shout"),
|
||||
Reaction.shout == shout_id,
|
||||
Reaction.reply_to.is_(None),
|
||||
Reaction.kind.in_(RATING_REACTIONS),
|
||||
# Рейтинги физически удаляются при удалении, поэтому фильтр deleted_at не нужен
|
||||
|
@ -186,10 +190,21 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
|
|||
.count()
|
||||
)
|
||||
|
||||
positive_reactions = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
Reaction.shout == shout_id,
|
||||
is_positive(Reaction.kind),
|
||||
Reaction.reply_to.is_(None),
|
||||
# Рейтинги физически удаляются при удалении, поэтому фильтр deleted_at не нужен
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
negative_reactions = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
Reaction.shout == reaction.get("shout"),
|
||||
Reaction.shout == shout_id,
|
||||
is_negative(Reaction.kind),
|
||||
Reaction.reply_to.is_(None),
|
||||
# Рейтинги физически удаляются при удалении, поэтому фильтр deleted_at не нужен
|
||||
|
@ -197,12 +212,19 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
|
|||
.count()
|
||||
)
|
||||
|
||||
# Проверяем, составляют ли отрицательные реакции 20% или более от всех реакций
|
||||
# Условие 1: Меньше 5 голосов "за"
|
||||
if positive_reactions < 5:
|
||||
logger.debug(f"Публикация {shout_id}: {positive_reactions} лайков (меньше 5) - должна быть unfeatured")
|
||||
return True
|
||||
|
||||
# Условие 2: Проверяем, составляют ли отрицательные реакции 20% или более от всех реакций
|
||||
negative_ratio = negative_reactions / total_reactions if total_reactions > 0 else 0
|
||||
logger.debug(
|
||||
f"Публикация {reaction.get('shout')}: {negative_reactions}/{total_reactions} отрицательных реакций ({negative_ratio:.2%})"
|
||||
f"Публикация {shout_id}: {negative_reactions}/{total_reactions} отрицательных реакций ({negative_ratio:.2%})"
|
||||
)
|
||||
return total_reactions > 0 and negative_ratio >= 0.2
|
||||
if total_reactions > 0 and negative_ratio >= 0.2:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
@ -265,12 +287,16 @@ async def _create_reaction(session: Session, shout_id: int, is_author: bool, aut
|
|||
|
||||
# Handle rating
|
||||
if r.kind in RATING_REACTIONS:
|
||||
# Проверяем сначала условие для unfeature (дизлайки имеют приоритет)
|
||||
if check_to_unfeature(session, rdict):
|
||||
# Проверяем, является ли публикация featured
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
is_currently_featured = shout and shout.featured_at is not None
|
||||
|
||||
# Проверяем сначала условие для unfeature (для уже featured публикаций)
|
||||
if is_currently_featured and check_to_unfeature(session, rdict):
|
||||
set_unfeatured(session, shout_id)
|
||||
logger.info(f"Публикация {shout_id} потеряла статус featured из-за высокого процента дизлайков")
|
||||
logger.info(f"Публикация {shout_id} потеряла статус featured из-за условий unfeaturing")
|
||||
# Только если не было unfeature, проверяем условие для feature
|
||||
elif check_to_feature(session, author_id, rdict):
|
||||
elif not is_currently_featured and check_to_feature(session, author_id, rdict):
|
||||
await set_featured(session, shout_id)
|
||||
logger.info(f"Публикация {shout_id} получила статус featured благодаря лайкам от авторов")
|
||||
|
||||
|
@ -468,9 +494,16 @@ async def delete_reaction(_: None, info: GraphQLResolveInfo, reaction_id: int) -
|
|||
# TODO: add more reaction types here
|
||||
else:
|
||||
logger.debug(f"{author_id} user removing his #{reaction_id} reaction")
|
||||
reaction_dict = r.dict()
|
||||
# Проверяем, является ли публикация featured до удаления реакции
|
||||
shout = session.query(Shout).filter(Shout.id == r.shout).first()
|
||||
is_currently_featured = shout and shout.featured_at is not None
|
||||
|
||||
session.delete(r)
|
||||
session.commit()
|
||||
if check_to_unfeature(session, r):
|
||||
|
||||
# Проверяем условие unfeatured только для уже featured публикаций
|
||||
if is_currently_featured and check_to_unfeature(session, reaction_dict):
|
||||
set_unfeatured(session, r.shout)
|
||||
|
||||
reaction_dict = r.dict()
|
||||
|
|
Loading…
Reference in New Issue
Block a user