Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e8f677b6f | ||
|
|
6553da41f2 | ||
|
|
420774f1b4 | ||
|
|
888351baac | ||
|
|
d794208193 | ||
|
|
71911c0905 | ||
|
|
a7fda95c29 | ||
|
|
2f824e264f | ||
|
|
beee16bf80 | ||
|
|
77c9242944 | ||
|
|
c6316262e5 | ||
|
|
d31f1c0c61 | ||
|
|
ca3f6ef07c | ||
|
|
186dbd0720 | ||
|
|
365a1fc1b0 | ||
|
|
0a6b19f334 | ||
|
|
3081580c21 | ||
|
|
94639da820 | ||
|
|
9e6b0b574c | ||
|
|
10bc0847b6 | ||
|
|
e541508f75 | ||
|
|
ac3b496459 | ||
|
|
2bf737b9d6 | ||
|
|
ba5e73108f | ||
|
|
94613a5b1b | ||
|
|
62e46f37ce | ||
|
|
a706303155 | ||
|
|
c8138860ca | ||
|
|
c2b066d371 | ||
|
|
cee5de003f | ||
|
|
5f0a1da052 | ||
|
|
ffb8d07ebe | ||
|
|
fd0aef83b1 | ||
|
|
66d9877552 | ||
|
|
63e2eed8c2 | ||
|
|
01f8a616bf | ||
|
|
6a7d51efc5 | ||
|
|
24825108a7 | ||
|
|
ed35de96c9 | ||
|
|
7c81069e96 | ||
|
|
400ad8b75a | ||
|
|
c0ae73440d | ||
|
|
1f8b338297 | ||
|
|
ff32c36a94 | ||
|
|
e28a6fce57 | ||
|
|
51316c492b | ||
|
|
c26eb97288 | ||
|
|
03c9f2d189 | ||
|
|
77b9552a9d | ||
|
|
cc95197682 | ||
|
|
57aad05359 | ||
|
|
dfb96e768e | ||
|
|
0e890dc201 | ||
|
|
3c757df7f1 | ||
|
|
dabed327dd | ||
|
|
3b1fd0351a | ||
|
|
3edc1c853f | ||
|
|
f4d709d621 | ||
|
|
f535d2866c | ||
|
|
89f5b8bb0c | ||
|
|
3167ea2f8a | ||
|
|
ecb37fb961 | ||
|
|
108a731608 | ||
|
|
19c1ff1846 | ||
|
|
bd5b84c9e7 | ||
|
|
e3ab6ba7f0 | ||
|
|
e8aa678555 | ||
|
|
f2e2c1a1ab | ||
|
|
ebf2995d51 | ||
|
|
52ddf6502a | ||
|
|
0de73a5ec2 | ||
|
|
3223d2ac00 | ||
|
|
322e61bce3 | ||
|
|
bc258efa3a | ||
|
|
7f7f79dad6 | ||
|
|
ce2c023eaf | ||
|
|
86fe3949f6 | ||
|
|
b355d0b439 | ||
|
|
2422f26f10 | ||
|
|
3f03021f5c | ||
|
|
dc49cd347c | ||
|
|
d820b92964 | ||
|
|
22a28ae70e | ||
|
|
2c53a8ffa3 | ||
|
|
f50c9660c7 | ||
|
|
037c7daef6 | ||
|
|
1760dba99d | ||
|
|
64cb544e2d |
@@ -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
|
||||||
@@ -7,3 +7,8 @@ MYSQL_DATABASE: "poletani_cz"
|
|||||||
MYSQL_USER: "mysql_user"
|
MYSQL_USER: "mysql_user"
|
||||||
MYSQL_PASSWORD: "****"
|
MYSQL_PASSWORD: "****"
|
||||||
APP_ENV: "production"
|
APP_ENV: "production"
|
||||||
|
|
||||||
|
SMTP_HOST: ""
|
||||||
|
SMTP_PORT: ""
|
||||||
|
SMTP_USERNAME: ""
|
||||||
|
SMTP_PASSWORD: ""
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude =
|
exclude =
|
||||||
venv
|
venv
|
||||||
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
alembic
|
alembic
|
||||||
@@ -8,3 +8,7 @@ __pycache__/
|
|||||||
# app specific files and dirs
|
# app specific files and dirs
|
||||||
uploads/*
|
uploads/*
|
||||||
!uploads/.gitkeep
|
!uploads/.gitkeep
|
||||||
|
src/emails/templates/html/*
|
||||||
|
.env
|
||||||
|
/static/airports/
|
||||||
|
/static/airports/.gitkeep
|
||||||
|
|||||||
+19
-6
@@ -1,15 +1,28 @@
|
|||||||
FROM python:3.11-alpine
|
FROM node:20-alpine AS email_builder
|
||||||
|
|
||||||
|
RUN npm install -g mjml
|
||||||
|
RUN mkdir -p /app/templates/html
|
||||||
|
|
||||||
|
COPY ./src/emails/templates/mjml /app/templates
|
||||||
|
RUN mjml /app/templates/* -o /app/templates/html/
|
||||||
|
|
||||||
|
|
||||||
|
FROM python:3.13-alpine AS app
|
||||||
|
|
||||||
RUN apk update && \
|
|
||||||
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
|
||||||
pip install --upgrade pip
|
|
||||||
# protoze xfs uzivatel ma id=33
|
# protoze xfs uzivatel ma id=33
|
||||||
RUN deluser xfs
|
# RUN deluser xfs
|
||||||
|
|
||||||
# uzivatel www-data v alpine neexistuje
|
# uzivatel www-data v alpine neexistuje
|
||||||
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
||||||
|
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
||||||
|
pip install --upgrade pip
|
||||||
|
|
||||||
|
|
||||||
COPY requirements.txt /app/requirements.txt
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
|
||||||
|
|
||||||
# required packages for pip (becasue of yarl lib)
|
# required packages for pip (becasue of yarl lib)
|
||||||
ENV INSTALL_PACKAGES build-base linux-headers
|
ENV INSTALL_PACKAGES build-base linux-headers
|
||||||
@@ -23,4 +36,4 @@ COPY . /app
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"]
|
CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ]
|
||||||
|
|||||||
+201
@@ -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.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""add terrain elevation to photos and event table
|
||||||
|
|
||||||
|
Revision ID: 6e5cc5123a2b
|
||||||
|
Revises: 76752a74a304
|
||||||
|
Create Date: 2023-09-05 09:54:45.509461
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6e5cc5123a2b'
|
||||||
|
down_revision = '76752a74a304'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('event',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=False),
|
||||||
|
sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('deleted', sa.Boolean(), server_default='0', nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.add_column('flight', sa.Column('event_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('flight', sa.Column('has_terrain_elevation', sa.Boolean(), nullable=False))
|
||||||
|
op.create_foreign_key(None, 'flight', 'event', ['event_id'], ['id'])
|
||||||
|
op.add_column('photo', sa.Column('terrain_elevation', sa.Float(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('photo', 'terrain_elevation')
|
||||||
|
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||||
|
op.drop_column('flight', 'has_terrain_elevation')
|
||||||
|
op.drop_column('flight', 'event_id')
|
||||||
|
op.drop_table('event')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""make gps in airport nullable
|
||||||
|
|
||||||
|
Revision ID: a09857ec9721
|
||||||
|
Revises: 6e5cc5123a2b
|
||||||
|
Create Date: 2023-09-13 09:32:06.444179
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a09857ec9721'
|
||||||
|
down_revision = '6e5cc5123a2b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('airport', 'gps_latitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('airport', 'gps_longitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('airport', 'gps_longitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('airport', 'gps_latitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
"""add from/to to event, organization to event
|
||||||
|
|
||||||
|
Revision ID: 39a62618eacb
|
||||||
|
Revises: a09857ec9721
|
||||||
|
Create Date: 2023-09-20 11:53:00.566736
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '39a62618eacb'
|
||||||
|
down_revision = 'a09857ec9721'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('aircraft_notes')
|
||||||
|
op.add_column('event', sa.Column('event_from', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('event', sa.Column('event_to', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('event', sa.Column('organization_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'event', 'organization', ['organization_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'event', type_='foreignkey')
|
||||||
|
op.drop_column('event', 'organization_id')
|
||||||
|
op.drop_column('event', 'event_to')
|
||||||
|
op.drop_column('event', 'event_from')
|
||||||
|
op.create_table('aircraft_notes',
|
||||||
|
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('aircraft_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
|
||||||
|
sa.Column('description', mysql.TEXT(), nullable=False),
|
||||||
|
sa.Column('is_public', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('created_by_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('created_at', mysql.DATETIME(), server_default=sa.text('current_timestamp()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['aircraft_id'], ['aircraft.id'], name='aircraft_notes_ibfk_1'),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], name='aircraft_notes_ibfk_2'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_collate='utf8mb4_general_ci',
|
||||||
|
mysql_default_charset='utf8mb4',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""rename from/to in events
|
||||||
|
|
||||||
|
Revision ID: 8b0c020dc0c4
|
||||||
|
Revises: 39a62618eacb
|
||||||
|
Create Date: 2023-09-22 08:53:20.901142
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8b0c020dc0c4'
|
||||||
|
down_revision = '39a62618eacb'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('event', sa.Column('date_from', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('event', sa.Column('date_to', sa.DateTime(), nullable=True))
|
||||||
|
op.drop_column('event', 'event_from')
|
||||||
|
op.drop_column('event', 'event_to')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('event', sa.Column('event_to', mysql.DATETIME(), nullable=True))
|
||||||
|
op.add_column('event', sa.Column('event_from', mysql.DATETIME(), nullable=True))
|
||||||
|
op.drop_column('event', 'date_to')
|
||||||
|
op.drop_column('event', 'date_from')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""add airports to track
|
||||||
|
|
||||||
|
Revision ID: 956d295689bf
|
||||||
|
Revises: 8b0c020dc0c4
|
||||||
|
Create Date: 2023-09-28 22:43:52.286599
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '956d295689bf'
|
||||||
|
down_revision = '8b0c020dc0c4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_track', sa.Column('airport_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('flight_track', sa.Column('landing_duration', sa.Integer(), nullable=True))
|
||||||
|
op.alter_column('flight_track', 'point_of_interest_id',
|
||||||
|
existing_type=mysql.INTEGER(display_width=11),
|
||||||
|
nullable=True)
|
||||||
|
op.create_foreign_key(None, 'flight_track', 'airport', ['airport_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_track', type_='foreignkey')
|
||||||
|
op.alter_column('flight_track', 'point_of_interest_id',
|
||||||
|
existing_type=mysql.INTEGER(display_width=11),
|
||||||
|
nullable=False)
|
||||||
|
op.drop_column('flight_track', 'landing_duration')
|
||||||
|
op.drop_column('flight_track', 'airport_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Add photo adjustment
|
||||||
|
|
||||||
|
Revision ID: 2acb6fa61028
|
||||||
|
Revises: 956d295689bf
|
||||||
|
Create Date: 2023-10-15 22:33:04.911765
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2acb6fa61028'
|
||||||
|
down_revision = '956d295689bf'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('photo_adjustment',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('photo_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('rotate', sa.Float(), nullable=True),
|
||||||
|
sa.Column('contrast', sa.Float(), nullable=True),
|
||||||
|
sa.Column('brightness', sa.Float(), nullable=True),
|
||||||
|
sa.Column('saturation', sa.Float(), nullable=True),
|
||||||
|
sa.Column('sharpness', sa.Float(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['photo_id'], ['photo.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('photo_adjustment')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""add crop info
|
||||||
|
|
||||||
|
Revision ID: c3aef4ae2669
|
||||||
|
Revises: 2acb6fa61028
|
||||||
|
Create Date: 2023-10-20 23:53:21.772545
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c3aef4ae2669'
|
||||||
|
down_revision = '2acb6fa61028'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_left', sa.Float(), nullable=True))
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_top', sa.Float(), nullable=True))
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_width', sa.Float(), nullable=True))
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_height', sa.Float(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('photo_adjustment', 'crop_height')
|
||||||
|
op.drop_column('photo_adjustment', 'crop_width')
|
||||||
|
op.drop_column('photo_adjustment', 'crop_top')
|
||||||
|
op.drop_column('photo_adjustment', 'crop_left')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add size to photos
|
||||||
|
|
||||||
|
Revision ID: 4996ad8bc231
|
||||||
|
Revises: c3aef4ae2669
|
||||||
|
Create Date: 2023-10-24 16:32:10.826763
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4996ad8bc231'
|
||||||
|
down_revision = 'c3aef4ae2669'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('photo', sa.Column('width', sa.Integer(), nullable=False))
|
||||||
|
op.add_column('photo', sa.Column('height', sa.Integer(), nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('photo', 'height')
|
||||||
|
op.drop_column('photo', 'width')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""on delete cascade u photo adjustment
|
||||||
|
|
||||||
|
Revision ID: d42172840f6c
|
||||||
|
Revises: 4996ad8bc231
|
||||||
|
Create Date: 2023-10-30 16:30:57.118694
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd42172840f6c'
|
||||||
|
down_revision = '4996ad8bc231'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('photo_adjustment_ibfk_1', 'photo_adjustment', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'photo_adjustment', 'photo', ['photo_id'], ['id'], ondelete='CASCADE')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'photo_adjustment', type_='foreignkey')
|
||||||
|
op.create_foreign_key('photo_adjustment_ibfk_1', 'photo_adjustment', 'photo', ['photo_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
"""add photos to aircraft, poi and copilots
|
||||||
|
|
||||||
|
Revision ID: 91a9608b509d
|
||||||
|
Revises: d42172840f6c
|
||||||
|
Create Date: 2023-11-28 16:32:56.725312
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '91a9608b509d'
|
||||||
|
down_revision = 'd42172840f6c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('copilot_has_photo',
|
||||||
|
sa.Column('copilot_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('photo_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['photo_id'], ['photo.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('copilot_id', 'photo_id')
|
||||||
|
)
|
||||||
|
op.add_column('aircraft', sa.Column('title_photo_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'aircraft', 'photo', ['title_photo_id'], ['id'])
|
||||||
|
op.add_column('flight', sa.Column('title_photo_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight', 'photo', ['title_photo_id'], ['id'])
|
||||||
|
op.add_column('photo', sa.Column('aircraft_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'photo', 'aircraft', ['aircraft_id'], ['id'])
|
||||||
|
op.add_column('point_of_interest', sa.Column('title_photo_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'point_of_interest', 'photo', ['title_photo_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'point_of_interest', type_='foreignkey')
|
||||||
|
op.drop_column('point_of_interest', 'title_photo_id')
|
||||||
|
op.drop_constraint(None, 'photo', type_='foreignkey')
|
||||||
|
op.drop_column('photo', 'aircraft_id')
|
||||||
|
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||||
|
op.drop_column('flight', 'title_photo_id')
|
||||||
|
op.drop_constraint(None, 'aircraft', type_='foreignkey')
|
||||||
|
op.drop_column('aircraft', 'title_photo_id')
|
||||||
|
op.drop_table('copilot_has_photo')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""add is_public to aircraft
|
||||||
|
|
||||||
|
Revision ID: e5682ac4355c
|
||||||
|
Revises: 91a9608b509d
|
||||||
|
Create Date: 2023-12-02 12:14:25.055308
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e5682ac4355c'
|
||||||
|
down_revision = '91a9608b509d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('aircraft', sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('aircraft', 'is_public')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add title photo to copilot
|
||||||
|
|
||||||
|
Revision ID: 022bf6fbcd47
|
||||||
|
Revises: e5682ac4355c
|
||||||
|
Create Date: 2023-12-02 13:05:46.267006
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '022bf6fbcd47'
|
||||||
|
down_revision = 'e5682ac4355c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('copilot', sa.Column('title_photo_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'copilot', 'photo', ['title_photo_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'copilot', type_='foreignkey')
|
||||||
|
op.drop_column('copilot', 'title_photo_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""pridani slugu, odstraneni deprecated sloupcu, pridani defaultu ke jmenu a popisumm
|
||||||
|
|
||||||
|
Revision ID: 6648fb80dd0e
|
||||||
|
Revises: 022bf6fbcd47
|
||||||
|
Create Date: 2024-01-03 06:17:13.457060
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6648fb80dd0e'
|
||||||
|
down_revision = '022bf6fbcd47'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('aircraft', 'photo_filename')
|
||||||
|
op.add_column('airport', sa.Column('airport_type', sa.Enum('airport', 'ull', 'heliport'), server_default='airport', nullable=False))
|
||||||
|
op.add_column('airport', sa.Column('use_in_gpx_guess', sa.Boolean(), server_default='1', nullable=False))
|
||||||
|
op.add_column('copilot', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||||
|
op.add_column('copilot', sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
op.add_column('event', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||||
|
op.add_column('flight', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||||
|
op.alter_column('flight', 'takeoff_airport_id',
|
||||||
|
existing_type=mysql.INTEGER(display_width=11),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('flight', 'landing_airport_id',
|
||||||
|
existing_type=mysql.INTEGER(display_width=11),
|
||||||
|
nullable=True)
|
||||||
|
op.add_column('photo', sa.Column('filename_extension', sa.String(length=4), nullable=False))
|
||||||
|
op.add_column('photo', sa.Column('cache_key', sa.String(length=128), nullable=True))
|
||||||
|
op.drop_column('photo', 'is_flight_cover')
|
||||||
|
op.add_column('point_of_interest', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('point_of_interest', 'url_slug')
|
||||||
|
op.add_column('photo', sa.Column('is_flight_cover', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||||
|
op.drop_column('photo', 'cache_key')
|
||||||
|
op.drop_column('photo', 'filename_extension')
|
||||||
|
op.alter_column('flight', 'landing_airport_id',
|
||||||
|
existing_type=mysql.INTEGER(display_width=11),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('flight', 'takeoff_airport_id',
|
||||||
|
existing_type=mysql.INTEGER(display_width=11),
|
||||||
|
nullable=False)
|
||||||
|
op.drop_column('flight', 'url_slug')
|
||||||
|
op.drop_column('event', 'url_slug')
|
||||||
|
op.drop_column('copilot', 'is_public')
|
||||||
|
op.drop_column('copilot', 'url_slug')
|
||||||
|
op.drop_column('airport', 'use_in_gpx_guess')
|
||||||
|
op.drop_column('airport', 'airport_type')
|
||||||
|
op.add_column('aircraft', sa.Column('photo_filename', mysql.VARCHAR(length=128), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""add flight.debriefind_notes field
|
||||||
|
|
||||||
|
Revision ID: 39ebf5a1ab7e
|
||||||
|
Revises: 6648fb80dd0e
|
||||||
|
Create Date: 2024-01-20 16:22:00.487226
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '39ebf5a1ab7e'
|
||||||
|
down_revision = '6648fb80dd0e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight', sa.Column('debriefing_notes', sa.Text(), server_default='', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('flight', 'debriefing_notes')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""add password_reset_token table
|
||||||
|
|
||||||
|
Revision ID: a410455fb3bb
|
||||||
|
Revises: 39ebf5a1ab7e
|
||||||
|
Create Date: 2024-02-08 07:59:02.066212
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a410455fb3bb'
|
||||||
|
down_revision = '39ebf5a1ab7e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('password_reset_token',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('token', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('valid_until', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('password_reset_token')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""add flight_has_copilot.token
|
||||||
|
|
||||||
|
Revision ID: 7dd139549851
|
||||||
|
Revises: a410455fb3bb
|
||||||
|
Create Date: 2024-03-02 15:02:26.594830
|
||||||
|
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '7dd139549851'
|
||||||
|
down_revision = 'a410455fb3bb'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def rand_str():
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join([random.choice(alphabet) for _ in range(64)])
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_has_copilot', sa.Column('token', sa.String(length=64), nullable=True))
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
data = conn.execute(text("SELECT flight_id, copilot_id FROM flight_has_copilot"))
|
||||||
|
for flight_id, copilot_id in data:
|
||||||
|
print(flight_id, copilot_id)
|
||||||
|
conn.execute(text(
|
||||||
|
f"UPDATE flight_has_copilot SET token='{rand_str()}'"
|
||||||
|
f" WHERE flight_id={flight_id} AND copilot_id={copilot_id}"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
op.create_unique_constraint(None, 'flight_has_copilot', ['token'])
|
||||||
|
op.add_column('photo', sa.Column('created_by_copilot_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'photo', 'copilot', ['created_by_copilot_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('photo_ibfk_5', 'photo', type_='foreignkey')
|
||||||
|
op.drop_column('photo', 'created_by_copilot_id')
|
||||||
|
op.drop_constraint('token', 'flight_has_copilot', type_='unique')
|
||||||
|
op.drop_column('flight_has_copilot', 'token')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Add airspace_item table
|
||||||
|
|
||||||
|
Revision ID: aba4c3876b28
|
||||||
|
Revises: 7dd139549851
|
||||||
|
Create Date: 2024-03-18 10:05:57.516697
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'aba4c3876b28'
|
||||||
|
down_revision = '7dd139549851'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('airspace_item',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('country', sa.String(length=8), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('type', sa.String(length=8), nullable=False),
|
||||||
|
sa.Column('lower_limit', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('upper_limit', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('center', mysql.JSON(), nullable=False),
|
||||||
|
sa.Column('radius', sa.Float(), nullable=True),
|
||||||
|
sa.Column('bounds', mysql.JSON(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_airspace_item_country'), 'airspace_item', ['country'], unique=False)
|
||||||
|
op.create_index(op.f('ix_airspace_item_type'), 'airspace_item', ['type'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_airspace_item_type'), table_name='airspace_item')
|
||||||
|
op.drop_index(op.f('ix_airspace_item_country'), table_name='airspace_item')
|
||||||
|
op.drop_table('airspace_item')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""add flight_plan table
|
||||||
|
|
||||||
|
Revision ID: 7a3738a805c5
|
||||||
|
Revises: aba4c3876b28
|
||||||
|
Create Date: 2024-03-21 16:42:08.428875
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '7a3738a805c5'
|
||||||
|
down_revision = 'aba4c3876b28'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('flight_plan',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('markers', mysql.JSON(), nullable=False),
|
||||||
|
sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('deleted', sa.Boolean(), server_default='0', nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('flight_plan')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
"""make markers in flight planner separate table
|
||||||
|
|
||||||
|
Revision ID: de70fc471699
|
||||||
|
Revises: 7a3738a805c5
|
||||||
|
Create Date: 2024-03-24 21:55:12.307588
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'de70fc471699'
|
||||||
|
down_revision = '7a3738a805c5'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('flight_plan_marker',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('flight_plan_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('gps_latitude', sa.Float(), nullable=True),
|
||||||
|
sa.Column('gps_longitude', sa.Float(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.drop_column('flight_plan', 'markers')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_plan', sa.Column('markers', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False))
|
||||||
|
op.drop_table('flight_plan_marker')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
"""add type and position to flight_plan_marker
|
||||||
|
|
||||||
|
Revision ID: 70152c47cb49
|
||||||
|
Revises: de70fc471699
|
||||||
|
Create Date: 2024-03-24 22:00:35.234920
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '70152c47cb49'
|
||||||
|
down_revision = 'de70fc471699'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('type', sa.String(length=64), nullable=False))
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('position', sa.Integer(), nullable=False))
|
||||||
|
op.alter_column('flight_plan_marker', 'gps_latitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('flight_plan_marker', 'gps_longitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('flight_plan_marker', 'gps_longitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('flight_plan_marker', 'gps_latitude',
|
||||||
|
existing_type=mysql.FLOAT(),
|
||||||
|
nullable=True)
|
||||||
|
op.drop_column('flight_plan_marker', 'position')
|
||||||
|
op.drop_column('flight_plan_marker', 'type')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""reworked airspace and airport db structure
|
||||||
|
|
||||||
|
Revision ID: cbdb018c5490
|
||||||
|
Revises: 70152c47cb49
|
||||||
|
Create Date: 2024-04-21 22:32:50.058166
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'cbdb018c5490'
|
||||||
|
down_revision = '70152c47cb49'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('airspace',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('country', sa.String(length=8), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('type', sa.String(length=8), nullable=False),
|
||||||
|
sa.Column('lower_limit', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('upper_limit', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('center', mysql.JSON(), nullable=False),
|
||||||
|
sa.Column('radius', sa.Float(), nullable=True),
|
||||||
|
sa.Column('bounds', mysql.JSON(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_airspace_country'), 'airspace', ['country'], unique=False)
|
||||||
|
op.create_index(op.f('ix_airspace_type'), 'airspace', ['type'], unique=False)
|
||||||
|
op.create_table('frequency',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('airport_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('airspace_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('frequency', sa.String(length=7), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['airspace_id'], ['airspace.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('runway',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('airport_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('heading', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('designator', sa.String(length=4), nullable=False),
|
||||||
|
sa.Column('surface', sa.Enum('asphalt', 'concrete', 'grass', 'other'), server_default='other', nullable=False),
|
||||||
|
sa.Column('length', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('width', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.drop_table('metar')
|
||||||
|
op.drop_table('license')
|
||||||
|
op.drop_index('ix_airspace_item_country', table_name='airspace_item')
|
||||||
|
op.drop_index('ix_airspace_item_type', table_name='airspace_item')
|
||||||
|
op.drop_index('name', table_name='airspace_item')
|
||||||
|
op.drop_table('airspace_item')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('airspace_item',
|
||||||
|
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('country', mysql.VARCHAR(length=8), nullable=False),
|
||||||
|
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
|
||||||
|
sa.Column('type', mysql.VARCHAR(length=8), nullable=False),
|
||||||
|
sa.Column('lower_limit', mysql.VARCHAR(length=32), nullable=False),
|
||||||
|
sa.Column('upper_limit', mysql.VARCHAR(length=32), nullable=False),
|
||||||
|
sa.Column('center', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False),
|
||||||
|
sa.Column('radius', mysql.FLOAT(), nullable=True),
|
||||||
|
sa.Column('bounds', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_collate='utf8mb4_general_ci',
|
||||||
|
mysql_default_charset='utf8mb4',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.create_index('name', 'airspace_item', ['name'], unique=True)
|
||||||
|
op.create_index('ix_airspace_item_type', 'airspace_item', ['type'], unique=False)
|
||||||
|
op.create_index('ix_airspace_item_country', 'airspace_item', ['country'], unique=False)
|
||||||
|
op.create_table('license',
|
||||||
|
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
|
||||||
|
sa.Column('number', mysql.VARCHAR(length=30), nullable=False),
|
||||||
|
sa.Column('valid_until', mysql.DATETIME(), nullable=False),
|
||||||
|
sa.Column('created_by_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('created_at', mysql.DATETIME(), server_default=sa.text('current_timestamp()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], name='license_ibfk_1'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_collate='utf8mb4_general_ci',
|
||||||
|
mysql_default_charset='utf8mb4',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.create_table('metar',
|
||||||
|
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('airport_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('metar', mysql.TEXT(), nullable=False),
|
||||||
|
sa.Column('issued_at', mysql.DATETIME(), nullable=False),
|
||||||
|
sa.Column('deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], name='metar_ibfk_1'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_collate='utf8mb4_general_ci',
|
||||||
|
mysql_default_charset='utf8mb4',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.drop_table('runway')
|
||||||
|
op.drop_table('frequency')
|
||||||
|
op.drop_index(op.f('ix_airspace_type'), table_name='airspace')
|
||||||
|
op.drop_index(op.f('ix_airspace_country'), table_name='airspace')
|
||||||
|
op.drop_table('airspace')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""remove old airspace columns, add altitude unit
|
||||||
|
|
||||||
|
Revision ID: e07a5c0f1dcc
|
||||||
|
Revises: cbdb018c5490
|
||||||
|
Create Date: 2024-04-22 23:42:52.140389
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e07a5c0f1dcc'
|
||||||
|
down_revision = 'cbdb018c5490'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('airspace', sa.Column('altitude_unit', sa.String(length=8), nullable=False))
|
||||||
|
op.drop_column('airspace', 'center')
|
||||||
|
op.drop_column('airspace', 'radius')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('airspace', sa.Column('radius', mysql.FLOAT(), nullable=True))
|
||||||
|
op.add_column('airspace', sa.Column('center', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False))
|
||||||
|
op.drop_column('airspace', 'altitude_unit')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
"""add alt unit for lower/upper limits in airspaces
|
||||||
|
|
||||||
|
Revision ID: 2012eca075c7
|
||||||
|
Revises: e07a5c0f1dcc
|
||||||
|
Create Date: 2024-04-22 23:49:17.725111
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2012eca075c7'
|
||||||
|
down_revision = 'e07a5c0f1dcc'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('airspace', sa.Column('lower_alt_unit', sa.String(length=8), nullable=False))
|
||||||
|
op.add_column('airspace', sa.Column('upper_alt_unit', sa.String(length=8), nullable=False))
|
||||||
|
op.drop_column('airspace', 'altitude_unit')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('airspace', sa.Column('altitude_unit', mysql.VARCHAR(length=8), nullable=False))
|
||||||
|
op.drop_column('airspace', 'upper_alt_unit')
|
||||||
|
op.drop_column('airspace', 'lower_alt_unit')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""add table for reporting points
|
||||||
|
|
||||||
|
Revision ID: 03ba4dfea911
|
||||||
|
Revises: 2012eca075c7
|
||||||
|
Create Date: 2024-04-23 21:39:18.254033
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '03ba4dfea911'
|
||||||
|
down_revision = '2012eca075c7'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('reporting_point',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('openaip_id', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('gps_latitude', sa.Float(), nullable=True),
|
||||||
|
sa.Column('gps_longitude', sa.Float(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('airport_has_reporting_point',
|
||||||
|
sa.Column('airport_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('reporting_point_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['reporting_point_id'], ['reporting_point.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('airport_id', 'reporting_point_id')
|
||||||
|
)
|
||||||
|
op.add_column('airport', sa.Column('openaip_id', sa.String(length=32), nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('airport', 'openaip_id')
|
||||||
|
op.drop_table('airport_has_reporting_point')
|
||||||
|
op.drop_table('reporting_point')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
"""remove obsolete airport columns
|
||||||
|
|
||||||
|
Revision ID: f7ab4bdac987
|
||||||
|
Revises: 03ba4dfea911
|
||||||
|
Create Date: 2024-04-24 06:24:48.219726
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f7ab4bdac987'
|
||||||
|
down_revision = '03ba4dfea911'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('airport', 'deleted')
|
||||||
|
op.drop_column('airport', 'is_public')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('airport', sa.Column('is_public', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||||
|
op.add_column('airport', sa.Column('deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""add flight plan meta information
|
||||||
|
|
||||||
|
Revision ID: 790ba00fc021
|
||||||
|
Revises: f7ab4bdac987
|
||||||
|
Create Date: 2024-04-29 16:40:09.829317
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '790ba00fc021'
|
||||||
|
down_revision = 'f7ab4bdac987'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('flight_plan_has_copilot',
|
||||||
|
sa.Column('flight_plan_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('copilot_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('token', sa.String(length=64), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('flight_plan_id', 'copilot_id'),
|
||||||
|
sa.UniqueConstraint('token')
|
||||||
|
)
|
||||||
|
op.add_column('flight_plan', sa.Column('is_default_name', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
op.add_column('flight_plan', sa.Column('is_editable_by_copilot', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
op.add_column('flight_plan', sa.Column('planned_speed', sa.Integer(), nullable=False))
|
||||||
|
op.add_column('flight_plan', sa.Column('planned_takeoff_datetime', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('flight_plan', sa.Column('aircraft_id', sa.Integer(), nullable=True))
|
||||||
|
op.alter_column('flight_plan', 'name',
|
||||||
|
existing_type=mysql.VARCHAR(length=128),
|
||||||
|
type_=sa.String(length=256),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.create_foreign_key(None, 'flight_plan', 'aircraft', ['aircraft_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
|
||||||
|
op.alter_column('flight_plan', 'name',
|
||||||
|
existing_type=sa.String(length=256),
|
||||||
|
type_=mysql.VARCHAR(length=128),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.drop_column('flight_plan', 'aircraft_id')
|
||||||
|
op.drop_column('flight_plan', 'planned_takeoff_datetime')
|
||||||
|
op.drop_column('flight_plan', 'planned_speed')
|
||||||
|
op.drop_column('flight_plan', 'is_editable_by_copilot')
|
||||||
|
op.drop_column('flight_plan', 'is_default_name')
|
||||||
|
op.drop_table('flight_plan_has_copilot')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
"""add weather info to flight plan markers
|
||||||
|
|
||||||
|
Revision ID: 8a67af3909aa
|
||||||
|
Revises: 790ba00fc021
|
||||||
|
Create Date: 2024-05-16 06:19:13.112911
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8a67af3909aa'
|
||||||
|
down_revision = '790ba00fc021'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('weather_info_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight_plan_marker', 'weather_info', ['weather_info_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||||
|
op.drop_column('flight_plan_marker', 'weather_info_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""add flight_plan.description
|
||||||
|
|
||||||
|
Revision ID: a3e1f1a37899
|
||||||
|
Revises: 8a67af3909aa
|
||||||
|
Create Date: 2024-06-11 16:46:29.786830
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a3e1f1a37899'
|
||||||
|
down_revision = '8a67af3909aa'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_plan', sa.Column('description', sa.Text(), server_default='', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('flight_plan', 'description')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""add track to flight
|
||||||
|
|
||||||
|
Revision ID: f9e1e8a01683
|
||||||
|
Revises: a3e1f1a37899
|
||||||
|
Create Date: 2024-06-24 16:08:01.084570
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f9e1e8a01683'
|
||||||
|
down_revision = 'a3e1f1a37899'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('track',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('bounds', mysql.JSON(), nullable=False),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('track_point',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('track_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('gps_latitude', sa.Float(), nullable=False),
|
||||||
|
sa.Column('gps_longitude', sa.Float(), nullable=False),
|
||||||
|
sa.Column('elevation', sa.Float(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['track_id'], ['track.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.add_column('flight', sa.Column('track_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight', 'track', ['track_id'], ['id'])
|
||||||
|
op.add_column('flight_plan', sa.Column('track_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight_plan', 'track', ['track_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
|
||||||
|
op.drop_column('flight_plan', 'track_id')
|
||||||
|
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||||
|
op.drop_column('flight', 'track_id')
|
||||||
|
op.drop_table('track_point')
|
||||||
|
op.drop_table('track')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add flight.flight_plan_id fk
|
||||||
|
|
||||||
|
Revision ID: 989152b7184a
|
||||||
|
Revises: f9e1e8a01683
|
||||||
|
Create Date: 2024-06-27 17:09:19.933094
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '989152b7184a'
|
||||||
|
down_revision = 'f9e1e8a01683'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight', sa.Column('flight_plan_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight', 'flight_plan', ['flight_plan_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||||
|
op.drop_column('flight', 'flight_plan_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
"""Add poi and airport id to flight plan track marker
|
||||||
|
|
||||||
|
Revision ID: 8cc01c03e980
|
||||||
|
Revises: 989152b7184a
|
||||||
|
Create Date: 2024-06-30 00:27:08.976891
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8cc01c03e980'
|
||||||
|
down_revision = '989152b7184a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('airport', 'openaip_id',
|
||||||
|
existing_type=mysql.VARCHAR(length=32),
|
||||||
|
nullable=True)
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('airport_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('point_of_interest_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight_plan_marker', 'point_of_interest', ['point_of_interest_id'], ['id'])
|
||||||
|
op.create_foreign_key(None, 'flight_plan_marker', 'airport', ['airport_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||||
|
op.drop_column('flight_plan_marker', 'point_of_interest_id')
|
||||||
|
op.drop_column('flight_plan_marker', 'airport_id')
|
||||||
|
op.alter_column('airport', 'openaip_id',
|
||||||
|
existing_type=mysql.VARCHAR(length=32),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
"""add speed, elevation, altitude to track
|
||||||
|
|
||||||
|
Revision ID: 03de3f7f8fdd
|
||||||
|
Revises: 8cc01c03e980
|
||||||
|
Create Date: 2024-07-11 07:45:59.376941
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '03de3f7f8fdd'
|
||||||
|
down_revision = '8cc01c03e980'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('track', sa.Column('min_speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('max_speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('avg_speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('max_altitude', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('avg_altitude', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('total_duration', sa.Integer(), nullable=True, comment='Total duration in seconds'))
|
||||||
|
op.add_column('track_point', sa.Column('terrain_elevation', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track_point', sa.Column('speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track_point', sa.Column('altitude', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track_point', sa.Column('magnetic_variation', sa.Float(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('track_point', 'magnetic_variation')
|
||||||
|
op.drop_column('track_point', 'altitude')
|
||||||
|
op.drop_column('track_point', 'speed')
|
||||||
|
op.drop_column('track_point', 'terrain_elevation')
|
||||||
|
op.drop_column('track', 'total_duration')
|
||||||
|
op.drop_column('track', 'avg_altitude')
|
||||||
|
op.drop_column('track', 'max_altitude')
|
||||||
|
op.drop_column('track', 'avg_speed')
|
||||||
|
op.drop_column('track', 'max_speed')
|
||||||
|
op.drop_column('track', 'min_speed')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
"""delete duplicated column with altitude
|
||||||
|
|
||||||
|
Revision ID: e3620deb41b6
|
||||||
|
Revises: 03de3f7f8fdd
|
||||||
|
Create Date: 2024-07-15 06:35:13.536320
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e3620deb41b6'
|
||||||
|
down_revision = '03de3f7f8fdd'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.rename_table("flight_track", "flight_turn_point")
|
||||||
|
op.create_index(op.f('ix_track_point_timestamp'), 'track_point', ['timestamp'], unique=False)
|
||||||
|
op.drop_column('track_point', 'elevation')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('track_point', sa.Column('elevation', mysql.FLOAT(), nullable=True))
|
||||||
|
op.drop_index(op.f('ix_track_point_timestamp'), table_name='track_point')
|
||||||
|
op.rename_table("flight_turn_point", "flight_track")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""prejmenovani date_time u WeatherInfo
|
||||||
|
|
||||||
|
Revision ID: db036549f318
|
||||||
|
Revises: e3620deb41b6
|
||||||
|
Create Date: 2025-09-16 09:26:04.082396
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'db036549f318'
|
||||||
|
down_revision = 'e3620deb41b6'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_unique_constraint(None, 'airport', ['openaip_id'])
|
||||||
|
op.create_index(op.f('ix_flight_turn_point_order'), 'flight_turn_point', ['order'], unique=False)
|
||||||
|
op.alter_column(
|
||||||
|
"weather_info", "datetime", new_column_name="date_time", existing_type=sa.DateTime, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
"weather_info", "date_time", new_column_name="datetime",
|
||||||
|
existing_type=sa.DateTime, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_flight_turn_point_order'), table_name='flight_turn_point')
|
||||||
|
op.drop_constraint(None, 'airport', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# https://docs.gunicorn.org/en/stable/settings.html#settings
|
||||||
|
|
||||||
|
workers = 1 if os.environ.get("APP_ENV") == "development" else os.cpu_count()
|
||||||
|
bind = "0.0.0.0:8000"
|
||||||
|
worker_class = "uvicorn.workers.UvicornWorker"
|
||||||
|
worker_connections = 1000 # default
|
||||||
|
threads = 1 # default
|
||||||
|
|
||||||
|
# restart workeru
|
||||||
|
max_requests = 500000
|
||||||
|
max_requests_jitter = 10000
|
||||||
|
|
||||||
|
timeout = 30 # default
|
||||||
|
graceful_timeout = 30 # default
|
||||||
|
|
||||||
|
keepalive = 600 # s klientem udrzuje nginx
|
||||||
+17
-4
@@ -2,8 +2,16 @@ import csv
|
|||||||
|
|
||||||
|
|
||||||
def deg_to_dec(val: str):
|
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)
|
||||||
|
|
||||||
val = val[:-1].lstrip("0")
|
|
||||||
# h_m, s = val.split(".")
|
# h_m, s = val.split(".")
|
||||||
# h_m = h_m.strip("0")
|
# h_m = h_m.strip("0")
|
||||||
# h = int(h_m[0:2])
|
# h = int(h_m[0:2])
|
||||||
@@ -16,15 +24,20 @@ def deg_to_dec(val: str):
|
|||||||
# print("--konec-----------------------------------")
|
# print("--konec-----------------------------------")
|
||||||
# return h + (m / 60.0) + (int(s) / 3600.0)
|
# return h + (m / 60.0) + (int(s) / 3600.0)
|
||||||
|
|
||||||
return len(val or "")
|
# return val
|
||||||
|
|
||||||
|
# return len(val or "")
|
||||||
|
|
||||||
|
|
||||||
# ZDAKOV: @49.504378,14.1808905
|
# ZDAKOV: @49.504378,14.1808905
|
||||||
|
|
||||||
|
# 4930.250N / 100 -> cele cislo stupne, desetinne prevest do sedesatkove soustavy
|
||||||
|
|
||||||
with open("./poi.csv") as f:
|
with open("./poi.csv") as f:
|
||||||
reader = csv.DictReader(f)
|
reader = csv.DictReader(f)
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
# if row['name'] != 'ZDAKOV':
|
if row['name'] != 'ZDAKOV':
|
||||||
# continue
|
continue
|
||||||
|
|
||||||
print(row['name'], row['lat'], row['lon'], deg_to_dec(row['lat']), deg_to_dec(row['lon']))
|
print(row['name'], row['lat'], row['lon'], deg_to_dec(row['lat']), deg_to_dec(row['lon']))
|
||||||
@@ -3,10 +3,12 @@ version: "3.3"
|
|||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
image: "docker.kvacek.cz/poletani/api:latest"
|
image: "docker.kvacek.cz/poletani/api:latest"
|
||||||
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
|
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
restart: "always"
|
restart: "always"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ../.env
|
||||||
|
environment:
|
||||||
|
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
|
- ./static:/app/static
|
||||||
|
|||||||
+18
-10
@@ -3,12 +3,13 @@ services:
|
|||||||
image: mariadb:10.11
|
image: mariadb:10.11
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_USER=root
|
MARIADB_ROOT_PASSWORD: "rootroot"
|
||||||
- MYSQL_DATABASE=ull_tracker
|
|
||||||
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
- db-data:/var/lib/mysql:rw
|
- db-data:/var/lib/mysql:rw
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
@@ -17,21 +18,28 @@ services:
|
|||||||
- 8000:8000
|
- 8000:8000
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Prague"
|
TZ: "Europe/Prague"
|
||||||
API_URL: "http://localhost:8000"
|
API_URL: "http://localhost:8000"
|
||||||
APP_ENV: "development"
|
APP_ENV: "development"
|
||||||
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001"
|
MYSQL_USER: "root"
|
||||||
|
MYSQL_PASSWORD: "root"
|
||||||
|
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
|
||||||
|
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
|
||||||
scrapy:
|
scrapy:
|
||||||
build:
|
build:
|
||||||
context: ../crawlers
|
context: ../crawlers
|
||||||
environment:
|
env_file:
|
||||||
- MYSQL_USER=root
|
- .env
|
||||||
- MYSQL_PASSWORD=
|
|
||||||
- MYSQL_DATABASE=ull_tracker
|
|
||||||
- MYSQL_HOST=db
|
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/mysql:rw
|
- db-data:/var/lib/mysql:rw
|
||||||
- ../crawlers:/app
|
- ../crawlers:/app
|
||||||
|
|||||||
+21
-14
@@ -1,15 +1,22 @@
|
|||||||
fastapi==0.100.0
|
fastapi==0.115.7
|
||||||
fastapi-jwt==0.1.12
|
fastapi-jwt[authlib]==0.3.0
|
||||||
strawberry-graphql[fastapi]==0.194.4
|
bcrypt==4.2.1
|
||||||
uvicorn==0.22.0
|
fastapi-mail==1.4.2
|
||||||
sqlalchemy[asyncio] >= 2.0.9
|
strawberry-graphql[fastapi]==0.258.0
|
||||||
aiomysql==0.2.0
|
|
||||||
alembic==1.11.1
|
|
||||||
passlib==1.7.4
|
|
||||||
pydantic==1.10.11 # vysla uz 2.0, ale nejak mi to nefunguje
|
|
||||||
|
|
||||||
pillow
|
gunicorn == 23.0.0
|
||||||
exif
|
uvicorn[standard] == 0.34.0
|
||||||
aiocache
|
|
||||||
aiohttp
|
sqlalchemy[asyncio] == 2.0.54
|
||||||
lxml
|
aiomysql==0.2.0
|
||||||
|
alembic==1.14.1
|
||||||
|
passlib==1.7.4
|
||||||
|
pydantic==2.10.6
|
||||||
|
sentry-sdk[fastapi]
|
||||||
|
pillow==11.1.0
|
||||||
|
exif==1.6.1
|
||||||
|
aiocache==0.12.3
|
||||||
|
aiohttp==3.11.11
|
||||||
|
lxml==5.3.0
|
||||||
|
|
||||||
|
colorlog
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from decorators.retryable import retryable
|
||||||
|
from external.elevation import elevation_api, update_track_points_elevation
|
||||||
|
|
||||||
|
|
||||||
|
@retryable
|
||||||
|
async def add_terrain_elevation_to_flight(flight_id: int):
|
||||||
|
async with get_session() as db:
|
||||||
|
track_points = (await db.scalars(
|
||||||
|
select(models.TrackPoint)
|
||||||
|
.join(models.TrackPoint.track)
|
||||||
|
.join(models.Track.flight)
|
||||||
|
.filter(models.Flight.id == flight_id)
|
||||||
|
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||||
|
)).all()
|
||||||
|
await update_track_points_elevation(db, track_points)
|
||||||
|
|
||||||
|
|
||||||
|
@retryable
|
||||||
|
async def add_terrain_elevation_to_photo(photo):
|
||||||
|
try:
|
||||||
|
elevation = await elevation_api.get_elevation_for_points(
|
||||||
|
[{"lat": photo.gps_latitude, "lng": photo.gps_longitude}]
|
||||||
|
)
|
||||||
|
if not elevation:
|
||||||
|
print("Cannot get elevation")
|
||||||
|
return
|
||||||
|
|
||||||
|
terrain_elevation = elevation[0].elevation
|
||||||
|
async with get_session() as db:
|
||||||
|
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Cannot get elevation: {e}")
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from PIL import Image, ImageDraw, ImageOps
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
|
||||||
|
|
||||||
|
async def add_circular_avatar(base_image_path, avatar_image_path, output_image_path):
|
||||||
|
# Load the base image
|
||||||
|
base_image = Image.open(base_image_path)
|
||||||
|
base_width, base_height = base_image.size
|
||||||
|
|
||||||
|
# Calculate the new dimensions for cropping to a 1.9:1 aspect ratio
|
||||||
|
new_width = base_width
|
||||||
|
new_height = int(new_width / 1.9)
|
||||||
|
if new_height > base_height:
|
||||||
|
new_height = base_height
|
||||||
|
new_width = int(new_height * 1.9)
|
||||||
|
|
||||||
|
# Center crop the base image to the new dimensions
|
||||||
|
left = (base_width - new_width) // 2
|
||||||
|
top = (base_height - new_height) // 2
|
||||||
|
right = (base_width + new_width) // 2
|
||||||
|
bottom = (base_height + new_height) // 2
|
||||||
|
base_image = base_image.crop((left, top, right, bottom))
|
||||||
|
base_width, base_height = base_image.size
|
||||||
|
|
||||||
|
# Load the avatar image
|
||||||
|
avatar_image = Image.open(avatar_image_path)
|
||||||
|
avatar_size = int(base_height * 0.3) # Set the size of the avatar
|
||||||
|
avatar_image = avatar_image.resize((avatar_size, avatar_size), Image.LANCZOS)
|
||||||
|
|
||||||
|
# Create a mask to make the avatar circular
|
||||||
|
mask = Image.new('L', (avatar_size, avatar_size), 0)
|
||||||
|
draw = ImageDraw.Draw(mask)
|
||||||
|
draw.ellipse((0, 0, avatar_size, avatar_size), fill=255)
|
||||||
|
|
||||||
|
# Apply the mask to the avatar image
|
||||||
|
avatar_image = ImageOps.fit(avatar_image, mask.size, centering=(0.5, 0.5))
|
||||||
|
avatar_image.putalpha(mask)
|
||||||
|
|
||||||
|
# Create a border around the avatar
|
||||||
|
border_size = int(avatar_size * 0.03)
|
||||||
|
border_image = Image.new('RGBA', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), (255, 255, 255, 0))
|
||||||
|
border_mask = Image.new('L', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), 0)
|
||||||
|
border_draw = ImageDraw.Draw(border_mask)
|
||||||
|
border_draw.ellipse((0, 0, avatar_size + 2 * border_size, avatar_size + 2 * border_size), fill=255)
|
||||||
|
border_draw.ellipse((border_size, border_size, avatar_size + border_size, avatar_size + border_size), fill=0)
|
||||||
|
|
||||||
|
avatar_margin = int(avatar_size * 0.1)
|
||||||
|
|
||||||
|
border_image.putalpha(border_mask)
|
||||||
|
border_image.paste(avatar_image, (border_size, border_size), avatar_image)
|
||||||
|
|
||||||
|
# Calculate the position to paste the avatar (bottom right corner)
|
||||||
|
position = (base_width - avatar_size - 2 * border_size - avatar_margin, base_height - avatar_size - 2 * border_size - avatar_margin)
|
||||||
|
|
||||||
|
# Paste the avatar with border onto the base image
|
||||||
|
base_image.paste(border_image, position, border_image)
|
||||||
|
|
||||||
|
# Save the result
|
||||||
|
base_image.save(output_image_path)
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_flight_title_photo(flight_id: int, photo_id: int):
|
||||||
|
async with get_session() as db:
|
||||||
|
photo = await models.Photo.get_one(db, id=photo_id)
|
||||||
|
pilot = (await db.scalars(
|
||||||
|
select(models.User)
|
||||||
|
.select_from(models.Flight)
|
||||||
|
.join(models.Flight.created_by)
|
||||||
|
.filter(models.Flight.id == flight_id)
|
||||||
|
)).one()
|
||||||
|
|
||||||
|
photo_filename, photo_ext = photo.filename, photo.filename_extension
|
||||||
|
pilot_id, avatar_filename = pilot.id, pilot.avatar_image_filename
|
||||||
|
|
||||||
|
await add_circular_avatar(
|
||||||
|
base_image_path=f'/app/uploads/photos/{flight_id}/{photo_filename}.{photo_ext}',
|
||||||
|
avatar_image_path=f'/app/uploads/profile/{pilot_id}/{avatar_filename}',
|
||||||
|
output_image_path=f'/app/uploads/photos/{flight_id}/title_photo.jpg',
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import os.path
|
||||||
|
from time import time
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from utils.image import PhotoEditor
|
||||||
|
|
||||||
|
|
||||||
|
async def resize_photo(path: str, filename: str, photo_id: int):
|
||||||
|
editor = PhotoEditor(path, filename)
|
||||||
|
editor.resize(new_width=2500)
|
||||||
|
|
||||||
|
name, _ = os.path.splitext(filename)
|
||||||
|
editor.write_to_file(quality=95, format_="webp", dest_filename=f"{name}.webp")
|
||||||
|
editor.write_to_file(quality=80) # toto je potreba pro prvni nacteni nahledu ihned po nahrani, pripadne pro
|
||||||
|
# vygenerovani nahledu (async, muze se delat pred/behem zmensovani fotky -> v tu dobu jeste neexistuje webp)
|
||||||
|
width, height = editor.img_size
|
||||||
|
|
||||||
|
# TODO: doresit uklid JPGu -> jsou zbytecne
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
await models.Photo.update(
|
||||||
|
db,
|
||||||
|
id=photo_id,
|
||||||
|
data={
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"filename_extension": "webp",
|
||||||
|
"cache_key": int(time())
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_thumbnail(path: str, filename: str):
|
||||||
|
editor = PhotoEditor(path, filename)
|
||||||
|
editor.resize(new_width=300)
|
||||||
|
|
||||||
|
name, _ = os.path.splitext(filename)
|
||||||
|
editor.write_to_file(
|
||||||
|
quality=85,
|
||||||
|
dest_path=f"{path}/thumbs",
|
||||||
|
dest_filename=f"{name}.webp",
|
||||||
|
format_="webp")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from emails.emails import Email
|
||||||
|
from mailer import Mailer
|
||||||
|
|
||||||
|
|
||||||
|
async def send_email(email: Email):
|
||||||
|
mailer = Mailer()
|
||||||
|
return await mailer.send(email)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal, Any
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from external.weather import weather_api
|
||||||
|
from logger import log
|
||||||
|
|
||||||
|
|
||||||
|
async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None:
|
||||||
|
try:
|
||||||
|
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error in downloading weather: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
|
||||||
|
"qnh": weather['pressure_msl'],
|
||||||
|
"temperature_surface": weather['temperature_2m'],
|
||||||
|
"dewpoint_surface": weather['dewpoint_2m'],
|
||||||
|
"rain": weather['rain'],
|
||||||
|
"cloudcover_total": weather['cloudcover'],
|
||||||
|
"cloudcover_low": weather['cloudcover_low'],
|
||||||
|
"wind_speed_surface": weather['windspeed_10m'],
|
||||||
|
"wind_direction_surface": weather['winddirection_10m'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def download_weather_for_flight(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
||||||
|
async with get_session() as db:
|
||||||
|
airport = await models.Airport.get_one(db, id=airport_id)
|
||||||
|
gps = (airport.gps_latitude, airport.gps_longitude)
|
||||||
|
|
||||||
|
data = await get_weather(date_time, gps)
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
flight = await models.Flight.get_one(db, id=flight_id)
|
||||||
|
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
|
||||||
|
|
||||||
|
if existing_weather_id:
|
||||||
|
await models.WeatherInfo.update(db, id=existing_weather_id, data=data)
|
||||||
|
else:
|
||||||
|
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||||
|
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
||||||
|
|
||||||
|
|
||||||
|
async def download_weather_for_plan_marker(date_time: datetime, marker_id: int):
|
||||||
|
async with get_session() as db:
|
||||||
|
marker = await models.FlightPlanMarker.get_one(db, id=marker_id)
|
||||||
|
|
||||||
|
if not marker.gps_latitude or not marker.gps_longitude:
|
||||||
|
return
|
||||||
|
|
||||||
|
gps = (marker.gps_latitude, marker.gps_longitude)
|
||||||
|
marker_weather_info_id = marker.weather_info_id
|
||||||
|
|
||||||
|
data = await get_weather(date_time, gps)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
if marker_weather_info_id:
|
||||||
|
await models.WeatherInfo.update(db, id=marker_weather_info_id, data=data)
|
||||||
|
else:
|
||||||
|
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||||
|
await models.FlightPlanMarker.update(
|
||||||
|
db,
|
||||||
|
id=marker_id,
|
||||||
|
data={"weather_info_id": weather_model.id}
|
||||||
|
)
|
||||||
+12
-2
@@ -1,10 +1,20 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
APP_DEBUG = True
|
APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
|
||||||
GRAPHIQL = True
|
GRAPHIQL = True
|
||||||
|
|
||||||
|
REFRESH_TOKEN_VALIDITY_DAYS = 30
|
||||||
|
|
||||||
API_URL = os.environ.get("API_URL") or "http://localhost:8000"
|
API_URL = os.environ.get("API_URL") or "http://localhost:8000"
|
||||||
APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") or "test"
|
|
||||||
ALLOW_CORS_ORIGINS = os.environ.get("ALLOW_CORS_ORIGINS", "").split()
|
ALLOW_CORS_ORIGINS = os.environ.get("ALLOW_CORS_ORIGINS", "").split()
|
||||||
|
SENTRY_DSN = os.environ.get("SENTRY_DSN")
|
||||||
|
|
||||||
|
APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") or "test"
|
||||||
|
|
||||||
|
SMTP_HOST = os.environ.get("SMTP_HOST")
|
||||||
|
SMTP_PORT = int(os.environ.get("SMTP_PORT") or 0)
|
||||||
|
SMTP_USERNAME = os.environ.get("SMTP_USERNAME")
|
||||||
|
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD")
|
||||||
|
|
||||||
if not APP_SECRET_KEY:
|
if not APP_SECRET_KEY:
|
||||||
raise ValueError("Missing APP_SECRET_KEY!")
|
raise ValueError("Missing APP_SECRET_KEY!")
|
||||||
|
|||||||
@@ -1,299 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
import datetime
|
|
||||||
from typing import Set, List
|
|
||||||
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float
|
|
||||||
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
|
|
||||||
@as_declarative()
|
|
||||||
class BaseModel:
|
|
||||||
excluded_columns_in_dict = tuple()
|
|
||||||
|
|
||||||
def as_dict(self):
|
|
||||||
return {
|
|
||||||
c.name: getattr(self, c.name)
|
|
||||||
for c in self.__table__.columns
|
|
||||||
if c.name not in self.excluded_columns_in_dict
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_one(cls, db_session: AsyncSession, id: int):
|
|
||||||
return (await db_session.scalars(select(cls).filter_by(id=id))).one()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, db_session: AsyncSession, data: dict):
|
|
||||||
model = cls(**data)
|
|
||||||
db_session.add(model)
|
|
||||||
await db_session.flush()
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
|
|
||||||
if not obj and not id:
|
|
||||||
raise ValueError("Provide either obj or id!")
|
|
||||||
|
|
||||||
if not obj:
|
|
||||||
obj = await cls.get_one(db_session, id)
|
|
||||||
for key, value in data.items():
|
|
||||||
if getattr(obj, key) != value:
|
|
||||||
setattr(obj, key, value)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
user_is_in_organization = Table(
|
|
||||||
"user_is_in_organization",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
|
|
||||||
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
flight_has_copilot = Table(
|
|
||||||
"flight_has_copilot",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
|
||||||
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Airport(BaseModel):
|
|
||||||
__tablename__ = "airport"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
|
||||||
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
metars: Mapped['Metar'] = relationship(back_populates="airport")
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class PointOfInterestType(BaseModel):
|
|
||||||
__tablename__ = "point_of_interest_type"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
points_of_interest: Mapped[List[PointOfInterest]] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class PointOfInterest(BaseModel):
|
|
||||||
__tablename__ = "point_of_interest"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
photos: Mapped[List[Photo]] = relationship()
|
|
||||||
type: Mapped[PointOfInterestType] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Photo(BaseModel):
|
|
||||||
__tablename__ = "photo"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
filename: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0")
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
|
||||||
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
|
||||||
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Aircraft(BaseModel):
|
|
||||||
__tablename__ = "aircraft"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
photo_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
|
||||||
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
|
|
||||||
seats: Mapped[str] = mapped_column(Integer, nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
|
||||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
organization: Mapped['Organization'] = relationship()
|
|
||||||
flights: Mapped[Set['Flight']] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
notes: Mapped['AircraftNotes'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class AircraftNotes(BaseModel):
|
|
||||||
__tablename__ = "aircraft_notes"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=False)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
aircraft: Mapped['Aircraft'] = relationship(back_populates="notes")
|
|
||||||
|
|
||||||
|
|
||||||
class Organization(BaseModel):
|
|
||||||
__tablename__ = "organization"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
users: Mapped[Set['User']] = relationship(back_populates='organizations', secondary=user_is_in_organization)
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class FlightTrack(BaseModel):
|
|
||||||
__tablename__ = "flight_track"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
|
||||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=False)
|
|
||||||
order: Mapped[int] = mapped_column(Integer)
|
|
||||||
|
|
||||||
flight: Mapped['Flight'] = relationship()
|
|
||||||
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class WeatherInfo(BaseModel):
|
|
||||||
__tablename__ = "weather_info"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
temperature_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
dewpoint_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
rain: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
cloudcover_low: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
cloudcover_total: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
wind_speed_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
wind_direction_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class Flight(BaseModel):
|
|
||||||
__tablename__ = "flight"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
|
|
||||||
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
|
|
||||||
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
||||||
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
||||||
duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
|
|
||||||
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
|
||||||
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
|
||||||
landings: Mapped[int] = mapped_column(Integer, default=1)
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
|
|
||||||
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
|
|
||||||
weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id])
|
|
||||||
weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id])
|
|
||||||
track: Mapped['FlightTrack'] = relationship()
|
|
||||||
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot)
|
|
||||||
aircraft: Mapped['Aircraft'] = relationship(back_populates="flights")
|
|
||||||
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Copilot(BaseModel):
|
|
||||||
__tablename__ = "copilot"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Metar(BaseModel):
|
|
||||||
__tablename__ = "metar"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
|
||||||
metar: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
issued_at: Mapped[datetime] = mapped_column(DateTime)
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
airport: Mapped['Airport'] = relationship(back_populates="metars")
|
|
||||||
|
|
||||||
|
|
||||||
class License(BaseModel):
|
|
||||||
__tablename__ = "license"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
number: Mapped[str] = mapped_column(String(30), nullable=False)
|
|
||||||
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
user: Mapped['User'] = relationship(back_populates="licences")
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
__tablename__ = "user"
|
|
||||||
excluded_columns_in_dict = ('password_hashed',)
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
|
|
||||||
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
licences: Mapped[Set['License']] = relationship()
|
|
||||||
flights: Mapped[Set['Flight']] = relationship()
|
|
||||||
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from database.models.airport import Airport, Runway, ReportingPoint, airport_has_reporting_point, Frequency
|
||||||
|
from database.models.aircraft import Aircraft
|
||||||
|
from database.models.airspace import Airspace
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.copilot import Copilot, copilot_has_photo
|
||||||
|
from database.models.event import Event
|
||||||
|
from database.models.flight import Flight, FlightTurnPoint, Track, TrackPoint, flight_has_copilot
|
||||||
|
from database.models.flight_plan import FlightPlan, flight_plan_has_copilot, FlightPlanMarker
|
||||||
|
from database.models.orgnization import Organization
|
||||||
|
from database.models.password_reset_token import PasswordResetToken
|
||||||
|
from database.models.photo import PhotoAdjustment, Photo
|
||||||
|
from database.models.point_of_interest import PointOfInterest, PointOfInterestType
|
||||||
|
from database.models.user import User, user_is_in_organization
|
||||||
|
from database.models.weather_info import WeatherInfo
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Aircraft",
|
||||||
|
"Airport",
|
||||||
|
"Runway",
|
||||||
|
"ReportingPoint",
|
||||||
|
"Airspace",
|
||||||
|
"BaseModel",
|
||||||
|
"Copilot",
|
||||||
|
"Event",
|
||||||
|
"Flight",
|
||||||
|
"FlightTurnPoint",
|
||||||
|
"FlightPlan",
|
||||||
|
"FlightPlanMarker",
|
||||||
|
"Frequency",
|
||||||
|
"Track",
|
||||||
|
"TrackPoint",
|
||||||
|
"Organization",
|
||||||
|
"PasswordResetToken",
|
||||||
|
"Photo",
|
||||||
|
"PhotoAdjustment",
|
||||||
|
"PointOfInterest",
|
||||||
|
"PointOfInterestType",
|
||||||
|
"WeatherInfo",
|
||||||
|
"User",
|
||||||
|
"airport_has_reporting_point",
|
||||||
|
"flight_has_copilot",
|
||||||
|
"flight_plan_has_copilot",
|
||||||
|
"user_is_in_organization",
|
||||||
|
"copilot_has_photo",
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Aircraft(BaseModel):
|
||||||
|
__tablename__ = "aircraft"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
|
||||||
|
seats: Mapped[str] = mapped_column(Integer, nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
organization: Mapped['Organization'] = relationship()
|
||||||
|
flights: Mapped[set['Flight']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, Enum
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
airport_has_reporting_point = Table(
|
||||||
|
"airport_has_reporting_point",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
|
||||||
|
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Airport(BaseModel):
|
||||||
|
__tablename__ = "airport"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
|
||||||
|
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
airport_type: Mapped[str] = mapped_column(
|
||||||
|
Enum("airport", "ull", "heliport"),
|
||||||
|
nullable=False,
|
||||||
|
server_default='airport'
|
||||||
|
)
|
||||||
|
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa
|
||||||
|
|
||||||
|
runways: Mapped[list['Runway']] = relationship('Runway')
|
||||||
|
frequencies: Mapped[list['Frequency']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Runway(BaseModel):
|
||||||
|
__tablename__ = "runway"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
||||||
|
heading: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
designator: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||||
|
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
|
||||||
|
length: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
width: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Frequency(BaseModel):
|
||||||
|
__tablename__ = "frequency"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
|
||||||
|
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
|
||||||
|
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportingPoint(BaseModel):
|
||||||
|
__tablename__ = "reporting_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from sqlalchemy import String, JSON
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Airspace(BaseModel):
|
||||||
|
__tablename__ = "airspace"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||||
|
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(DeclarativeBase):
|
||||||
|
excluded_columns_in_dict = ("deleted",)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_column_names(cls):
|
||||||
|
return [col.name for col in cls.__table__.columns]
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_one(cls, db_session: AsyncSession, **kwargs):
|
||||||
|
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, db_session: AsyncSession, data: dict):
|
||||||
|
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
|
||||||
|
db_session.add(model)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
|
||||||
|
if not obj and not id:
|
||||||
|
raise ValueError("Provide either obj or id!")
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
obj = await cls.get_one(db_session, id=id)
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in cls._get_column_names() and getattr(obj, key) != value:
|
||||||
|
setattr(obj, key, value)
|
||||||
|
|
||||||
|
return obj
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, DateTime, func
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.flight import flight_has_copilot
|
||||||
|
from database.models.flight_plan import flight_plan_has_copilot
|
||||||
|
|
||||||
|
copilot_has_photo = Table(
|
||||||
|
"copilot_has_photo",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Copilot(BaseModel):
|
||||||
|
__tablename__ = "copilot"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
flights: Mapped[set['Flight']] = relationship(secondary=flight_has_copilot)
|
||||||
|
flight_plans: Mapped[set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
photos: Mapped[list['Photo']] = relationship(secondary=copilot_has_photo)
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
__tablename__ = "event"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
organization: Mapped['Organization'] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, JSON
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
flight_has_copilot = Table(
|
||||||
|
"flight_has_copilot",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("token", String(64), unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlightTurnPoint(BaseModel):
|
||||||
|
__tablename__ = "flight_turn_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
order: Mapped[int] = mapped_column(Integer, index=True)
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship()
|
||||||
|
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
||||||
|
airport: Mapped['Airport'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Flight(BaseModel):
|
||||||
|
__tablename__ = "flight"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
|
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
|
duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||||
|
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
|
||||||
|
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
|
||||||
|
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
landings: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
|
||||||
|
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
|
||||||
|
weather_info_landing: Mapped['WeatherInfo'] = relationship(foreign_keys=[landing_weather_info_id])
|
||||||
|
weather_info_takeoff: Mapped['WeatherInfo'] = relationship(foreign_keys=[takeoff_weather_info_id])
|
||||||
|
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
|
||||||
|
event: Mapped['Event'] = relationship()
|
||||||
|
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_has_copilot)
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
|
track: Mapped['Track'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Track(BaseModel):
|
||||||
|
__tablename__ = "track"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
|
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship()
|
||||||
|
track_points: Mapped[list['TrackPoint']] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class TrackPoint(BaseModel):
|
||||||
|
__tablename__ = "track_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'))
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
track: Mapped['Track'] = relationship()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
flight_plan_has_copilot = Table(
|
||||||
|
"flight_plan_has_copilot",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("token", String(64), unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class FlightPlan(BaseModel):
|
||||||
|
__tablename__ = "flight_plan"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||||
|
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
|
||||||
|
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_plan_has_copilot)
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship()
|
||||||
|
markers: Mapped[list['FlightPlanMarker']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class FlightPlanMarker(BaseModel):
|
||||||
|
__tablename__ = "flight_plan_marker"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.user import user_is_in_organization
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(BaseModel):
|
||||||
|
__tablename__ = "organization"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
users: Mapped[set['User']] = relationship(secondary=user_is_in_organization)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, ForeignKey, Integer, DateTime
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetToken(BaseModel):
|
||||||
|
__tablename__ = "password_reset_token"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
token: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
user: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, ForeignKey, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Photo(BaseModel):
|
||||||
|
__tablename__ = "photo"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
filename: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||||
|
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||||
|
width: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
||||||
|
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
|
||||||
|
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
|
||||||
|
# copilots: Mapped[list['Copilot']] = relationship(secondary=copilot_has_photo)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoAdjustment(BaseModel):
|
||||||
|
__tablename__ = "photo_adjustment"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
rotate: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
contrast: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
brightness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
saturation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
photo: Mapped['Photo'] = relationship()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterestType(BaseModel):
|
||||||
|
__tablename__ = "point_of_interest_type"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
points_of_interest: Mapped[list['PointOfInterest']] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterest(BaseModel):
|
||||||
|
__tablename__ = "point_of_interest"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
type: Mapped[PointOfInterestType] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import datetime
|
||||||
|
from typing import Set
|
||||||
|
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column
|
||||||
|
from sqlalchemy.orm import Mapped, relationship, mapped_column
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
user_is_in_organization = Table(
|
||||||
|
"user_is_in_organization",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
|
||||||
|
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
__tablename__ = "user"
|
||||||
|
excluded_columns_in_dict = ('password_hashed',)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
|
||||||
|
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||||
|
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||||
|
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flights: Mapped[Set['Flight']] = relationship()
|
||||||
|
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Integer, DateTime, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherInfo(BaseModel):
|
||||||
|
__tablename__ = "weather_info"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
temperature_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
dewpoint_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
rain: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
cloudcover_low: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
cloudcover_total: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
wind_speed_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
wind_direction_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
date_time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from typing import Type
|
||||||
|
from sqlalchemy import select, or_, and_
|
||||||
|
from database import models
|
||||||
|
|
||||||
|
|
||||||
|
class QueryBuilder[ModelType: models.BaseModel]:
|
||||||
|
def __init__(self, model: Type[ModelType]):
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def get_simple_query(
|
||||||
|
self,
|
||||||
|
extra_select: list | None = None,
|
||||||
|
created_by_id: int | None = None,
|
||||||
|
order_by: list | None = None,
|
||||||
|
only_public: bool | None = False,
|
||||||
|
only_my: bool | None = False,
|
||||||
|
include_others_public: bool | None = False,
|
||||||
|
url_slug: str | None = None,
|
||||||
|
include_deleted: bool = False
|
||||||
|
):
|
||||||
|
if not extra_select:
|
||||||
|
extra_select = []
|
||||||
|
|
||||||
|
query = select(self.model, *extra_select)
|
||||||
|
|
||||||
|
if not include_deleted and hasattr(self.model, "deleted"):
|
||||||
|
query = query.filter(self.model.deleted.is_(False))
|
||||||
|
|
||||||
|
ownership_filters = []
|
||||||
|
my_filters = []
|
||||||
|
if only_public and hasattr(self.model, "is_public"):
|
||||||
|
my_filters.append(self.model.is_public.is_(True))
|
||||||
|
if hasattr(self.model, "url_slug"):
|
||||||
|
my_filters.append(self.model.url_slug != '')
|
||||||
|
|
||||||
|
if only_my:
|
||||||
|
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:
|
||||||
|
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))
|
||||||
|
|
||||||
|
query = query.filter(or_(*ownership_filters))
|
||||||
|
|
||||||
|
if url_slug is not None and hasattr(self.model, 'url_slug'):
|
||||||
|
query = query.filter(self.model.url_slug == url_slug)
|
||||||
|
|
||||||
|
if order_by:
|
||||||
|
query = query.order_by(*order_by)
|
||||||
|
|
||||||
|
elif hasattr(self.model, "name"):
|
||||||
|
query = query.order_by(self.model.name)
|
||||||
|
|
||||||
|
return query
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from database import async_session
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_session():
|
||||||
|
async with async_session() as session:
|
||||||
|
async with session.begin():
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
await session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
print(f"ERROR: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.expunge_all()
|
||||||
|
await session.close()
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
|
||||||
|
|
||||||
|
|
||||||
def public_endpoint(func):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
|
|
||||||
def wrapper(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
if 'info' in kwargs:
|
|
||||||
if not kwargs['info'].context.user_id:
|
|
||||||
if raise_when_unauthorized:
|
|
||||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
|
||||||
else:
|
|
||||||
return return_value_unauthorized
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from graphql import GraphQLError
|
|
||||||
from sqlalchemy.exc import NoResultFound
|
|
||||||
|
|
||||||
|
|
||||||
def error_logging(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
except NoResultFound as e:
|
|
||||||
raise GraphQLError(f"Not found", original_error=e)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
def retryable(func):
|
||||||
|
max_retries = 5
|
||||||
|
|
||||||
|
async def func_wrapper(*args, **kwargs):
|
||||||
|
for i in range(1, max_retries):
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except Exception as ex:
|
||||||
|
if i > max_retries:
|
||||||
|
raise ex
|
||||||
|
sleep_time = i * 2
|
||||||
|
print(
|
||||||
|
"Error while getting the data from source:"
|
||||||
|
" decorator is going to sleep for %s seconds"
|
||||||
|
" and retry it again (%s/%s)" % (
|
||||||
|
sleep_time, i, max_retries
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
return func_wrapper
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from database import async_session
|
|
||||||
|
|
||||||
|
|
||||||
async def db_session():
|
|
||||||
async with async_session() as session:
|
|
||||||
async with session.begin():
|
|
||||||
yield session
|
|
||||||
await session.flush()
|
|
||||||
await session.commit()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# from fastapi import Depends, Security
|
|
||||||
# from fastapi_jwt import JwtAuthorizationCredentials
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# async def check_jwt_token():
|
|
||||||
# authorize.jwt_required()
|
|
||||||
#
|
|
||||||
# subject = authorize.get_jwt_subject()
|
|
||||||
#
|
|
||||||
# print("Subject: ", subject)
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from typing import Any
|
||||||
|
from fastapi_mail import MessageSchema, MessageType
|
||||||
|
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:
|
||||||
|
data['subtype'] = MessageType.html
|
||||||
|
data['recipients'] = to
|
||||||
|
data['template'] = template
|
||||||
|
data['template_body'] = template_variables
|
||||||
|
data['subject'] = subject + " | Polétání.cz"
|
||||||
|
super().__init__(**data)
|
||||||
|
|
||||||
|
|
||||||
|
class ContactFormEmail(Email):
|
||||||
|
def __init__(self, email_from: str, content: str):
|
||||||
|
super().__init__(
|
||||||
|
to=["michal@kvacek.cz"],
|
||||||
|
reply_to=[email_from],
|
||||||
|
subject="Zpráva z kontaktního formuláře",
|
||||||
|
template="contact_form.html",
|
||||||
|
template_variables={
|
||||||
|
"content": content,
|
||||||
|
"email_from": email_from
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeEmail(Email):
|
||||||
|
def __init__(self, to: EmailStr | str):
|
||||||
|
super().__init__(
|
||||||
|
to=[to],
|
||||||
|
subject="Vítej",
|
||||||
|
template="welcome.html",
|
||||||
|
template_variables={}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordEmail(Email):
|
||||||
|
def __init__(self, to: EmailStr | str, reset_token: str, validity_hours: int):
|
||||||
|
super().__init__(
|
||||||
|
to=[to],
|
||||||
|
subject="Žádost o reset",
|
||||||
|
template="reset_password.html",
|
||||||
|
template_variables={
|
||||||
|
"token": reset_token,
|
||||||
|
"validity_hours": validity_hours,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-body background-color="#f4f4f4">
|
||||||
|
<mj-container>
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
<mj-text font-size="20px" color="#469ccc" font-family="Arial, sans-serif" align="center" padding-bottom="20px">
|
||||||
|
Zpráva z kontaktního formuláře | Polétání.cz
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
|
||||||
|
Od: {{ email_from | e }}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
|
{{ content | e }}
|
||||||
|
</mj-text>
|
||||||
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-container>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-body background-color="#f4f4f4">
|
||||||
|
<mj-container>
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
<mj-text font-size="20px" color="#469ccc" font-family="Arial, sans-serif" align="center" padding-bottom="20px">
|
||||||
|
Zapomenuté heslo | Polétání.cz
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Ahoj,
|
||||||
|
<br>
|
||||||
|
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro reset hesla.
|
||||||
|
<br>
|
||||||
|
</p>
|
||||||
|
</mj-text>
|
||||||
|
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
|
||||||
|
Obnovit heslo
|
||||||
|
</mj-button>
|
||||||
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-container>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-body background-color="#f4f4f4">
|
||||||
|
<mj-container>
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
<mj-text font-size="20px" color="#469ccc" font-family="Arial, sans-serif" align="center" padding-bottom="20px">
|
||||||
|
Vítejte v aplikaci Polétání.cz!
|
||||||
|
</mj-text>
|
||||||
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
|
Ahoj!
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Vítej v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Co můžeš očekávat:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
|
||||||
|
<li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
|
||||||
|
<li>Objevovat nová místa pro další výlety</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Začni nyní a sdílejte své vzdušné zážitky s celým světem!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
S pozdravem,
|
||||||
|
<br>
|
||||||
|
Michal Kváček
|
||||||
|
</p>
|
||||||
|
</mj-text>
|
||||||
|
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="https://poletani.cz/login">
|
||||||
|
Přihlásit se do Polétání.cz
|
||||||
|
</mj-button>
|
||||||
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-container>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
from fastapi_jwt.jwt import JwtAccess, JwtRefresh
|
from fastapi_jwt.jwt import JwtAccess, JwtRefresh
|
||||||
|
|
||||||
|
|
||||||
class BaseEndpoint(object):
|
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
|
|
||||||
class AuthEndpoint:
|
class AuthEndpoint:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.access_security: JwtAccess = kwargs.pop("access_token")
|
self.access_security: JwtAccess = kwargs.pop("access_token")
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from fastapi import BackgroundTasks
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from background_jobs.send_email import send_email
|
||||||
|
from emails.emails import ContactFormEmail
|
||||||
|
|
||||||
|
|
||||||
|
class ContactInput(BaseModel):
|
||||||
|
email_from: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class ContactEndpoint():
|
||||||
|
async def on_post(self, input: ContactInput, backgorund_tasks: BackgroundTasks):
|
||||||
|
backgorund_tasks.add_task(send_email, ContactFormEmail(
|
||||||
|
email_from=input.email_from,
|
||||||
|
content=input.content
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"email": {
|
||||||
|
"from": input.email_from,
|
||||||
|
"content": input.content
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from fastapi import BackgroundTasks
|
||||||
|
from passlib.hash import bcrypt
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from background_jobs.send_email import send_email
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from emails.emails import ResetPasswordEmail
|
||||||
|
|
||||||
|
|
||||||
|
class ForgottenPasswordRequest(BaseModel):
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeForgottenPassword(BaseModel):
|
||||||
|
password: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class ForgottenPasswordEndpoint():
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_random_token(length):
|
||||||
|
characters = string.ascii_letters + string.digits
|
||||||
|
return ''.join(random.choice(characters) for i in range(length))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def token_info(token: str):
|
||||||
|
async with get_session() as db:
|
||||||
|
reset = await models.PasswordResetToken.get_one(db, token=token)
|
||||||
|
user = await models.User.get_one(db, id=reset.user_id)
|
||||||
|
token_validity = reset.valid_until
|
||||||
|
email, name = user.email, user.name
|
||||||
|
|
||||||
|
return {
|
||||||
|
"token": token,
|
||||||
|
"token_validity": token_validity,
|
||||||
|
"user_email": email,
|
||||||
|
"user_name": name
|
||||||
|
}
|
||||||
|
|
||||||
|
async def request(self, input: ForgottenPasswordRequest, backgorund_tasks: BackgroundTasks):
|
||||||
|
validity_hours = 8
|
||||||
|
validity = datetime.now() + timedelta(hours=validity_hours)
|
||||||
|
random_str_token = self.generate_random_token(64)
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
user = await models.User.get_one(db, email=input.email)
|
||||||
|
token = await models.PasswordResetToken.create(db, {
|
||||||
|
"user_id": user.id,
|
||||||
|
"token": random_str_token,
|
||||||
|
"valid_until": validity
|
||||||
|
})
|
||||||
|
|
||||||
|
backgorund_tasks.add_task(
|
||||||
|
send_email,
|
||||||
|
email=ResetPasswordEmail(
|
||||||
|
to=input.email,
|
||||||
|
reset_token=token,
|
||||||
|
validity_hours=validity_hours
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def change_password(input: ChangeForgottenPassword):
|
||||||
|
async with get_session() as db:
|
||||||
|
token = await models.PasswordResetToken.get_one(db, token=input.token)
|
||||||
|
user_id = token.user_id
|
||||||
|
|
||||||
|
await models.User.update(
|
||||||
|
db, id=token.user_id,
|
||||||
|
data={"password_hashed": bcrypt.hash(input.password)}
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.delete(token)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_id
|
||||||
|
}
|
||||||
+14
-8
@@ -4,7 +4,8 @@ from passlib.hash import bcrypt
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
from database.models import User
|
from database.models import User
|
||||||
from endpoints.base import BaseEndpoint, AuthEndpoint
|
from database.transaction import get_session
|
||||||
|
from endpoints.base import AuthEndpoint
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@@ -13,21 +14,27 @@ class LoginInput(BaseModel):
|
|||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class LoginEndpoint(AuthEndpoint, BaseEndpoint):
|
class LoginEndpoint(AuthEndpoint):
|
||||||
async def on_post(self, user_data: LoginInput, resp: Response) -> dict:
|
async def on_post(self, user_data: LoginInput, resp: Response) -> dict:
|
||||||
query = select(User).filter_by(email=user_data.email)
|
query = select(User).filter_by(email=user_data.email)
|
||||||
logged_user = (await self.db.scalars(query)).first()
|
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
logged_user = (await db.scalars(query)).first()
|
||||||
if not logged_user:
|
if not logged_user:
|
||||||
raise HTTPException(status_code=401, detail="Invalid user")
|
raise HTTPException(status_code=401, detail="Invalid user")
|
||||||
|
|
||||||
if not bcrypt.verify(user_data.password, logged_user.password_hashed):
|
user = logged_user.as_dict()
|
||||||
|
password_hashed = logged_user.password_hashed
|
||||||
|
|
||||||
|
if not bcrypt.verify(user_data.password, password_hashed):
|
||||||
raise HTTPException(status_code=401, detail="Bad username or password")
|
raise HTTPException(status_code=401, detail="Bad username or password")
|
||||||
|
|
||||||
subject = {"id": logged_user.id, "email": logged_user.email}
|
subject = {"id": user['id'], "email": user['email']}
|
||||||
access_token = self.access_security.create_access_token(subject=subject)
|
access_token = self.access_security.create_access_token(subject=subject)
|
||||||
refresh_token = self.refresh_security.create_refresh_token(subject=subject)
|
refresh_token = self.refresh_security.create_refresh_token(subject=subject)
|
||||||
|
|
||||||
|
# TODO: SameSite na refresh_token_cookie!
|
||||||
|
# https://medium.com/swlh/how-the-new-chrome-80-cookie-rule-samesite-none-secure-affects-web-development-c06380220ced
|
||||||
# self.access_security.set_access_cookie(resp, access_token)
|
# self.access_security.set_access_cookie(resp, access_token)
|
||||||
self.refresh_security.set_refresh_cookie(
|
self.refresh_security.set_refresh_cookie(
|
||||||
resp, refresh_token,
|
resp, refresh_token,
|
||||||
@@ -35,9 +42,8 @@ class LoginEndpoint(AuthEndpoint, BaseEndpoint):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user": logged_user.as_dict(),
|
"user": user,
|
||||||
"access_token": access_token,
|
"access_token": access_token
|
||||||
"access_token_validity": self.access_security.access_expires_delta.total_seconds(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import os.path
|
||||||
|
from sqlalchemy import select
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from endpoints.base import AuthEndpoint
|
||||||
|
from paths import get_photo_basepath
|
||||||
|
from utils.image import PhotoEditor
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoEditorEndpoint(AuthEndpoint):
|
||||||
|
@staticmethod
|
||||||
|
async def show_preview(photo_id: int, **kwargs):
|
||||||
|
async with get_session() as db:
|
||||||
|
photo = (await db.scalars(
|
||||||
|
select(models.Photo)
|
||||||
|
.filter(models.Photo.id == photo_id)
|
||||||
|
)).one()
|
||||||
|
|
||||||
|
basepath = get_photo_basepath(photo.flight_id)
|
||||||
|
filename = f"{photo.filename}.{photo.filename_extension}"
|
||||||
|
|
||||||
|
original_filename = '_original_' + filename
|
||||||
|
if os.path.exists(f"{basepath}/{original_filename}"):
|
||||||
|
filename = original_filename
|
||||||
|
|
||||||
|
editor = PhotoEditor(basepath, filename)
|
||||||
|
editor.resize(new_height=900)
|
||||||
|
# TODO: idealni je udelat co nejdriv resize
|
||||||
|
# velikost muze ovlivnit: orez, otoceni, coz jsou dve nejnarocnejsi operace...
|
||||||
|
# ^^ tohle by bylo fajn cachovat
|
||||||
|
|
||||||
|
if kwargs.get("rotate"):
|
||||||
|
editor.rotate(kwargs['rotate'], True)
|
||||||
|
|
||||||
|
crop = {
|
||||||
|
key.replace("crop_", ""): value
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
if key in ('crop_left', 'crop_top', 'crop_width', 'crop_height') and value is not None
|
||||||
|
}
|
||||||
|
if crop and len(crop.keys()) == 4:
|
||||||
|
editor.crop(**crop)
|
||||||
|
|
||||||
|
adjustments = {
|
||||||
|
key: value for key, value in kwargs.items()
|
||||||
|
if key in ('saturation', 'brightness', 'contrast', 'sharpness') and value is not None
|
||||||
|
}
|
||||||
|
if adjustments:
|
||||||
|
editor.adjust(**adjustments)
|
||||||
|
|
||||||
|
return StreamingResponse(content=editor.get_as_stream(), media_type="image/jpeg")
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import re
|
import re
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException, BackgroundTasks
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import Optional
|
from pydantic import BaseModel, Field, model_validator
|
||||||
from pydantic import BaseModel, root_validator, Field
|
from background_jobs.send_email import send_email
|
||||||
from database.models import User
|
from database.models import User
|
||||||
from endpoints.base import BaseEndpoint
|
from database.transaction import get_session
|
||||||
from passlib.hash import bcrypt
|
from passlib.hash import bcrypt
|
||||||
|
from emails.emails import WelcomeEmail
|
||||||
|
|
||||||
|
|
||||||
class RegistrationInput(BaseModel):
|
class RegistrationInput(BaseModel):
|
||||||
email: str = Field(..., min_length=4)
|
email: str = Field(..., min_length=4)
|
||||||
name: Optional[str]
|
name: str | None
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
@root_validator()
|
@model_validator(mode="before")
|
||||||
def validate_email(cls, values):
|
def validate_email(cls, values):
|
||||||
email = values.get("email") or ""
|
email = values.get("email") or ""
|
||||||
|
|
||||||
@@ -23,22 +24,23 @@ class RegistrationInput(BaseModel):
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
class RegistrationEndpoint(BaseEndpoint):
|
class RegistrationEndpoint:
|
||||||
|
@staticmethod
|
||||||
async def on_post(self, user_data: RegistrationInput) -> User:
|
async def on_post(user_data: RegistrationInput, backgorund_tasks: BackgroundTasks) -> User:
|
||||||
query = select(User).filter_by(email=user_data.email)
|
query = select(User).filter_by(email=user_data.email)
|
||||||
existing_user = (await self.db.scalars(query)).first()
|
async with get_session() as db:
|
||||||
|
existing_user = (await db.scalars(query)).first()
|
||||||
|
|
||||||
if existing_user:
|
if existing_user:
|
||||||
raise HTTPException(status_code=422, detail="User already exists")
|
raise HTTPException(status_code=422, detail="User already exists")
|
||||||
|
|
||||||
model = await User.create(self.db, {
|
model = await User.create(db, {
|
||||||
"name": user_data.name,
|
"name": user_data.name,
|
||||||
"email": user_data.email,
|
"email": user_data.email,
|
||||||
"password_hashed": bcrypt.hash(user_data.password),
|
"password_hashed": bcrypt.hash(user_data.password),
|
||||||
"description": ""
|
"description": ""
|
||||||
})
|
})
|
||||||
|
|
||||||
await self.db.commit()
|
backgorund_tasks.add_task(send_email, WelcomeEmail(to=user_data.email))
|
||||||
|
|
||||||
return model.as_dict()
|
return model.as_dict()
|
||||||
|
|||||||
@@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">"""
|
||||||
|
|
||||||
|
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"""
|
||||||
|
<url>
|
||||||
|
<loc>https://poletani.cz/{sitemap_entry.url}</loc>
|
||||||
|
<priority>{sitemap_entry.priority}</priority>
|
||||||
|
</url>
|
||||||
|
"""
|
||||||
|
|
||||||
|
my_sitemap += "</urlset>"
|
||||||
|
return Response(content=my_sitemap, media_type="application/xml")
|
||||||
Vendored
+75
-13
@@ -1,25 +1,87 @@
|
|||||||
from typing import List, Tuple, Dict
|
import dataclasses
|
||||||
|
from typing import Optional
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
from database import models
|
||||||
|
from decorators.retryable import retryable
|
||||||
|
from utils.list_utils import chunks
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ElevationInfo:
|
||||||
|
id: int | None
|
||||||
|
lat: float
|
||||||
|
lng: float
|
||||||
|
elevation: float
|
||||||
|
|
||||||
|
|
||||||
class ElevationAPI:
|
class ElevationAPI:
|
||||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
ELEVATION_ENDPOINT: str
|
||||||
|
|
||||||
def get_request(self, points: List[Dict[str, float]]):
|
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||||
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
...
|
||||||
|
|
||||||
async def call_api(self, points: List[Dict[str, float]]):
|
@retryable
|
||||||
|
async def call_api(self, points: list[dict[str, float]]):
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(self.ELEVATION_ENDPOINT, json=self.get_request(points)) as resp:
|
async with self.get_request(session, points) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
|
|
||||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[dict]:
|
|
||||||
|
class OpenElevationAPI(ElevationAPI):
|
||||||
|
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||||
|
|
||||||
|
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||||
|
data = {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||||
|
return session.post(self.ELEVATION_ENDPOINT, json=data)
|
||||||
|
|
||||||
|
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||||
response = await self.call_api(points)
|
response = await self.call_api(points)
|
||||||
|
|
||||||
return [{
|
elevation = []
|
||||||
"lat": loc['latitude'],
|
for i, point in enumerate(points):
|
||||||
"lng": loc['longitude'],
|
response_point = response['results'][i]
|
||||||
"elevation": loc['elevation']
|
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
|
||||||
} for loc in response['results']]
|
return elevation
|
||||||
|
|
||||||
|
|
||||||
|
class OpenMeteoElevationAPI(ElevationAPI):
|
||||||
|
ELEVATION_ENDPOINT = "https://api.open-meteo.com/v1/elevation"
|
||||||
|
|
||||||
|
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||||
|
latitudes = ",".join(str(point['lat']) for point in points)
|
||||||
|
longitudes = ",".join(str(point['lng']) for point in points)
|
||||||
|
url = f"{self.ELEVATION_ENDPOINT}?latitude={latitudes}&longitude={longitudes}"
|
||||||
|
|
||||||
|
return session.get(url)
|
||||||
|
|
||||||
|
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||||
|
elevation = []
|
||||||
|
for chunk in chunks(points, n=100):
|
||||||
|
response = await self.call_api(chunk)
|
||||||
|
chunk_elevation = []
|
||||||
|
for i, point in enumerate(chunk):
|
||||||
|
chunk_elevation.append(ElevationInfo(**point, elevation=response['elevation'][i]))
|
||||||
|
|
||||||
|
elevation += chunk_elevation
|
||||||
|
return elevation
|
||||||
|
|
||||||
|
|
||||||
|
class CombinedElevationAPI:
|
||||||
|
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||||
|
for api in (OpenMeteoElevationAPI(), OpenElevationAPI()):
|
||||||
|
elevation = await api.get_elevation_for_points(points)
|
||||||
|
|
||||||
|
if elevation:
|
||||||
|
return elevation
|
||||||
|
|
||||||
|
|
||||||
|
async def update_track_points_elevation(db, track_points: list[models.TrackPoint]):
|
||||||
|
points = [{"id": p.id, "lat": p.gps_latitude, "lng": p.gps_longitude} for p in track_points]
|
||||||
|
elevation = await elevation_api.get_elevation_for_points(points)
|
||||||
|
for point in elevation:
|
||||||
|
await models.TrackPoint.update(db, {"terrain_elevation": point.elevation}, id=point.id)
|
||||||
|
|
||||||
|
|
||||||
|
elevation_api = CombinedElevationAPI()
|
||||||
|
|||||||
Vendored
+77
-48
@@ -1,9 +1,7 @@
|
|||||||
from datetime import datetime
|
from collections import defaultdict
|
||||||
from typing import List, Dict
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Dict, Any
|
||||||
from aiocache import cached
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.etree import _ElementTree
|
|
||||||
|
|
||||||
|
|
||||||
class GPXParser:
|
class GPXParser:
|
||||||
@@ -12,8 +10,55 @@ class GPXParser:
|
|||||||
self.gpx = etree.parse(self.file)
|
self.gpx = etree.parse(self.file)
|
||||||
self.namespace = None
|
self.namespace = None
|
||||||
self.precision_digits = 6
|
self.precision_digits = 6
|
||||||
|
self.max_samples_per_minute = 2
|
||||||
self.set_namespace()
|
self.set_namespace()
|
||||||
|
|
||||||
|
def get_timestamp_key(self, dt: datetime):
|
||||||
|
ts = round(dt.replace(microsecond=0).timestamp())
|
||||||
|
return ts - (ts % (60 / self.max_samples_per_minute))
|
||||||
|
|
||||||
|
async def group_data(self, data: list):
|
||||||
|
times = await self.get_times_all()
|
||||||
|
|
||||||
|
indexed_data: dict[datetime, Any] = {}
|
||||||
|
for time, item in zip(times, data):
|
||||||
|
indexed_data[time] = item
|
||||||
|
|
||||||
|
sampled_data = defaultdict(list)
|
||||||
|
for time, item in indexed_data.items():
|
||||||
|
key = self.get_timestamp_key(time)
|
||||||
|
sampled_data[key].append(item)
|
||||||
|
|
||||||
|
return sampled_data
|
||||||
|
|
||||||
|
async def average_sample_numbers(self, data: list[int | float]):
|
||||||
|
sampled_data = await self.group_data(data)
|
||||||
|
return [sum(items) / len(items) for items in sampled_data.values()]
|
||||||
|
|
||||||
|
async def sample_times(self, times: List[datetime]):
|
||||||
|
sampled_times = set()
|
||||||
|
for time in times:
|
||||||
|
original_tz = time.tzinfo
|
||||||
|
sampled_times.add(datetime.fromtimestamp(self.get_timestamp_key(time), tz=original_tz))
|
||||||
|
|
||||||
|
times_sorted = list(sampled_times)
|
||||||
|
times_sorted.sort()
|
||||||
|
return times_sorted
|
||||||
|
|
||||||
|
async def average_coordinates(self, coordinates: list[dict[str, float]]):
|
||||||
|
sampled_data = await self.group_data(coordinates)
|
||||||
|
coordinates_aggregated = []
|
||||||
|
for coordinates_group in sampled_data.values():
|
||||||
|
lat = [c['lat'] for c in coordinates_group]
|
||||||
|
lng = [c['lng'] for c in coordinates_group]
|
||||||
|
|
||||||
|
coordinates_aggregated.append({
|
||||||
|
"lat": sum(lat) / len(lat),
|
||||||
|
"lng": sum(lng) / len(lng),
|
||||||
|
})
|
||||||
|
|
||||||
|
return coordinates_aggregated
|
||||||
|
|
||||||
def set_namespace(self):
|
def set_namespace(self):
|
||||||
namespace = self.gpx.getroot().nsmap.get(None)
|
namespace = self.gpx.getroot().nsmap.get(None)
|
||||||
self.namespace = {'gpx': namespace}
|
self.namespace = {'gpx': namespace}
|
||||||
@@ -21,73 +66,57 @@ class GPXParser:
|
|||||||
def run_xpath(self, path: str):
|
def run_xpath(self, path: str):
|
||||||
return self.gpx.xpath(path, namespaces=self.namespace)
|
return self.gpx.xpath(path, namespaces=self.namespace)
|
||||||
|
|
||||||
@cached()
|
async def get_times_all(self):
|
||||||
async def get_times(self):
|
nodes = self.run_xpath("//gpx:trkpt/gpx:time")
|
||||||
nodes = self.run_xpath("//gpx:time")
|
|
||||||
return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
|
return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
|
||||||
|
|
||||||
@cached()
|
async def get_times(self):
|
||||||
|
times = await self.get_times_all()
|
||||||
|
return await self.sample_times(times)
|
||||||
|
|
||||||
|
async def get_total_duration(self) -> timedelta:
|
||||||
|
times = await self.get_times_all()
|
||||||
|
return times[-1] - times[0]
|
||||||
|
|
||||||
async def get_coordinates(self) -> List[Dict[str, float]]:
|
async def get_coordinates(self) -> List[Dict[str, float]]:
|
||||||
|
return await self.average_coordinates(await self.get_coordinates_all())
|
||||||
|
|
||||||
|
async def get_coordinates_all(self) -> List[Dict[str, float]]:
|
||||||
nodes = self.run_xpath("//gpx:trkpt")
|
nodes = self.run_xpath("//gpx:trkpt")
|
||||||
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
|
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
|
||||||
|
|
||||||
@cached()
|
async def get_speed(self) -> List[float]:
|
||||||
async def get_speed(self) -> List[int]:
|
|
||||||
nodes = self.run_xpath("//gpx:speed")
|
nodes = self.run_xpath("//gpx:speed")
|
||||||
return [int(node.text) for node in nodes]
|
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
async def get_magnetic_variation(self) -> List[float]:
|
||||||
async def get_magnetic_variation(self) -> List[int]:
|
|
||||||
nodes = self.run_xpath("//gpx:magvar")
|
nodes = self.run_xpath("//gpx:magvar")
|
||||||
return [int(node.text) for node in nodes]
|
return await self.average_sample_numbers([int(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_altitude(self) -> List[float]:
|
async def get_altitude(self) -> List[float]:
|
||||||
nodes = self.run_xpath("//gpx:ele")
|
nodes = self.run_xpath("//gpx:ele")
|
||||||
return [float(node.text) for node in nodes]
|
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_terrain_elevation(self) -> List[float]:
|
async def get_terrain_elevation(self) -> List[float]:
|
||||||
nodes = self.run_xpath("//gpx:terrain_elevation")
|
nodes = self.run_xpath("//gpx:terrain_elevation")
|
||||||
return [float(node.text) for node in nodes]
|
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_max_speed(self):
|
async def get_max_speed(self):
|
||||||
return max(await self.get_speed())
|
return max(await self.get_speed()) or 0
|
||||||
|
|
||||||
|
async def get_min_speed(self):
|
||||||
|
return min(await self.get_speed()) or 0
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_avg_speed(self):
|
async def get_avg_speed(self):
|
||||||
speeds = await self.get_speed()
|
speeds = await self.get_speed()
|
||||||
|
if not speeds:
|
||||||
|
return 0
|
||||||
|
|
||||||
return round(sum(speeds) / len(speeds), 2)
|
return round(sum(speeds) / len(speeds), 2)
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_max_altitude(self):
|
async def get_max_altitude(self):
|
||||||
return max(await self.get_altitude())
|
return max(await self.get_altitude()) or 0
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_avg_altitude(self):
|
async def get_avg_altitude(self):
|
||||||
altitudes = await self.get_altitude()
|
altitudes = await self.get_altitude()
|
||||||
return round(sum(altitudes) / len(altitudes), 2)
|
return round(sum(altitudes) / len(altitudes), 2)
|
||||||
|
|
||||||
def add_terrain_elevation(self, points_with_elevation: List[Dict[str, float]]):
|
|
||||||
track_points = self.run_xpath("//gpx:trkpt")
|
|
||||||
# TODO: open elevation API umi jen presnost na 6 desetinnych mist!
|
|
||||||
track_points_index = {(float(n.attrib['lat']), float(n.attrib['lon'])): n for n in track_points}
|
|
||||||
|
|
||||||
for point_with_elevation in points_with_elevation:
|
|
||||||
lat = point_with_elevation['lat']
|
|
||||||
lng = point_with_elevation['lng']
|
|
||||||
elevation = point_with_elevation['elevation']
|
|
||||||
target_node = track_points_index.get((lat, lng))
|
|
||||||
|
|
||||||
if target_node is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
extensions = target_node.find("./extensions", self.gpx.getroot().nsmap)
|
|
||||||
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>"))
|
|
||||||
|
|
||||||
return self.gpx
|
|
||||||
|
|
||||||
def write(self, tree: _ElementTree, output: str):
|
|
||||||
print(f"ZAPISUJI DO {output}")
|
|
||||||
tree.write(output)
|
|
||||||
|
|||||||
Vendored
+281
@@ -0,0 +1,281 @@
|
|||||||
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
import aiohttp
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
|
||||||
|
|
||||||
|
|
||||||
|
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
||||||
|
# http://www.winpilot.com/usersguide/userairspace.asp
|
||||||
|
|
||||||
|
class AirspaceType(enum.Enum):
|
||||||
|
AIRSPACE_A = "A"
|
||||||
|
AIRSPACE_B = "B"
|
||||||
|
AIRSPACE_C = "C"
|
||||||
|
AIRSPACE_D = "D"
|
||||||
|
AIRSPACE_E = "E"
|
||||||
|
AIRSPACE_F = "F"
|
||||||
|
AIRSPACE_G = "G"
|
||||||
|
ATZ = "ATZ"
|
||||||
|
PARAGLIDING_ZONE = "PGZ"
|
||||||
|
TRAGA = "GSEC"
|
||||||
|
CTR = "CTR"
|
||||||
|
TMA = "TMA"
|
||||||
|
TMZ = "TMZ"
|
||||||
|
FIR = "FIR"
|
||||||
|
RMZ = "RMZ"
|
||||||
|
RESTRICTED = "R"
|
||||||
|
DANGER = "Q"
|
||||||
|
PROHIBITED = "P"
|
||||||
|
|
||||||
|
|
||||||
|
class ArcDirection(enum.Enum):
|
||||||
|
CLOCKWISE = "+"
|
||||||
|
COUNTER_CLOCKWISE = "-"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Coordinates:
|
||||||
|
lat: float
|
||||||
|
lng: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Airspace:
|
||||||
|
type: AirspaceType
|
||||||
|
info: str = ""
|
||||||
|
upper_limit: str = None
|
||||||
|
lower_limit: str = None
|
||||||
|
center: Optional[Coordinates] = None
|
||||||
|
radius_nm: float | None = None
|
||||||
|
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
|
class ArcExpander:
|
||||||
|
@classmethod
|
||||||
|
def get_arc_coordinates(
|
||||||
|
cls,
|
||||||
|
center_point: tuple[float, float],
|
||||||
|
origin_point: tuple[float, float],
|
||||||
|
end_point: tuple[float, float]
|
||||||
|
) -> list[Coordinates]:
|
||||||
|
initial_bearing = get_bearing(center_point, origin_point)
|
||||||
|
target_bearing = get_bearing(center_point, end_point)
|
||||||
|
distance = get_distance(center_point, origin_point)
|
||||||
|
|
||||||
|
print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}")
|
||||||
|
|
||||||
|
def radians_range(start: float, stop: float, step_deg: int = 15):
|
||||||
|
start_deg = int(math.degrees(start)) % 360
|
||||||
|
stop_deg = int(math.degrees(stop)) % 360
|
||||||
|
|
||||||
|
for i in range(start_deg, stop_deg, step_deg):
|
||||||
|
yield math.radians(i)
|
||||||
|
|
||||||
|
list_bearings = list(radians_range(initial_bearing, target_bearing))
|
||||||
|
print("RADIANS", list_bearings)
|
||||||
|
print("\n")
|
||||||
|
print(f"DEGREES {list(map(math.degrees, list_bearings))}")
|
||||||
|
print(f"INIT DEGREES {math.degrees(initial_bearing)=}, {math.degrees(target_bearing)=}")
|
||||||
|
print("==========================================")
|
||||||
|
|
||||||
|
coordinates = []
|
||||||
|
for bearing in list_bearings:
|
||||||
|
lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1])
|
||||||
|
lat2 = math.asin(
|
||||||
|
math.sin(lat1) * math.cos(distance / EARTH_RADIUS) + math.cos(lat1) * math.sin(distance / EARTH_RADIUS) * math.cos(bearing)
|
||||||
|
)
|
||||||
|
|
||||||
|
lon2 = lon1 + math.atan2(
|
||||||
|
math.sin(bearing) * math.sin(distance / EARTH_RADIUS) * math.cos(lat1),
|
||||||
|
math.cos(distance / EARTH_RADIUS) - math.sin(lat1) * math.sin(lat2)
|
||||||
|
)
|
||||||
|
|
||||||
|
lat2, lon2 = math.degrees(lat2), math.degrees(lon2)
|
||||||
|
coordinates.append(Coordinates(lat2, lon2))
|
||||||
|
|
||||||
|
return coordinates
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAirParser:
|
||||||
|
def __init__(self, file: Path | str):
|
||||||
|
self.file = None
|
||||||
|
|
||||||
|
self.context_type: Optional[AirspaceType] = None
|
||||||
|
self.context = {}
|
||||||
|
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
def save_context(self):
|
||||||
|
if not self.context_type:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = Airspace(type=self.context_type, **self.context)
|
||||||
|
|
||||||
|
self.context_type = None
|
||||||
|
self.context = {}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
with open(self.file, "r") as file:
|
||||||
|
for line in file:
|
||||||
|
stripped_line = line.strip()
|
||||||
|
if not stripped_line:
|
||||||
|
if self.context_type:
|
||||||
|
yield self.save_context()
|
||||||
|
else:
|
||||||
|
self.parse_line(stripped_line)
|
||||||
|
|
||||||
|
if self.context_type:
|
||||||
|
yield self.save_context()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dms_to_dd(dms, hemisphere):
|
||||||
|
if '.' in dms:
|
||||||
|
degrees, minutes = dms.split(':')
|
||||||
|
seconds = 0
|
||||||
|
else:
|
||||||
|
degrees, minutes, seconds = dms.split(":")
|
||||||
|
|
||||||
|
decimal = float(degrees) + float(minutes) / 60 + float(seconds) / 3600
|
||||||
|
if hemisphere in ['S', 'W']:
|
||||||
|
decimal *= -1
|
||||||
|
|
||||||
|
return decimal
|
||||||
|
|
||||||
|
def parse_coordinates(self, coordinates: str) -> Coordinates:
|
||||||
|
parts = coordinates.split()
|
||||||
|
return Coordinates(
|
||||||
|
self.dms_to_dd(parts[0], parts[1]),
|
||||||
|
self.dms_to_dd(parts[2], parts[3])
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_arcs(self, arcs: str) -> list[Coordinates]:
|
||||||
|
arc_from, arc_to = arcs.split(",")
|
||||||
|
if self.context.get('arc_direction', ArcDirection.CLOCKWISE) == ArcDirection.CLOCKWISE:
|
||||||
|
starting_point = self.parse_coordinates(arc_from)
|
||||||
|
ending_point = self.parse_coordinates(arc_to)
|
||||||
|
else:
|
||||||
|
# souradnice jsou zadane proti smeru hodinovych rucicek -> prohodim souradnice a pocitam jako by byly po smeru
|
||||||
|
starting_point = self.parse_coordinates(arc_to)
|
||||||
|
ending_point = self.parse_coordinates(arc_from)
|
||||||
|
|
||||||
|
center = self.context['center']
|
||||||
|
|
||||||
|
del self.context['center']
|
||||||
|
if self.context.get('arc_direction'):
|
||||||
|
del self.context['arc_direction']
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/62712947/calculate-arc-coordinates-based-on-original-point-end-point-center-distance-a
|
||||||
|
|
||||||
|
return ArcExpander.get_arc_coordinates(
|
||||||
|
(center.lat, center.lng),
|
||||||
|
(starting_point.lat, starting_point.lng),
|
||||||
|
(ending_point.lat, ending_point.lng)
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_line(self, line: str):
|
||||||
|
tokens = line.split(" ")
|
||||||
|
keyword = tokens[0]
|
||||||
|
value = None
|
||||||
|
if len(tokens) > 1:
|
||||||
|
value = " ".join(tokens[1:])
|
||||||
|
|
||||||
|
match keyword:
|
||||||
|
case '*':
|
||||||
|
# comment
|
||||||
|
pass
|
||||||
|
case 'AC':
|
||||||
|
try:
|
||||||
|
self.context_type = AirspaceType(value)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Neznam {e}, ignoruji")
|
||||||
|
return
|
||||||
|
case 'AN':
|
||||||
|
if self.context_type == AirspaceType.AIRSPACE_G:
|
||||||
|
match value[0:2]:
|
||||||
|
case 'PG':
|
||||||
|
self.context_type = AirspaceType.PARAGLIDING_ZONE
|
||||||
|
case 'LK':
|
||||||
|
self.context_type = AirspaceType.ATZ
|
||||||
|
case _:
|
||||||
|
self.context_type = AirspaceType.TRAGA
|
||||||
|
|
||||||
|
self.context['info'] = value
|
||||||
|
case 'AL':
|
||||||
|
self.context['lower_limit'] = value
|
||||||
|
case 'AH':
|
||||||
|
self.context['upper_limit'] = value
|
||||||
|
case 'DP':
|
||||||
|
if 'bounds' not in self.context:
|
||||||
|
self.context['bounds'] = []
|
||||||
|
self.context['bounds'].append(self.parse_coordinates(value))
|
||||||
|
case 'V':
|
||||||
|
match value[0]:
|
||||||
|
case 'X':
|
||||||
|
self.context['center'] = self.parse_coordinates(value[2:]) # odstranit X= na zacatku
|
||||||
|
case 'D':
|
||||||
|
self.context['arc_direction'] = ArcDirection(value[2:])
|
||||||
|
|
||||||
|
case 'DB':
|
||||||
|
if 'bounds' not in self.context:
|
||||||
|
self.context['bounds'] = []
|
||||||
|
|
||||||
|
self.context['bounds'] = [*self.context['bounds'], *self.parse_arcs(value)]
|
||||||
|
case 'DC':
|
||||||
|
self.context['radius_nm'] = float(value)
|
||||||
|
case _:
|
||||||
|
print(f"Neznam {keyword}, radek: {line}")
|
||||||
|
|
||||||
|
|
||||||
|
src = "https://www.alus.it/AirspaceConverter/openAIP/cz_asp.txt"
|
||||||
|
dest = "/app/static/airspaces/cz_asp.txt"
|
||||||
|
|
||||||
|
|
||||||
|
async def download_airspace(source, destination):
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(source) as response:
|
||||||
|
with open(destination, "w") as f:
|
||||||
|
f.write(await response.text(encoding="latin-1"))
|
||||||
|
|
||||||
|
|
||||||
|
async def save_airspace(destination):
|
||||||
|
parser = OpenAirParser(destination)
|
||||||
|
async with get_session() as db:
|
||||||
|
await db.execute(delete(models.Airspace))
|
||||||
|
|
||||||
|
for item in parser.parse():
|
||||||
|
# if 'CTR KARLOVY VARY' not in item.info:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# print("---" * 10)
|
||||||
|
# print(len(item.bounds))
|
||||||
|
# print(item.bounds)
|
||||||
|
# print("---" * 10)
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
await models.Airspace.create(db, {
|
||||||
|
"country": "cz",
|
||||||
|
"name": item.info,
|
||||||
|
"type": item.type.value,
|
||||||
|
'lower_limit': item.lower_limit,
|
||||||
|
"upper_limit": item.upper_limit,
|
||||||
|
"center": [item.center.lat, item.center.lng] if item.center else None,
|
||||||
|
"radius": item.radius_nm * 1000 * 1.852 if item.radius_nm else None,
|
||||||
|
"bounds": [(round(i.lat, 6), round(i.lng, 6)) for i in item.bounds]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_airspaces(src, dest):
|
||||||
|
await download_airspace(src, dest)
|
||||||
|
await save_airspace(dest)
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(refresh_airspaces(src, dest))
|
||||||
Vendored
+12
-3
@@ -3,6 +3,7 @@ import urllib.parse
|
|||||||
from typing import Tuple, Dict
|
from typing import Tuple, Dict
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
|
from decorators.retryable import retryable
|
||||||
|
|
||||||
|
|
||||||
class Weather:
|
class Weather:
|
||||||
@@ -38,21 +39,29 @@ class Weather:
|
|||||||
return f"{url}{query_string}"
|
return f"{url}{query_string}"
|
||||||
|
|
||||||
@cached(ttl=6 * 3600)
|
@cached(ttl=6 * 3600)
|
||||||
|
@retryable
|
||||||
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
||||||
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
||||||
|
|
||||||
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in downloading weather: {e}, {type(e)}")
|
||||||
|
|
||||||
async def get_weather_for_hour(self, date_time: datetime.datetime, gps: Tuple[float, float]) -> Dict[str, float|str]:
|
async def get_weather_for_hour(
|
||||||
|
self, date_time: datetime.datetime, gps: Tuple[float, float]
|
||||||
|
) -> Dict[str, float | str]:
|
||||||
data = await self.download_weather_for_day(date_time.date(), gps)
|
data = await self.download_weather_for_day(date_time.date(), gps)
|
||||||
|
|
||||||
# TODO: kontrola timezone!
|
# TODO: kontrola timezone!
|
||||||
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
|
|
||||||
idx = date_time.hour
|
idx = date_time.hour
|
||||||
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
||||||
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
||||||
|
|
||||||
return result_data
|
return result_data
|
||||||
|
|
||||||
|
|
||||||
|
weather_api = Weather()
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from external.elevation import ElevationAPI
|
|
||||||
from external.gpx_parser import GPXParser
|
|
||||||
|
|
||||||
|
|
||||||
async def test():
|
|
||||||
elevation_api = ElevationAPI()
|
|
||||||
parser = GPXParser("./uploads/tracks/37b979cb-71c0-4d09-a2c1-cfdad5f7a0cf-OK-AUR_28_AUR_Bristell_NG5_Zapisnik_letu_2023-08-04-00 00_2023-08-04-12 00(1).gpx")
|
|
||||||
points = await parser.get_coordinates()
|
|
||||||
elevation = await elevation_api.get_elevation_for_points(points)
|
|
||||||
parser.add_terrain_elevation(elevation)
|
|
||||||
|
|
||||||
asyncio.run(test())
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import dataclasses
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import BackgroundTasks, Depends, Security
|
||||||
|
from fastapi_jwt import JwtAuthorizationCredentials
|
||||||
|
from sqlalchemy import select
|
||||||
|
from strawberry.fastapi import BaseContext
|
||||||
|
from database import async_session, models
|
||||||
|
from jwt import access_security
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class GraphQLContext(BaseContext):
|
||||||
|
request_start_time: datetime
|
||||||
|
user_id: int
|
||||||
|
organization_ids: set[int]
|
||||||
|
jwt_auth_credentials: JwtAuthorizationCredentials
|
||||||
|
background_tasks: BackgroundTasks
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
||||||
|
user_id = credentials['id'] if credentials else None
|
||||||
|
organization_ids = set()
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
async with async_session() as db:
|
||||||
|
organization_ids = set((await db.scalars(
|
||||||
|
select(models.user_is_in_organization.c.organization_id)
|
||||||
|
.filter(models.user_is_in_organization.c.user_id == user_id)
|
||||||
|
)).all())
|
||||||
|
|
||||||
|
return GraphQLContext(
|
||||||
|
user_id=user_id,
|
||||||
|
organization_ids=organization_ids,
|
||||||
|
jwt_auth_credentials=credentials,
|
||||||
|
background_tasks=Depends(BackgroundTasks),
|
||||||
|
request_start_time=datetime.now()
|
||||||
|
)
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from sqlalchemy import select
|
|
||||||
from strawberry.dataloader import DataLoader
|
|
||||||
from database import async_session
|
|
||||||
from database.models import Aircraft
|
|
||||||
|
|
||||||
|
|
||||||
async def load(ids: List[int]):
|
|
||||||
async with async_session() as session:
|
|
||||||
models = (await session.scalars(select(Aircraft).filter(Aircraft.id.in_(ids)))).all()
|
|
||||||
|
|
||||||
models_by_id = {model.id: model for model in models}
|
|
||||||
return [models_by_id.get(id_) for id_ in ids]
|
|
||||||
|
|
||||||
|
|
||||||
aircraft_dataloader = DataLoader(load_fn=load, cache=False)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from sqlalchemy import select
|
|
||||||
from strawberry.dataloader import DataLoader
|
|
||||||
from database import async_session
|
|
||||||
from database.models import Airport
|
|
||||||
|
|
||||||
|
|
||||||
async def load(ids: List[int]):
|
|
||||||
async with async_session() as session:
|
|
||||||
models = (await session.scalars(select(Airport).filter(Airport.id.in_(ids)))).all()
|
|
||||||
|
|
||||||
models_by_id = {model.id: model for model in models}
|
|
||||||
return [models_by_id[id_] for id_ in ids]
|
|
||||||
|
|
||||||
|
|
||||||
airport_dataloader = DataLoader(load_fn=load, cache=False)
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import Type
|
||||||
|
from logger import log
|
||||||
|
from database import models, async_session
|
||||||
|
from database.query_builder import QueryBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDataloader:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
model: Type[models.BaseModel],
|
||||||
|
relationship_column, filters: list | None = None
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.model = model
|
||||||
|
self.query_builder = QueryBuilder(self.model)
|
||||||
|
|
||||||
|
if relationship_column is None:
|
||||||
|
relationship_column = model.id
|
||||||
|
self.relationship_column = relationship_column
|
||||||
|
|
||||||
|
if filters is None:
|
||||||
|
filters = []
|
||||||
|
self.filters = filters
|
||||||
|
|
||||||
|
|
||||||
|
class SingleModelByIdDataloader(BaseDataloader):
|
||||||
|
async def load(self, ids: list[int]):
|
||||||
|
ids_set = {id_ for id_ in set(ids) if id_ is not None}
|
||||||
|
if not ids_set:
|
||||||
|
return [None for _ in ids]
|
||||||
|
|
||||||
|
async with async_session() as session:
|
||||||
|
query = (
|
||||||
|
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
|
||||||
|
.filter(self.relationship_column.in_(ids_set))
|
||||||
|
.filter(*self.filters)
|
||||||
|
)
|
||||||
|
|
||||||
|
items = (await session.execute(query)).all()
|
||||||
|
items_by_id = {rel_id: item for item, rel_id in items}
|
||||||
|
return [items_by_id.get(id_) for id_ in ids]
|
||||||
|
|
||||||
|
|
||||||
|
class MultiModelsDataloader(BaseDataloader):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
model: Type[models.BaseModel],
|
||||||
|
relationship_column=None,
|
||||||
|
filters: list | None = None,
|
||||||
|
extra_select: list | None = None,
|
||||||
|
extra_join: list | None = None,
|
||||||
|
order_by: list | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(model, relationship_column, filters)
|
||||||
|
|
||||||
|
if extra_join is None:
|
||||||
|
extra_join = []
|
||||||
|
self.extra_join = extra_join
|
||||||
|
|
||||||
|
if extra_select is None:
|
||||||
|
extra_select = []
|
||||||
|
self.extra_select = extra_select
|
||||||
|
|
||||||
|
if order_by is None:
|
||||||
|
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
|
||||||
|
self.order_by = order_by
|
||||||
|
|
||||||
|
def get_query(self, ids: set[int]):
|
||||||
|
query = (
|
||||||
|
self.query_builder.get_simple_query(
|
||||||
|
extra_select=[self.relationship_column] + self.extra_select,
|
||||||
|
order_by=self.order_by
|
||||||
|
)
|
||||||
|
.filter(self.relationship_column.in_(ids))
|
||||||
|
.filter(*self.filters)
|
||||||
|
)
|
||||||
|
|
||||||
|
for joined_table in self.extra_join:
|
||||||
|
query = query.join(joined_table)
|
||||||
|
|
||||||
|
if self.filters:
|
||||||
|
query = query.filter(*self.filters)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
async def load(self, ids: list[int]) -> list:
|
||||||
|
ids_set = {id_ for id_ in set(ids) if id_ is not None}
|
||||||
|
if not ids_set:
|
||||||
|
return [[] for _ in ids]
|
||||||
|
|
||||||
|
query = self.get_query(ids_set)
|
||||||
|
|
||||||
|
async with async_session() as db:
|
||||||
|
data = (await db.execute(query)).all()
|
||||||
|
result_data = self.process_data(data)
|
||||||
|
return [result_data[id_] for id_ in ids]
|
||||||
|
|
||||||
|
def process_data(self, data):
|
||||||
|
result_data = defaultdict(list)
|
||||||
|
for row in data:
|
||||||
|
item, rel_id = row[0:2]
|
||||||
|
extra = row[2:]
|
||||||
|
if extra:
|
||||||
|
log.warning(f"Override function process_data, extra params={extra} are going to be discarded!")
|
||||||
|
|
||||||
|
result_data[rel_id].append(item)
|
||||||
|
return result_data
|
||||||
|
|
||||||
|
|
||||||
|
class FlightCopilotDataloader(MultiModelsDataloader):
|
||||||
|
def process_data(self, data):
|
||||||
|
result_data = defaultdict(list)
|
||||||
|
for row in data:
|
||||||
|
item, rel_id = row[0:2]
|
||||||
|
token = row[2]
|
||||||
|
|
||||||
|
item.token = token
|
||||||
|
result_data[rel_id].append(item)
|
||||||
|
|
||||||
|
return result_data
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from typing import List
|
|
||||||
from sqlalchemy import select
|
|
||||||
from strawberry.dataloader import DataLoader
|
|
||||||
from database import async_session
|
|
||||||
from database.models import Copilot, Flight
|
|
||||||
|
|
||||||
|
|
||||||
async def load(ids: List[int]):
|
|
||||||
async with async_session() as session:
|
|
||||||
models = (await session.execute(
|
|
||||||
select(Copilot, Flight.id)
|
|
||||||
.join(Copilot.flights)
|
|
||||||
.filter(Flight.id.in_(ids))
|
|
||||||
)).all()
|
|
||||||
|
|
||||||
copilots_by_flight_id = defaultdict(list)
|
|
||||||
for copilot, flight_id in models:
|
|
||||||
copilots_by_flight_id[flight_id].append(copilot)
|
|
||||||
|
|
||||||
return [copilots_by_flight_id[id_] for id_ in ids]
|
|
||||||
|
|
||||||
|
|
||||||
flight_copilots_dataloader = DataLoader(load_fn=load, cache=False)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from typing import List, Optional
|
|
||||||
from sqlalchemy import select
|
|
||||||
from strawberry.dataloader import DataLoader
|
|
||||||
from database import async_session
|
|
||||||
from database.models import Flight, Copilot, PointOfInterest
|
|
||||||
|
|
||||||
|
|
||||||
class FlightsLoader:
|
|
||||||
def __init__(self, relationship_column, extra_join: Optional[list] = None):
|
|
||||||
if extra_join is None:
|
|
||||||
extra_join = []
|
|
||||||
|
|
||||||
self.relationship_column = relationship_column
|
|
||||||
self.extra_join = extra_join
|
|
||||||
|
|
||||||
async def load(self, ids: List[int]):
|
|
||||||
async with async_session() as session:
|
|
||||||
rel_column = self.relationship_column
|
|
||||||
query = (
|
|
||||||
select(Flight, rel_column)
|
|
||||||
.filter(rel_column.in_(ids))
|
|
||||||
)
|
|
||||||
|
|
||||||
for table in self.extra_join:
|
|
||||||
query = query.join(table)
|
|
||||||
|
|
||||||
data = (await session.execute(query)).all()
|
|
||||||
|
|
||||||
result_data = defaultdict(list)
|
|
||||||
for item, rel_id in data:
|
|
||||||
result_data[rel_id].append(item)
|
|
||||||
|
|
||||||
return [result_data[id_] for id_ in ids]
|
|
||||||
|
|
||||||
|
|
||||||
flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader(Copilot.id, extra_join=[Flight.copilots]).load, cache=False)
|
|
||||||
flights_by_aircraft_dataloader = DataLoader(load_fn=FlightsLoader(Flight.aircraft_id).load, cache=False)
|
|
||||||
|
|
||||||
flight_by_poi_dataloader = DataLoader(load_fn=FlightsLoader(PointOfInterest.id, extra_join=[Flight.track, PointOfInterest]).load, cache=False)
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
from strawberry.dataloader import DataLoader
|
||||||
|
from database import async_session, models
|
||||||
|
|
||||||
|
|
||||||
|
async def load_flight_durations(ids: List[int]):
|
||||||
|
async with async_session() as db:
|
||||||
|
flights = (await db.execute(
|
||||||
|
select(
|
||||||
|
models.Flight.id,
|
||||||
|
func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"),
|
||||||
|
func.coalesce(func.sum(models.FlightTurnPoint.landing_duration), 0).label("landing_duration")
|
||||||
|
).join(models.Flight.turn_points, isouter=True)
|
||||||
|
.group_by(models.Flight.id)
|
||||||
|
.filter(models.Flight.id.in_(ids))
|
||||||
|
|
||||||
|
)).all()
|
||||||
|
|
||||||
|
items_by_id = {item.id: item.diff.seconds // 60 - item.landing_duration for item in flights}
|
||||||
|
return [items_by_id.get(id_) for id_ in ids]
|
||||||
|
|
||||||
|
|
||||||
|
flight_duration_dataloader = DataLoader(load_fn=load_flight_durations, cache=False)
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
from strawberry.dataloader import DataLoader
|
||||||
|
from database import models
|
||||||
|
from graphql_schema.dataloaders.base import MultiModelsDataloader, FlightCopilotDataloader
|
||||||
|
|
||||||
|
aircrafts_from_organization_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Aircraft,
|
||||||
|
relationship_column=models.Organization.id,
|
||||||
|
extra_join=[models.Aircraft.organization]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
flight_copilots_dataloader = DataLoader(
|
||||||
|
load_fn=FlightCopilotDataloader(
|
||||||
|
models.Copilot,
|
||||||
|
relationship_column=models.Flight.id,
|
||||||
|
extra_select=[models.flight_has_copilot.c.token],
|
||||||
|
filters=[
|
||||||
|
models.flight_has_copilot.c.flight_id == models.Flight.id,
|
||||||
|
models.flight_has_copilot.c.copilot_id == models.Copilot.id,
|
||||||
|
],
|
||||||
|
extra_join=[models.Copilot.flights]
|
||||||
|
).load,
|
||||||
|
cache=False)
|
||||||
|
|
||||||
|
|
||||||
|
flight_plan_copilots_dataloader = DataLoader(
|
||||||
|
load_fn=FlightCopilotDataloader(
|
||||||
|
models.Copilot,
|
||||||
|
relationship_column=models.FlightPlan.id,
|
||||||
|
extra_select=[models.flight_plan_has_copilot.c.token],
|
||||||
|
# filters=[
|
||||||
|
# models.flight_plan_has_copilot.c.flight_plan_id == models.FlightPlan.id,
|
||||||
|
# models.flight_plan_has_copilot.c.copilot_id == models.Copilot.id,
|
||||||
|
# ],
|
||||||
|
extra_join=[models.Copilot.flight_plans]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
reporting_points_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.ReportingPoint,
|
||||||
|
relationship_column=models.airport_has_reporting_point.c.airport_id,
|
||||||
|
extra_join=[models.airport_has_reporting_point]
|
||||||
|
).load, cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
runways_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Runway,
|
||||||
|
relationship_column=models.Airport.id,
|
||||||
|
extra_join=[models.Airport.runways],
|
||||||
|
order_by=[models.Runway.designator]
|
||||||
|
).load, cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
frequencies_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Frequency,
|
||||||
|
relationship_column=models.Airport.id,
|
||||||
|
extra_join=[models.Airport.frequencies],
|
||||||
|
order_by=[models.Frequency.frequency]
|
||||||
|
).load, cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
flights_by_copilot_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Flight,
|
||||||
|
relationship_column=models.Copilot.id,
|
||||||
|
extra_join=[models.Flight.copilots],
|
||||||
|
order_by=[models.Flight.takeoff_datetime.desc()]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
public_flights_by_copilot_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Flight,
|
||||||
|
relationship_column=models.Copilot.id,
|
||||||
|
extra_join=[models.Flight.copilots],
|
||||||
|
filters=[models.Flight.is_public.is_(True)],
|
||||||
|
order_by=[models.Flight.takeoff_datetime.desc()]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
flights_by_aircraft_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Flight,
|
||||||
|
relationship_column=models.Flight.aircraft_id,
|
||||||
|
order_by=[models.Flight.takeoff_datetime.desc()]
|
||||||
|
).load, cache=False)
|
||||||
|
|
||||||
|
flight_by_poi_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Flight,
|
||||||
|
relationship_column=models.PointOfInterest.id,
|
||||||
|
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||||
|
extra_join=[models.Flight.turn_points, models.PointOfInterest]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
flights_by_event_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Flight,
|
||||||
|
relationship_column=models.Event.id,
|
||||||
|
extra_join=[models.Flight.event],
|
||||||
|
order_by=[models.Flight.takeoff_datetime.desc()]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
public_flights_by_event_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Flight,
|
||||||
|
relationship_column=models.Event.id,
|
||||||
|
filters=[models.Flight.is_public.is_(True)],
|
||||||
|
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||||
|
extra_join=[models.Flight.event]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
user_organizations_dataloader = DataLoader(load_fn=MultiModelsDataloader(
|
||||||
|
models.Organization,
|
||||||
|
relationship_column=models.user_is_in_organization.c.user_id,
|
||||||
|
extra_join=[models.user_is_in_organization]
|
||||||
|
).load, cache=False)
|
||||||
|
|
||||||
|
users_in_organization_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.User,
|
||||||
|
relationship_column=models.Organization.id,
|
||||||
|
extra_join=[models.Organization.users]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
photos_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Photo,
|
||||||
|
relationship_column=models.Photo.flight_id,
|
||||||
|
order_by=[models.Photo.exposed_at]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
poi_photos_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Photo,
|
||||||
|
relationship_column=models.Photo.point_of_interest_id,
|
||||||
|
order_by=[models.Photo.exposed_at]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
flight_turn_points_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.FlightTurnPoint,
|
||||||
|
relationship_column=models.FlightTurnPoint.flight_id,
|
||||||
|
order_by=[models.FlightTurnPoint.order]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
track_points_dataloder = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.TrackPoint,
|
||||||
|
relationship_column=models.TrackPoint.track_id,
|
||||||
|
order_by=[models.TrackPoint.timestamp, models.TrackPoint.id]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
copilots_in_photo_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Copilot,
|
||||||
|
relationship_column=models.copilot_has_photo.c.photo_id,
|
||||||
|
extra_join=[models.copilot_has_photo],
|
||||||
|
order_by=[models.Copilot.name]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
photo_copilots_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Photo,
|
||||||
|
relationship_column=models.copilot_has_photo.c.copilot_id,
|
||||||
|
extra_join=[models.copilot_has_photo],
|
||||||
|
order_by=[models.Photo.exposed_at]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
photos_aircraft_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Photo,
|
||||||
|
relationship_column=models.Photo.aircraft_id,
|
||||||
|
order_by=[models.Photo.exposed_at]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
flight_plan_markers_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.FlightPlanMarker,
|
||||||
|
relationship_column=models.FlightPlanMarker.flight_plan_id,
|
||||||
|
order_by=[models.FlightPlanMarker.position, models.FlightPlanMarker.id]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from typing import List, Literal
|
|
||||||
from sqlalchemy import select
|
|
||||||
from strawberry.dataloader import DataLoader
|
|
||||||
from database import async_session
|
|
||||||
from database.models import Photo
|
|
||||||
|
|
||||||
|
|
||||||
class PhotoDataloader:
|
|
||||||
|
|
||||||
def __init__(self, relationship_column: Literal['flight_id', 'point_of_interest_id']) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.relationship_column = relationship_column
|
|
||||||
|
|
||||||
async def load_collection(self, ids: List[int]):
|
|
||||||
async with async_session() as session:
|
|
||||||
models = (await session.scalars(
|
|
||||||
select(Photo)
|
|
||||||
.filter(getattr(Photo, self.relationship_column).in_(ids))
|
|
||||||
)).all()
|
|
||||||
photos_by_relationship_id = defaultdict(list)
|
|
||||||
for photo in models:
|
|
||||||
photos_by_relationship_id[getattr(photo, self.relationship_column)].append(photo)
|
|
||||||
|
|
||||||
return [photos_by_relationship_id[id_] for id_ in ids]
|
|
||||||
|
|
||||||
|
|
||||||
async def flight_cover_photo_load(ids: List[int]):
|
|
||||||
async with async_session() as session:
|
|
||||||
models = (
|
|
||||||
await session.scalars(
|
|
||||||
select(Photo)
|
|
||||||
.filter(Photo.is_flight_cover.is_(True))
|
|
||||||
.filter(Photo.flight_id.in_(ids)))
|
|
||||||
).all()
|
|
||||||
photos = {p.flight_id: p for p in models}
|
|
||||||
return [photos.get(id_) for id_ in ids]
|
|
||||||
|
|
||||||
|
|
||||||
cover_photo_loader = DataLoader(load_fn=flight_cover_photo_load, cache=False)
|
|
||||||
photos_dataloader = DataLoader(load_fn=PhotoDataloader("flight_id").load_collection, cache=False)
|
|
||||||
poi_photos_dataloader = DataLoader(load_fn=PhotoDataloader("point_of_interest_id").load_collection, cache=False)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user