diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..061df26 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# 4 space indentation +[*.{py,java,r,R}] +indent_style = space +indent_size = 4 + +# 2 space indentation +[*.{js,json,yml,yaml,html}] +indent_style = space +indent_size = 2 + +[*.{md,Rmd,rst}] +trim_trailing_whitespace = false +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d88146 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Polétání.cz + + diff --git a/db/csvToDb.py b/db/csvToDb.py index 61f94c0..1e7f5de 100644 --- a/db/csvToDb.py +++ b/db/csvToDb.py @@ -2,14 +2,12 @@ import csv def deg_to_dec(val: str): - val = float(val[:-1]) / 100 deg = int(val) frac = val % 1 # https://www.pgc.umn.edu/apps/convert/ N47 17.6 E012 47.3 - print(frac, frac * 60, frac * 3600) return deg + (frac * 60) @@ -30,6 +28,7 @@ def deg_to_dec(val: str): # return len(val or "") + # ZDAKOV: @49.504378,14.1808905 # 4930.250N / 100 -> cele cislo stupne, desetinne prevest do sedesatkove soustavy @@ -41,4 +40,4 @@ with open("./poi.csv") as f: if row['name'] != 'ZDAKOV': continue - print(row['name'], row['lat'], row['lon'], deg_to_dec(row['lat']), deg_to_dec(row['lon'])) \ No newline at end of file + print(row['name'], row['lat'], row['lon'], deg_to_dec(row['lat']), deg_to_dec(row['lon'])) diff --git a/src/background_jobs/send_email.py b/src/background_jobs/send_email.py index ae2857b..29ed5f2 100644 --- a/src/background_jobs/send_email.py +++ b/src/background_jobs/send_email.py @@ -4,4 +4,4 @@ from mailer import Mailer async def send_email(email: Email): mailer = Mailer() - return await mailer.send(email) \ No newline at end of file + return await mailer.send(email) diff --git a/src/database/query_builder.py b/src/database/query_builder.py index a408e8b..07da7df 100644 --- a/src/database/query_builder.py +++ b/src/database/query_builder.py @@ -8,15 +8,15 @@ class QueryBuilder: self.model = model def get_simple_query( - self, - extra_select: Optional[list] = None, - created_by_id: Optional[int] = None, - order_by: Optional[list] = None, - only_public: Optional[bool] = False, - only_my: Optional[bool] = False, - # include_others_public: Optional[bool] = False, - url_slug: Optional[str] = None, - include_deleted: bool = False + self, + extra_select: Optional[list] = None, + created_by_id: Optional[int] = None, + order_by: Optional[list] = None, + only_public: Optional[bool] = False, + only_my: Optional[bool] = False, + include_others_public: Optional[bool] = False, + url_slug: Optional[str] = None, + include_deleted: bool = False ): if not extra_select: extra_select = [] @@ -37,22 +37,22 @@ class QueryBuilder: if created_by_id and hasattr(self.model, "created_by_id"): my_filters.append(self.model.created_by_id == created_by_id) - # others_filters = [] - # if include_others_public: - # # TODO: Toto jeste neni implementovane nikde v resolverech! - # if hasattr(self.model, "is_public"): - # others_filters.append(self.model.is_public.is_(True)) - # - # if hasattr(self.model, "created_by_id"): - # others_filters.append(or_( - # self.model.created_by_id != created_by_id, - # self.model.created_by_id.is_(None) - # )) + others_filters = [] + if include_others_public: + # TODO: Toto jeste neni implementovane nikde v resolverech! + if hasattr(self.model, "is_public"): + others_filters.append(self.model.is_public.is_(True)) + + if hasattr(self.model, "created_by_id"): + others_filters.append(or_( + self.model.created_by_id != created_by_id, + self.model.created_by_id.is_(None) + )) if my_filters: ownership_filters.append(and_(*my_filters)) - # if others_filters: - # ownership_filters.append(and_(*others_filters)) + if others_filters: + ownership_filters.append(and_(*others_filters)) query = query.filter(or_(*ownership_filters)) diff --git a/src/emails/emails.py b/src/emails/emails.py index 845befb..663162d 100644 --- a/src/emails/emails.py +++ b/src/emails/emails.py @@ -6,7 +6,9 @@ from pydantic import EmailStr class Email(MessageSchema): template: str - def __init__(self, to: list[EmailStr | str], subject: str, template: str, template_variables: dict, **data: Any) -> None: + def __init__( + self, to: list[EmailStr | str], subject: str, template: str, template_variables: dict, **data: Any + ) -> None: data['subtype'] = MessageType.html data['recipients'] = to data['template'] = template diff --git a/src/endpoints/contact.py b/src/endpoints/contact.py index 51ff5a7..5c01e86 100644 --- a/src/endpoints/contact.py +++ b/src/endpoints/contact.py @@ -1,8 +1,7 @@ from fastapi import BackgroundTasks from pydantic import BaseModel -from starlette.responses import Response from background_jobs.send_email import send_email -from emails.emails import WelcomeEmail, ContactFormEmail +from emails.emails import ContactFormEmail class ContactInput(BaseModel): diff --git a/src/endpoints/forgotten_password.py b/src/endpoints/forgotten_password.py index d7f6f1b..aa3766d 100644 --- a/src/endpoints/forgotten_password.py +++ b/src/endpoints/forgotten_password.py @@ -1,8 +1,5 @@ -import os import random import string - -import binascii from datetime import datetime, timedelta from fastapi import BackgroundTasks from passlib.hash import bcrypt diff --git a/src/endpoints/sitemap.py b/src/endpoints/sitemap.py new file mode 100644 index 0000000..53167e5 --- /dev/null +++ b/src/endpoints/sitemap.py @@ -0,0 +1,133 @@ +from pydantic import BaseModel +from sqlalchemy import select +from starlette.responses import Response +from database import models +from database.transaction import get_session + + +class SitemapURL(BaseModel): + url: str + priority: float + + +class SitemapEndpoint(): + + def get_static_routes(self): + routes = [ + ("", 1), + ("kontakt", 0.5), + ("napoveda", 0.7), + ("podminky", 0.5), + ("mista", 0.8), + ("o-projektu", 0.8), + ("osobni-udaje", 0.4), + ("funkce", 0.8) + ] + + return [SitemapURL(url=url, priority=priority) for url, priority in routes] + + async def get_public_pilots(self): + async with get_session() as db: + pilots = (await db.scalars( + select(models.User.public_username) + .filter(models.User.public_username != '') + )) + + return [SitemapURL(url=username, priority=0.8) for username in pilots] + + async def get_public_flights(self): + async with get_session() as db: + flights = (await db.execute( + select(models.Flight, models.User.public_username) + .join(models.Flight.created_by) + .filter(models.Flight.is_public.is_(True)) + .filter(models.Flight.url_slug != '') + .filter(models.User.public_username != '') + )).fetchall() + + return [SitemapURL( + url=f"{pilot_username}/{flight.url_slug}", + priority=0.8 + ) for flight, pilot_username in flights] + + async def get_public_copilots(self): + async with get_session() as db: + copilots = (await db.execute( + select(models.Copilot, models.User.public_username) + .join(models.Copilot.created_by) + .filter(models.Copilot.is_public.is_(True)) + .filter(models.Copilot.url_slug != '') + .filter(models.User.public_username != '') + )).fetchall() + + return [SitemapURL( + url=f"{pilot_username}/a/{copilot.url_slug}", + priority=0.5 + ) for copilot, pilot_username in copilots] + + async def get_public_aircrafts(self): + async with get_session() as db: + aircrafts = (await db.scalars( + select(models.Aircraft.call_sign) + .filter(models.Aircraft.is_public.is_(True)) + )).fetchall() + + return [SitemapURL(url=f"letadlo/{call_sign}", priority=0.6) for call_sign in aircrafts] + + async def get_public_places(self): + async with get_session() as db: + pois = (await db.scalars( + select(models.PointOfInterest) + .filter(models.PointOfInterest.is_public.is_(True)) + .filter(models.PointOfInterest.url_slug != '') + )).fetchall() + + return [SitemapURL(url=f"mista/{poi.url_slug}", priority=0.6) for poi in pois] + + async def get_public_events(self): + async with get_session() as db: + data = (await db.execute( + select(models.Event, models.User.public_username) + .join(models.Event.created_by) + .filter(models.Event.is_public.is_(True)) + .filter(models.User.public_username != '') + )) + + pilots_with_events = set() + routes = [] + for event, pilot_username in data: + + if pilot_username not in pilots_with_events: + routes.append(SitemapURL(url=f"{pilot_username}/udalosti", priority=0.7)) + pilots_with_events.add(pilot_username) + + routes.append(SitemapURL(url=f"{pilot_username}/udalosti/{event.url_slug}", priority=0.7)) + + return routes + + async def on_get(self): + my_sitemap = """ + """ + + sitemap_entries = [ + *self.get_static_routes(), + *(await self.get_public_pilots()), + *(await self.get_public_flights()), + *(await self.get_public_aircrafts()), + *(await self.get_public_events()), + *(await self.get_public_places()), + *(await self.get_public_copilots()), + ] + + # copilots = self.get_public_copilots() + + for sitemap_entry in sitemap_entries: + my_sitemap += f""" + + https://poletani.cz/{sitemap_entry.url} + {sitemap_entry.priority} + + """ + + my_sitemap += "" + return Response(content=my_sitemap, media_type="application/xml") diff --git a/src/external/weather.py b/src/external/weather.py index 79fbcc8..a0fc0dc 100644 --- a/src/external/weather.py +++ b/src/external/weather.py @@ -47,7 +47,8 @@ class Weather: return await resp.json() async def get_weather_for_hour( - self, date_time: datetime.datetime, gps: Tuple[float, float]) -> Dict[str, float | str]: + self, date_time: datetime.datetime, gps: Tuple[float, float] + ) -> Dict[str, float | str]: data = await self.download_weather_for_day(date_time.date(), gps) # TODO: kontrola timezone! diff --git a/src/graphql_schema/dataloaders/base.py b/src/graphql_schema/dataloaders/base.py index a44c6e2..4239898 100644 --- a/src/graphql_schema/dataloaders/base.py +++ b/src/graphql_schema/dataloaders/base.py @@ -6,9 +6,9 @@ from database.query_builder import QueryBuilder class BaseDataloader: def __init__( - self, - model: Type[models.BaseModel], - relationship_column, filters: Optional[list] = None + self, + model: Type[models.BaseModel], + relationship_column, filters: Optional[list] = None ) -> None: super().__init__() self.model = model @@ -39,12 +39,12 @@ class SingleModelByIdDataloader(BaseDataloader): class MultiModelsDataloader(BaseDataloader): def __init__( - self, - model: Type[models.BaseModel], - relationship_column=None, - filters: Optional[list] = None, - extra_join: Optional[list] = None, - order_by: Optional[list] = None, + self, + model: Type[models.BaseModel], + relationship_column=None, + filters: Optional[list] = None, + extra_join: Optional[list] = None, + order_by: Optional[list] = None, ): super().__init__(model, relationship_column, filters) diff --git a/src/graphql_schema/entities/aircraft.py b/src/graphql_schema/entities/aircraft.py index 0d0fd7d..da859be 100644 --- a/src/graphql_schema/entities/aircraft.py +++ b/src/graphql_schema/entities/aircraft.py @@ -30,10 +30,10 @@ class AircraftQueries: @error_logging @allow_public async def aircraft( - root, info, - id: Optional[int] = None, - call_sign: Optional[str] = None, - public: Optional[bool] = False + root, info, + id: Optional[int] = None, + call_sign: Optional[str] = None, + public: Optional[bool] = False ) -> Aircraft: filter_params = {} if id: diff --git a/src/graphql_schema/entities/copilot.py b/src/graphql_schema/entities/copilot.py index 651b66c..5c4a63a 100644 --- a/src/graphql_schema/entities/copilot.py +++ b/src/graphql_schema/entities/copilot.py @@ -23,13 +23,12 @@ class CopilotQueries: @error_logging @allow_public async def copilot( - root, info: Info, - id: Optional[int] = None, - url_slug: Optional[str] = None, - pilot_username: Optional[str] = None, - public: Optional[bool] = False + root, info: Info, + id: Optional[int] = None, + url_slug: Optional[str] = None, + pilot_username: Optional[str] = None, + public: Optional[bool] = False ) -> Copilot: - filter_params = get_detail_filters(id, url_slug) if pilot_username: filter_params['pilot_username'] = pilot_username diff --git a/src/graphql_schema/entities/event.py b/src/graphql_schema/entities/event.py index 5cfc792..71000d8 100644 --- a/src/graphql_schema/entities/event.py +++ b/src/graphql_schema/entities/event.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional import strawberry from database import models from decorators.endpoints import authenticated_user_only, allow_public @@ -17,12 +17,12 @@ class EventQueries: @error_logging @allow_public async def events( - root, - info, - limit: int, - offset: int = 0, - username: Optional[str] = None, - public: Optional[bool] = False, + root, + info, + limit: int, + offset: int = 0, + username: Optional[str] = None, + public: Optional[bool] = False, ) -> PaginationWindow[Event]: query = EventQueryResolver().get_query( info.context.user_id, @@ -42,11 +42,11 @@ class EventQueries: @error_logging @allow_public async def event( - root, info, - id: Optional[int] = None, - url_slug: Optional[str] = None, - username: Optional[str] = None, - public: Optional[bool] = False + root, info, + id: Optional[int] = None, + url_slug: Optional[str] = None, + username: Optional[str] = None, + public: Optional[bool] = False ) -> Event: filter_params = get_detail_filters(id, url_slug) if username: diff --git a/src/graphql_schema/entities/flight.py b/src/graphql_schema/entities/flight.py index b366eca..5ee6cc2 100644 --- a/src/graphql_schema/entities/flight.py +++ b/src/graphql_schema/entities/flight.py @@ -16,15 +16,15 @@ class FlightQueries: @error_logging @allow_public async def flights( - root, info, - limit: int, - offset: int = 0, - username: Optional[str] = None, - event_id: Optional[int] = None, - public: Optional[bool] = False, - copilot_id: Optional[int] = None, - point_of_interest_id: Optional[int] = None, - aircraft_id: Optional[int] = None, + root, info, + limit: int, + offset: int = 0, + username: Optional[str] = None, + event_id: Optional[int] = None, + public: Optional[bool] = False, + copilot_id: Optional[int] = None, + point_of_interest_id: Optional[int] = None, + aircraft_id: Optional[int] = None, ) -> PaginationWindow[Flight]: query = FlightQueryResolver().get_query( user_id=info.context.user_id, @@ -47,11 +47,11 @@ class FlightQueries: @error_logging @allow_public async def flight( - root, info, - id: Optional[int] = None, - url_slug: Optional[str] = None, - username: Optional[str] = None, - public: Optional[bool] = False + root, info, + id: Optional[int] = None, + url_slug: Optional[str] = None, + username: Optional[str] = None, + public: Optional[bool] = False ) -> Flight: filter_params = get_detail_filters(id, url_slug) if username: diff --git a/src/graphql_schema/entities/helpers/combobox.py b/src/graphql_schema/entities/helpers/combobox.py index 5ff7471..8de0c76 100644 --- a/src/graphql_schema/entities/helpers/combobox.py +++ b/src/graphql_schema/entities/helpers/combobox.py @@ -5,12 +5,12 @@ from graphql_schema.entities.types.mutation_input import ComboboxInput async def handle_combobox_save( - db: AsyncSession, - model: Type[models.BaseModel], - input: ComboboxInput, - user_id: int, - name_column: str = "name", - extra_data: Optional[dict] = None + db: AsyncSession, + model: Type[models.BaseModel], + input: ComboboxInput, + user_id: int, + name_column: str = "name", + extra_data: Optional[dict] = None ) -> int: if input.id: return input.id diff --git a/src/graphql_schema/entities/helpers/pagination.py b/src/graphql_schema/entities/helpers/pagination.py index 06e9868..a6014e4 100644 --- a/src/graphql_schema/entities/helpers/pagination.py +++ b/src/graphql_schema/entities/helpers/pagination.py @@ -18,10 +18,10 @@ class PaginationWindow(Generic[Item]): async def get_pagination_window( - query: Select, - item_type: type, - limit: int, - offset: int = 0, + query: Select, + item_type: type, + limit: int, + offset: int = 0, ) -> PaginationWindow: if limit <= 0: raise Exception(f"limit ({limit}) must be > 0") diff --git a/src/graphql_schema/entities/organization.py b/src/graphql_schema/entities/organization.py index f50a291..f0c505c 100644 --- a/src/graphql_schema/entities/organization.py +++ b/src/graphql_schema/entities/organization.py @@ -1,13 +1,13 @@ from typing import List import strawberry -from sqlalchemy import delete, select +from sqlalchemy import delete from sqlalchemy.dialects.mysql import insert from sqlalchemy.exc import IntegrityError from database import models from decorators.endpoints import authenticated_user_only from database.transaction import get_session from decorators.error_logging import error_logging -from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver +from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput from graphql_schema.entities.types.types import Organization diff --git a/src/graphql_schema/entities/photo.py b/src/graphql_schema/entities/photo.py index 060ee50..6b1e080 100644 --- a/src/graphql_schema/entities/photo.py +++ b/src/graphql_schema/entities/photo.py @@ -15,12 +15,12 @@ class PhotoQueries: @error_logging @allow_public async def photos( - root, info, - flight_id: Optional[int] = None, - copilot_id: Optional[int] = None, - point_of_interest_id: Optional[int] = None, - aircraft_id: Optional[int] = None, - public: Optional[bool] = False, + root, info, + flight_id: Optional[int] = None, + copilot_id: Optional[int] = None, + point_of_interest_id: Optional[int] = None, + aircraft_id: Optional[int] = None, + public: Optional[bool] = False, ) -> List[Photo]: return await PhotoQueryResolver().get_list( public=public, @@ -35,7 +35,7 @@ class PhotoQueries: @strawberry.field() @error_logging @allow_public - async def photo(root, info, id: int, public: Optional[bool] = False,) -> Photo: + async def photo(root, info, id: int, public: Optional[bool] = False, ) -> Photo: return await BaseQueryResolver(Photo, models.Photo).get_one( object_id=id, user_id=info.context.user_id, diff --git a/src/graphql_schema/entities/poi.py b/src/graphql_schema/entities/poi.py index 28160ff..7822fd5 100644 --- a/src/graphql_schema/entities/poi.py +++ b/src/graphql_schema/entities/poi.py @@ -1,7 +1,5 @@ -from typing import List, Optional +from typing import Optional import strawberry -from graphql import GraphQLError - from database import models from decorators.endpoints import authenticated_user_only, allow_public from database.transaction import get_session @@ -13,19 +11,24 @@ from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutati from graphql_schema.entities.types.types import PointOfInterest from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput + @strawberry.type class PointOfInterestQueries: @strawberry.field() @error_logging @allow_public async def points_of_interest( - root, info, - limit: int, offset: int = 0, - search: Optional[str] = None, - public: bool = False + root, info, + limit: int, offset: int = 0, + search: Optional[str] = None, + public: bool = False ) -> PaginationWindow[PointOfInterest]: query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query( - info.context.user_id, only_public=public, search=search, + info.context.user_id, + only_my=bool(info.context.user_id), + include_others_public=True, + only_public=public, + search=search, ) return await get_pagination_window( query=query, @@ -37,10 +40,10 @@ class PointOfInterestQueries: @strawberry.field() @allow_public async def point_of_interest( - root, info, - url_slug: Optional[str] = None, - id: Optional[int] = None, - public: bool = False + root, info, + url_slug: Optional[str] = None, + id: Optional[int] = None, + public: bool = False ) -> PointOfInterest: filter_params = get_detail_filters(id, url_slug) diff --git a/src/graphql_schema/entities/resolvers/aircraft.py b/src/graphql_schema/entities/resolvers/aircraft.py index e27df91..fdf0ff0 100644 --- a/src/graphql_schema/entities/resolvers/aircraft.py +++ b/src/graphql_schema/entities/resolvers/aircraft.py @@ -16,13 +16,13 @@ class AircraftQueryResolver(BaseQueryResolver): super().__init__(graphql_type=Aircraft, model=models.Aircraft) def get_query( - self, - user_id: Optional[int] = None, - object_id: Optional[int] = None, - order_by: Optional[list] = None, - organization_ids: Optional[Set[int]] = None, - *args, - **kwargs, + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + organization_ids: Optional[Set[int]] = None, + *args, + **kwargs, ): filters = {} if object_id: diff --git a/src/graphql_schema/entities/resolvers/base.py b/src/graphql_schema/entities/resolvers/base.py index 61713ad..d416bd6 100644 --- a/src/graphql_schema/entities/resolvers/base.py +++ b/src/graphql_schema/entities/resolvers/base.py @@ -30,19 +30,21 @@ class BaseQueryResolver(BaseResolver): return self.graphql_type(**data.as_dict()) def get_query( - self, - user_id: Optional[int] = None, - object_id: Optional[int] = None, - order_by: Optional[list] = None, - only_public: Optional[bool] = False, - only_my: Optional[bool] = False, - url_slug: Optional[str] = None, - **kwargs, + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + only_public: Optional[bool] = False, + only_my: Optional[bool] = False, + include_others_public: Optional[bool] = False, + url_slug: Optional[str] = None, + **kwargs, ): query = self.query_builder.get_simple_query( created_by_id=user_id, order_by=order_by, only_public=only_public, + include_others_public=include_others_public, only_my=only_my, url_slug=url_slug ) diff --git a/src/graphql_schema/entities/resolvers/copilot.py b/src/graphql_schema/entities/resolvers/copilot.py index a736a4f..f26bd06 100644 --- a/src/graphql_schema/entities/resolvers/copilot.py +++ b/src/graphql_schema/entities/resolvers/copilot.py @@ -9,12 +9,12 @@ class CopilotQueryResolver(BaseQueryResolver): super().__init__(Copilot, models.Copilot) def get_query( - self, - user_id: Optional[int] = None, - object_id: Optional[int] = None, - order_by: Optional[list] = None, - only_public: Optional[bool] = False, - **kwargs + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + only_public: Optional[bool] = False, + **kwargs ): pilot_username = kwargs.pop("pilot_username", None) diff --git a/src/graphql_schema/entities/resolvers/event.py b/src/graphql_schema/entities/resolvers/event.py index 9289711..6b8a482 100644 --- a/src/graphql_schema/entities/resolvers/event.py +++ b/src/graphql_schema/entities/resolvers/event.py @@ -9,13 +9,13 @@ class EventQueryResolver(BaseQueryResolver): super().__init__(graphql_type=Event, model=models.Event) def get_query( - self, - user_id: Optional[int] = None, - object_id: Optional[int] = None, - order_by: Optional[list] = None, - only_public: Optional[bool] = True, - *args, - **kwargs, + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + only_public: Optional[bool] = True, + *args, + **kwargs, ): query = super().get_query( user_id, object_id, diff --git a/src/graphql_schema/entities/resolvers/flight.py b/src/graphql_schema/entities/resolvers/flight.py index 2b21975..e4ba81b 100644 --- a/src/graphql_schema/entities/resolvers/flight.py +++ b/src/graphql_schema/entities/resolvers/flight.py @@ -10,8 +10,10 @@ from database.models import flight_has_copilot from database.transaction import get_session from external.gpx_parser import GPXParser from graphql_schema.entities.helpers.combobox import handle_combobox_save -from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver, GQL_TYPE -from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput, CreateFlightInput +from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver +from graphql_schema.entities.types.mutation_input import ( + EditFlightInput, TrackItemInput, ComboboxInput, CreateFlightInput +) from graphql_schema.entities.types.types import Flight from paths import FLIGHT_GPX_TRACK_PATH from utils.file import delete_file @@ -23,13 +25,13 @@ class FlightQueryResolver(BaseQueryResolver): super().__init__(graphql_type=Flight, model=models.Flight) def get_query( - self, - user_id: Optional[int] = None, - object_id: Optional[int] = None, - url_slug: Optional[str] = None, - only_public: Optional[bool] = False, - *args, - **kwargs + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + url_slug: Optional[str] = None, + only_public: Optional[bool] = False, + *args, + **kwargs ): filters = {} if object_id: @@ -77,20 +79,22 @@ class FlightMutationResolver(BaseMutationResolver): super().__init__(Flight, models.Flight) async def get_airport_id_by_gps(self, gps_lat: float, gps_lng: float) -> Optional[int]: - async with get_session() as db: - query = select( - models.Airport, - func.coalesce((6371 * func.acos( + async with (get_session() as db): + query = ( + select(models.Airport, func.coalesce(6371 * func.acos( func.cos(func.radians(gps_lat)) * func.cos(func.radians(models.Airport.gps_latitude)) * func.cos(func.radians(models.Airport.gps_longitude) - func.radians(gps_lng)) + func.sin(func.radians(gps_lat)) * func.sin(func.radians(models.Airport.gps_latitude)) - )), 9999).label("distance") - ).filter(models.Airport.use_in_gpx_guess.is_(True)).order_by("distance").having(text("distance < 1")).limit(1) + ), 9999).label("distance")) + .filter(models.Airport.use_in_gpx_guess.is_(True)) + .order_by("distance") + .having(text("distance < 1")) + .limit(1) + ) data = (await db.execute(query)).one_or_none() - if data: airport, distance = data return airport.id @@ -151,11 +155,13 @@ class FlightMutationResolver(BaseMutationResolver): context.background_tasks.add_task( download_weather, - flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime, type_="takeoff" + flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime, + type_="takeoff" ) context.background_tasks.add_task( download_weather, - flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime, type_="landing" + flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime, + type_="landing" ) return flight diff --git a/src/graphql_schema/entities/resolvers/organization.py b/src/graphql_schema/entities/resolvers/organization.py index c606066..c3d4fd0 100644 --- a/src/graphql_schema/entities/resolvers/organization.py +++ b/src/graphql_schema/entities/resolvers/organization.py @@ -11,11 +11,11 @@ class OrganizationQueryResolver(BaseQueryResolver): super().__init__(Organization, models.Organization) def get_query( - self, - object_id: Optional[int] = None, - order_by: Optional[list] = None, - **kwargs): - + self, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + **kwargs + ): query = ( select(models.Organization) .filter(models.Organization.deleted.is_(False)) diff --git a/src/graphql_schema/entities/resolvers/photo.py b/src/graphql_schema/entities/resolvers/photo.py index 9a353e3..5f6c38a 100644 --- a/src/graphql_schema/entities/resolvers/photo.py +++ b/src/graphql_schema/entities/resolvers/photo.py @@ -25,12 +25,12 @@ class PhotoQueryResolver(BaseQueryResolver): super().__init__(Photo, models.Photo) def get_query( - self, - user_id: Optional[int] = None, - object_id: Optional[int] = None, - order_by: Optional[list] = None, - only_public: Optional[bool] = False, - *args, **kwargs + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + only_public: Optional[bool] = False, + *args, **kwargs ): query = super().get_query( @@ -80,7 +80,7 @@ class PhotoMutationResolver(BaseMutationResolver): async with get_session() as db: photo = await self._get_one(db, id, created_by_id=user_id) flight_id = photo.flight_id - filename = photo.filename+"."+photo.filename_extension + filename = photo.filename + "." + photo.filename_extension path = get_photo_basepath(flight_id) return PhotoDetailInfo( diff --git a/src/graphql_schema/entities/types/types.py b/src/graphql_schema/entities/types/types.py index 743ec63..43f19d1 100644 --- a/src/graphql_schema/entities/types/types.py +++ b/src/graphql_schema/entities/types/types.py @@ -1,11 +1,7 @@ from __future__ import annotations from datetime import datetime -from typing import Optional, List, Any +from typing import Optional, List import strawberry -from graphql import GraphQLError -from strawberry.permission import PermissionExtension, BasePermission -from strawberry.types import Info - from database import models from decorators.endpoints import authenticated_user_only from external.gpx_parser import GPXParser diff --git a/src/graphql_schema/sqlalchemy_to_strawberry_type.py b/src/graphql_schema/sqlalchemy_to_strawberry_type.py index 73aed31..e62e6c2 100644 --- a/src/graphql_schema/sqlalchemy_to_strawberry_type.py +++ b/src/graphql_schema/sqlalchemy_to_strawberry_type.py @@ -44,9 +44,10 @@ def strawberry_sqlalchemy_type(model, exclude_fields: Optional[typing.Union[List def strawberry_sqlalchemy_input( - model, - exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None, - all_optional: bool = False) -> typing.Callable[[...], strawberry.object_type]: + model, + exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None, + all_optional: bool = False +) -> typing.Callable[[...], strawberry.object_type]: if exclude_fields is None: exclude_fields = [] diff --git a/src/main.py b/src/main.py index 9db531a..6293c2f 100644 --- a/src/main.py +++ b/src/main.py @@ -16,6 +16,7 @@ from endpoints.graphql import setup_graphql_endpoint from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint from endpoints.photo_editor_preview import PhotoEditorEndpoint from endpoints.registration import RegistrationInput, RegistrationEndpoint +from endpoints.sitemap import SitemapEndpoint class StaticFilesCache(StaticFiles): @@ -111,8 +112,8 @@ class App: @self.api_router.post("/refresh", summary="Refresh access token") async def refresh( - resp: Response, - credentials: JwtAuthorizationCredentials = Security(self.refresh_security) + resp: Response, + credentials: JwtAuthorizationCredentials = Security(self.refresh_security) ): return await RefreshEndpoint( access_token=self.access_security, @@ -126,34 +127,47 @@ class App: refresh_token=self.refresh_security ).on_post(resp) - @self.api_router.get("/forgotten-password/token/{token}") + @self.api_router.get( + "/forgotten-password/token/{token}", + summary="Info about token used for resetting password" + ) async def token_info(token: str): return await ForgottenPasswordEndpoint().token_info(token) - @self.api_router.post("/forgotten-password/request") - async def password_reset(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks): + @self.api_router.post( + "/forgotten-password/request", + summary="Request password change, e-mail will be sent to validate your request." + ) + async def request_password_change(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks): return await ForgottenPasswordEndpoint().request(input, background_tasks) - @self.api_router.post("/forgotten-password/reset") - async def password_reset(input: ChangeForgottenPassword): + @self.api_router.post( + "/forgotten-password/reset", + summary="Set new password after successfull token validation" + ) + async def reset_password(input: ChangeForgottenPassword): return await ForgottenPasswordEndpoint().change_password(input) @self.api_router.post("/contact", summary="Send email from contact form") - async def contact(input: ContactInput, background_tasks: BackgroundTasks): + async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks): return await ContactEndpoint().on_post(input, background_tasks) - @self.api_router.get("/photo/editor-preview/{photo_id}") + @self.api_router.get("/sitemap.xml") + async def sitemap(): + return await SitemapEndpoint().on_get() + + @self.api_router.get("/photo/editor-preview/{photo_id}", summary="Photo editor preview") async def photo_editor_preview( - photo_id: int, - brightness: Optional[float] = None, - contrast: Optional[float] = None, - saturation: Optional[float] = None, - sharpness: Optional[float] = None, - rotate: Optional[float] = None, - crop_left: Optional[float] = None, - crop_top: Optional[float] = None, - crop_width: Optional[float] = None, - crop_height: Optional[float] = None, + photo_id: int, + brightness: Optional[float] = None, + contrast: Optional[float] = None, + saturation: Optional[float] = None, + sharpness: Optional[float] = None, + rotate: Optional[float] = None, + crop_left: Optional[float] = None, + crop_top: Optional[float] = None, + crop_width: Optional[float] = None, + crop_height: Optional[float] = None, ): return await PhotoEditorEndpoint( access_token=self.access_security, diff --git a/src/paths.py b/src/paths.py index 1d0facf..6e9d569 100644 --- a/src/paths.py +++ b/src/paths.py @@ -39,7 +39,7 @@ def get_photo_thumbnail_url(root) -> str: log.warning(f"Missing thumbnail {thumbnail} in flight ID={root.flight_id}") # TODO: doplnit chybejici nahled, tohle by se ale nikdy nemelo stat! Vzdy musi existovat alespon originalni fotka - return get_public_url(f"photos/missing-thumbnail.webp") + return get_public_url("photos/missing-thumbnail.webp") def get_avatar_url(user) -> str: diff --git a/src/utils/image.py b/src/utils/image.py index fe6c22b..87ca037 100644 --- a/src/utils/image.py +++ b/src/utils/image.py @@ -101,11 +101,11 @@ class PhotoEditor: return self def adjust( - self, - brightness: Optional[float] = None, - contrast: Optional[float] = None, - saturation: Optional[float] = None, - sharpness: Optional[float] = None + self, + brightness: Optional[float] = None, + contrast: Optional[float] = None, + saturation: Optional[float] = None, + sharpness: Optional[float] = None ): adjustments = [ (Brightness, brightness), @@ -127,8 +127,8 @@ class PhotoEditor: return img_io def write_to_file( - self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None, - format_: Optional[str] = "JPEG" + self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None, + format_: Optional[str] = "JPEG" ) -> str: check_directories(dest_path or self.path) diff --git a/src/utils/upload.py b/src/utils/upload.py index 2b0bdcc..57c4677 100644 --- a/src/utils/upload.py +++ b/src/utils/upload.py @@ -2,18 +2,23 @@ import os import re import uuid from strawberry.file_uploads import Upload - from utils.file import check_directories -async def handle_file_upload(file: Upload, path: str, filename_maxlength: int = 64, uid_prefix: bool = True, overwrite: bool = True) -> str: +async def handle_file_upload( + file: Upload, + path: str, + filename_maxlength: int = 64, + uid_prefix: bool = True, + overwrite: bool = True +) -> str: check_directories(path) prefix = f"{uuid.uuid4()}-" if uid_prefix else "" filename = f"{prefix}{file.filename}"[-1 * filename_maxlength:] # sanitize filename - filename = re.sub('[^\w_. -]', '', filename).replace(" ", "-") + filename = re.sub(r'[^\w_. -]', '', filename).replace(" ", "-") target_path = f"{path}/{filename}"