diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0eaa111..cb58b47b 100644
--- a/CHANGELOG.md
+++ b/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
### Оптимизация инфраструктуры
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..6a9db8a5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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! 🙏
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..d2071f96
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index 0d17826e..a61b1f46 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,28 @@
# GraphQL API Backend
-Backend service providing GraphQL API for content management system with reactions, ratings and comments.
+
-## Core Features
+
+
+
+
+
+
+
+
+
+
+
+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
+
+
+
+
+
+
+
+
+
+
+## 🤝 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)
+
+---
+
+
+
+**Made with ❤️ by the Discours Team**
+
+
+
+
+
diff --git a/docs/README.md b/docs/README.md
index c0f6f1be..3429ecec 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -33,7 +33,7 @@ python dev.py
- [API методы](api.md) - GraphQL эндпоинты
- [Функции системы](features.md) - Полный список возможностей
-## ⚡ Ключевые возможности (v0.5.4)
+## ⚡ Ключевые возможности
### Авторизация
- **Модульная архитектура**: SessionTokenManager, VerificationTokenManager, OAuthTokenManager
diff --git a/docs/features.md b/docs/features.md
index 682a3b57..d22e286a 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -95,3 +95,22 @@
- **Дополнительные мутации**:
- confirmEmailChange
- cancelEmailChange
+
+## Система featured публикаций
+
+- **Автоматическое получение статуса featured**:
+ - Публикация получает статус featured при более чем 4 лайках от авторов с featured статьями
+ - Проверка квалификации автора: наличие опубликованных featured статей
+ - Логирование процесса для отладки и мониторинга
+- **Условия удаления с главной (unfeatured)**:
+ - **Условие 1**: Менее 5 голосов "за" (положительные реакции)
+ - **Условие 2**: 20% или более отрицательных реакций от общего количества голосов
+ - Проверка выполняется только для уже featured публикаций
+- **Оптимизированная логика обработки**:
+ - Проверка unfeatured имеет приоритет над featured при обработке реакций
+ - Автоматическая проверка условий при добавлении/удалении реакций
+ - Корректная обработка типов данных в функциях проверки
+- **Интеграция с системой реакций**:
+ - Обработка в `create_reaction` для новых реакций
+ - Обработка в `delete_reaction` для удаленных реакций
+ - Учет только реакций на саму публикацию (не на комментарии)
diff --git a/requirements.txt b/requirements.txt
index 8e452a2a..ecd287d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,6 @@ bcrypt
PyJWT
authlib
passlib==1.7.4
-opensearch-py
google-analytics-data
colorlog
psycopg2-binary
diff --git a/resolvers/reaction.py b/resolvers/reaction.py
index 355431e7..f4af7ca0 100644
--- a/resolvers/reaction.py
+++ b/resolvers/reaction.py
@@ -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()