From ab65fd4fd8f6952e5ac890655e949b0989f373cf Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 30 Jun 2025 22:43:32 +0300 Subject: [PATCH] schema-fix --- README.md | 4 +- auth/oauth.py | 10 +++- panel/graphql/queries.ts | 89 ++++++++++++++---------------- resolvers/admin.py | 116 +++++++++++++++++++++++++++++---------- resolvers/author.py | 6 +- resolvers/collection.py | 6 +- settings.py | 18 +++++- 7 files changed, 163 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index a61b1f46..6235db6c 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ Backend service providing GraphQL API for content management system with reactio ## πŸ› οΈ Tech Stack -**Core:** Python 3.12 β€’ GraphQL β€’ PostgreSQL β€’ Redis β€’ txtai +**Core:** Python 3.12 β€’ GraphQL β€’ PostgreSQL β€’ SQLAlchemy β€’ JWT β€’ Redis β€’ txtai **Server:** Starlette β€’ Granian β€’ Nginx -**Tools:** SQLAlchemy β€’ JWT β€’ Pytest β€’ Ruff +**Tools:** Pytest β€’ MyPy β€’ Ruff **Deploy:** Dokku β€’ Gitea β€’ Glitchtip ## πŸ”§ Development diff --git a/auth/oauth.py b/auth/oauth.py index f38ffeaa..337fd6e7 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -346,7 +346,11 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse: "provider": provider, "profile": profile, }, - username=author.name, + username=author.name + if isinstance(author.name, str) + else str(author.name) + if author.name is not None + else None, device_info={ "user_agent": request.headers.get("user-agent"), "ip": request.client.host if hasattr(request, "client") else None, @@ -357,9 +361,11 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse: state = request.query_params.get("state") state_data = await get_oauth_state(state) if state else None redirect_uri = state_data.get("redirect_uri") if state_data else FRONTEND_URL + if not isinstance(redirect_uri, str) or not redirect_uri: + redirect_uri = FRONTEND_URL # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΎΡ‚Π²Π΅Ρ‚ с Ρ€Π΅Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΠΌ - response = RedirectResponse(url=redirect_uri) + response = RedirectResponse(url=str(redirect_uri)) # УстанавливаСм cookie с сСссиСй response.set_cookie( diff --git a/panel/graphql/queries.ts b/panel/graphql/queries.ts index a493170e..28461f67 100644 --- a/panel/graphql/queries.ts +++ b/panel/graphql/queries.ts @@ -17,10 +17,11 @@ export const ADMIN_GET_SHOUTS_QUERY: string = cover cover_caption media { - type url + title body - caption + source + pic } seo created_at @@ -132,24 +133,22 @@ export const GET_COMMUNITIES_QUERY: string = gql` query GetCommunities { get_communities_all { - communities { + id + slug + name + desc + pic + created_at + created_by { id - slug name - desc - pic - created_at - created_by { - id - name - email - slug - } - stat { - shouts - followers - authors - } + email + slug + } + stat { + shouts + followers + authors } } } @@ -159,23 +158,21 @@ export const GET_TOPICS_QUERY: string = gql` query GetTopics { get_topics_all { - topics { - id - slug - title - body - pic - community - parent_ids - stat { - shouts - followers - authors - comments - } - oid - is_main + id + slug + title + body + pic + community + parent_ids + stat { + shouts + followers + authors + comments } + oid + is_main } } `.loc?.source.body || '' @@ -184,21 +181,19 @@ export const GET_COLLECTIONS_QUERY: string = gql` query GetCollections { get_collections_all { - collections { + id + slug + title + desc + pic + amount + published_at + created_at + created_by { id + name + email slug - title - desc - pic - amount - published_at - created_at - created_by { - id - name - email - slug - } } } } diff --git a/resolvers/admin.py b/resolvers/admin.py index 9efe7c83..81157449 100644 --- a/resolvers/admin.py +++ b/resolvers/admin.py @@ -59,7 +59,10 @@ async def admin_get_users( # ВычисляСм ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠΈ per_page = limit - total_pages = ceil(total_count / per_page) + if total_count is None or per_page in (None, 0): + total_pages = 1 + else: + total_pages = ceil(total_count / per_page) current_page = (offset // per_page) + 1 if per_page > 0 else 1 # ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅ΠΌ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΡŽ @@ -420,7 +423,10 @@ async def admin_get_shouts( # ВычисляСм ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠΈ per_page = limit - total_pages = ceil(total_count / per_page) + if total_count is None or per_page in (None, 0): + total_pages = 1 + else: + total_pages = ceil(total_count / per_page) current_page = (offset // per_page) + 1 if per_page > 0 else 1 # ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅ΠΌ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΡŽ ΠΈ сортировку (Π½ΠΎΠ²Ρ‹Π΅ свСрху) @@ -430,12 +436,20 @@ async def admin_get_shouts( if status == "all": # Для статуса "all" ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ простой запрос Π±Π΅Π· статистики q = q.limit(limit).offset(offset) - shouts_result = session.execute(q).unique().all() + shouts_result: list[Any] = session.execute(q).unique().all() shouts_data = [] for row in shouts_result: # Get the Shout object from the row - shout = row[0] if isinstance(row, tuple) else row.Shout if hasattr(row, "Shout") else row + if isinstance(row, tuple): + shout = row[0] + elif hasattr(row, "Shout"): + shout = row.Shout + elif isinstance(row, dict) and "id" in row: + shout = row + else: + shout = row + # ΠžΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅ΠΌ ΠΏΠΎΠ»Π΅ media media_data = [] if hasattr(shout, "media") and shout.media: @@ -452,49 +466,90 @@ async def admin_get_shouts( media_data = [shout.media] shout_dict = { - "id": shout.id, - "title": shout.title, - "slug": shout.slug, - "body": shout.body, - "lead": shout.lead, - "subtitle": shout.subtitle, - "layout": shout.layout, - "lang": shout.lang, - "cover": shout.cover, - "cover_caption": shout.cover_caption, + "id": getattr(shout, "id", None) if not isinstance(shout, dict) else shout.get("id"), + "title": getattr(shout, "title", None) if not isinstance(shout, dict) else shout.get("title"), + "slug": getattr(shout, "slug", None) if not isinstance(shout, dict) else shout.get("slug"), + "body": getattr(shout, "body", None) if not isinstance(shout, dict) else shout.get("body"), + "lead": getattr(shout, "lead", None) if not isinstance(shout, dict) else shout.get("lead"), + "subtitle": getattr(shout, "subtitle", None) + if not isinstance(shout, dict) + else shout.get("subtitle"), + "layout": getattr(shout, "layout", None) + if not isinstance(shout, dict) + else shout.get("layout"), + "lang": getattr(shout, "lang", None) if not isinstance(shout, dict) else shout.get("lang"), + "cover": getattr(shout, "cover", None) if not isinstance(shout, dict) else shout.get("cover"), + "cover_caption": getattr(shout, "cover_caption", None) + if not isinstance(shout, dict) + else shout.get("cover_caption"), "media": media_data, - "seo": shout.seo, - "created_at": shout.created_at, - "updated_at": shout.updated_at, - "published_at": shout.published_at, - "featured_at": shout.featured_at, - "deleted_at": shout.deleted_at, + "seo": getattr(shout, "seo", None) if not isinstance(shout, dict) else shout.get("seo"), + "created_at": getattr(shout, "created_at", None) + if not isinstance(shout, dict) + else shout.get("created_at"), + "updated_at": getattr(shout, "updated_at", None) + if not isinstance(shout, dict) + else shout.get("updated_at"), + "published_at": getattr(shout, "published_at", None) + if not isinstance(shout, dict) + else shout.get("published_at"), + "featured_at": getattr(shout, "featured_at", None) + if not isinstance(shout, dict) + else shout.get("featured_at"), + "deleted_at": getattr(shout, "deleted_at", None) + if not isinstance(shout, dict) + else shout.get("deleted_at"), "created_by": { - "id": shout.created_by, + "id": getattr(shout, "created_by", None) + if not isinstance(shout, dict) + else shout.get("created_by"), "email": "unknown", # Π—Π°ΠΏΠΎΠ»Π½ΠΈΠΌ ΠΏΡ€ΠΈ нСобходимости "name": "unknown", }, "updated_by": None, # Π—Π°ΠΏΠΎΠ»Π½ΠΈΠΌ ΠΏΡ€ΠΈ нСобходимости "deleted_by": None, # Π—Π°ΠΏΠΎΠ»Π½ΠΈΠΌ ΠΏΡ€ΠΈ нСобходимости "community": { - "id": shout.community, + "id": getattr(shout, "community", None) + if not isinstance(shout, dict) + else shout.get("community"), "name": "unknown", # Π—Π°ΠΏΠΎΠ»Π½ΠΈΠΌ ΠΏΡ€ΠΈ нСобходимости }, "authors": [ - {"id": author.id, "email": author.email, "name": author.name, "slug": author.slug} - for author in (shout.authors or []) + { + "id": getattr(author, "id", None), + "email": getattr(author, "email", None), + "name": getattr(author, "name", None), + "slug": getattr(author, "slug", None), + } + for author in ( + getattr(shout, "authors", []) + if not isinstance(shout, dict) + else shout.get("authors", []) + ) ], "topics": [ - {"id": topic.id, "title": topic.title, "slug": topic.slug} for topic in (shout.topics or []) + { + "id": getattr(topic, "id", None), + "title": getattr(topic, "title", None), + "slug": getattr(topic, "slug", None), + } + for topic in ( + getattr(shout, "topics", []) if not isinstance(shout, dict) else shout.get("topics", []) + ) ], - "version_of": shout.version_of, - "draft": shout.draft, + "version_of": getattr(shout, "version_of", None) + if not isinstance(shout, dict) + else shout.get("version_of"), + "draft": getattr(shout, "draft", None) if not isinstance(shout, dict) else shout.get("draft"), "stat": None, # Π—Π°ΠΏΠΎΠ»Π½ΠΈΠΌ ΠΏΡ€ΠΈ нСобходимости } shouts_data.append(shout_dict) else: # Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΡƒΡŽ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ для получСния ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ со статистикой - shouts_data = get_shouts_with_links(info, q, limit, offset) + shouts_result = get_shouts_with_links(info, q, limit, offset) + shouts_data = [ + s.dict() if hasattr(s, "dict") else dict(s) if hasattr(s, "_mapping") else s for s in shouts_result + ] return { "shouts": shouts_data, @@ -702,7 +757,10 @@ async def admin_get_invites( # ВычисляСм ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠΈ per_page = limit - total_pages = ceil(total_count / per_page) + if total_count is None or per_page in (None, 0): + total_pages = 1 + else: + total_pages = ceil(total_count / per_page) current_page = (offset // per_page) + 1 if per_page > 0 else 1 # ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅ΠΌ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΡŽ ΠΈ сортировку (ΠΏΠΎ ID ΠΏΡ€ΠΈΠ³Π»Π°ΡˆΠ°ΡŽΡ‰Π΅Π³ΠΎ, Π·Π°Ρ‚Π΅ΠΌ Π°Π²Ρ‚ΠΎΡ€Π°, Π·Π°Ρ‚Π΅ΠΌ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ) diff --git a/resolvers/author.py b/resolvers/author.py index 582f91c4..3f14639d 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -143,7 +143,7 @@ async def get_authors_with_stats( default_sort_applied = True else: # If order is not a stats field, treat it as a regular field - column = getattr(Author, order_value, None) + column = getattr(Author, order_value or "", "") if column: base_query = base_query.order_by(sql_desc(column)) logger.debug(f"Applying sorting by column: {order_value}") @@ -153,9 +153,11 @@ async def get_authors_with_stats( else: # Regular sorting by fields for field, direction in by.items(): + if field is None: + continue column = getattr(Author, field, None) if column: - if direction.lower() == "desc": + if isinstance(direction, str) and direction.lower() == "desc": base_query = base_query.order_by(sql_desc(column)) else: base_query = base_query.order_by(column) diff --git a/resolvers/collection.py b/resolvers/collection.py index 303155e2..1df3b251 100644 --- a/resolvers/collection.py +++ b/resolvers/collection.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from graphql import GraphQLResolveInfo @@ -211,8 +211,8 @@ async def delete_collection(_: None, info: GraphQLResolveInfo, slug: str) -> dic @type_collection.field("created_by") -def resolve_collection_created_by(obj: Collection, *_: Any) -> Author: - """Π Π΅Π·ΠΎΠ»Π²Π΅Ρ€ для поля created_by ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΠΈ""" +def resolve_collection_created_by(obj: Collection, *_: Any) -> Optional[Author]: + """Π Π΅Π·ΠΎΠ»Π²Π΅Ρ€ для поля created_by ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΠΈ (ΠΌΠΎΠΆΠ΅Ρ‚ Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ None)""" with local_session() as session: if hasattr(obj, "created_by_author") and obj.created_by_author: return obj.created_by_author diff --git a/settings.py b/settings.py index 91a3f14e..49c4da0f 100644 --- a/settings.py +++ b/settings.py @@ -51,6 +51,22 @@ OAUTH_CLIENTS = { "id": os.getenv("FACEBOOK_CLIENT_ID", ""), "key": os.getenv("FACEBOOK_CLIENT_SECRET", ""), }, + "X": { + "id": os.getenv("X_CLIENT_ID", ""), + "key": os.getenv("X_CLIENT_SECRET", ""), + }, + "YANDEX": { + "id": os.getenv("YANDEX_CLIENT_ID", ""), + "key": os.getenv("YANDEX_CLIENT_SECRET", ""), + }, + "VK": { + "id": os.getenv("VK_CLIENT_ID", ""), + "key": os.getenv("VK_CLIENT_SECRET", ""), + }, + "TELEGRAM": { + "id": os.getenv("TELEGRAM_CLIENT_ID", ""), + "key": os.getenv("TELEGRAM_CLIENT_SECRET", ""), + }, } # Настройки Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… @@ -62,7 +78,7 @@ JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 30 JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30 # Настройки для HTTP cookies (ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π² auth middleware) -SESSION_COOKIE_NAME = "auth_token" +SESSION_COOKIE_NAME = "session_token" SESSION_COOKIE_SECURE = True # Π’ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ для HTTPS SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE: Literal["lax", "strict", "none"] = "lax"