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_PASSWORD: "****"
|
||||
APP_ENV: "production"
|
||||
|
||||
SMTP_HOST: ""
|
||||
SMTP_PORT: ""
|
||||
SMTP_USERNAME: ""
|
||||
SMTP_PASSWORD: ""
|
||||
@@ -2,5 +2,6 @@
|
||||
max-line-length = 120
|
||||
exclude =
|
||||
venv
|
||||
.venv
|
||||
__pycache__
|
||||
alembic
|
||||
alembic
|
||||
|
||||
+5
-1
@@ -7,4 +7,8 @@ __pycache__/
|
||||
|
||||
# app specific files and dirs
|
||||
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
|
||||
RUN deluser xfs
|
||||
# RUN deluser xfs
|
||||
|
||||
# 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 apk update && \
|
||||
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
||||
pip install --upgrade pip
|
||||
|
||||
|
||||
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)
|
||||
ENV INSTALL_PACKAGES build-base linux-headers
|
||||
@@ -23,4 +36,4 @@ COPY . /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
|
||||
+18
-5
@@ -2,8 +2,16 @@ import csv
|
||||
|
||||
|
||||
def deg_to_dec(val: str):
|
||||
val = float(val[:-1]) / 100
|
||||
deg = int(val)
|
||||
frac = val % 1
|
||||
|
||||
# https://www.pgc.umn.edu/apps/convert/ N47 17.6 E012 47.3
|
||||
|
||||
print(frac, frac * 60, frac * 3600)
|
||||
|
||||
return deg + (frac * 60)
|
||||
|
||||
val = val[:-1].lstrip("0")
|
||||
# h_m, s = val.split(".")
|
||||
# h_m = h_m.strip("0")
|
||||
# h = int(h_m[0:2])
|
||||
@@ -16,15 +24,20 @@ def deg_to_dec(val: str):
|
||||
# print("--konec-----------------------------------")
|
||||
# return h + (m / 60.0) + (int(s) / 3600.0)
|
||||
|
||||
return len(val or "")
|
||||
# return val
|
||||
|
||||
# return len(val or "")
|
||||
|
||||
|
||||
# ZDAKOV: @49.504378,14.1808905
|
||||
|
||||
# 4930.250N / 100 -> cele cislo stupne, desetinne prevest do sedesatkove soustavy
|
||||
|
||||
with open("./poi.csv") as f:
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
for row in reader:
|
||||
# if row['name'] != 'ZDAKOV':
|
||||
# continue
|
||||
if row['name'] != 'ZDAKOV':
|
||||
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']))
|
||||
|
||||
@@ -21,4 +21,4 @@ ssh -t michal@kvacek.cz << EOF
|
||||
docker-compose down --remove-orphans
|
||||
docker-compose up -d
|
||||
docker-compose exec -T api alembic upgrade head
|
||||
EOF
|
||||
EOF
|
||||
|
||||
@@ -3,10 +3,12 @@ version: "3.3"
|
||||
services:
|
||||
api:
|
||||
image: "docker.kvacek.cz/poletani/api:latest"
|
||||
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
|
||||
network_mode: "host"
|
||||
restart: "always"
|
||||
env_file:
|
||||
- .env
|
||||
- ../.env
|
||||
environment:
|
||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./static:/app/static
|
||||
|
||||
+19
-11
@@ -3,12 +3,13 @@ services:
|
||||
image: mariadb:10.11
|
||||
ports:
|
||||
- 3306:3306
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- MYSQL_USER=root
|
||||
- MYSQL_DATABASE=ull_tracker
|
||||
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
|
||||
MARIADB_ROOT_PASSWORD: "rootroot"
|
||||
|
||||
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
|
||||
api:
|
||||
build:
|
||||
@@ -17,24 +18,31 @@ services:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- .:/app
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
TZ: "Europe/Prague"
|
||||
API_URL: "http://localhost:8000"
|
||||
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:
|
||||
build:
|
||||
context: ../crawlers
|
||||
environment:
|
||||
- MYSQL_USER=root
|
||||
- MYSQL_PASSWORD=
|
||||
- MYSQL_DATABASE=ull_tracker
|
||||
- MYSQL_HOST=db
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql:rw
|
||||
- ../crawlers:/app
|
||||
|
||||
volumes:
|
||||
db-data: { }
|
||||
db-data: { }
|
||||
|
||||
+21
-14
@@ -1,15 +1,22 @@
|
||||
fastapi==0.100.0
|
||||
fastapi-jwt==0.1.12
|
||||
strawberry-graphql[fastapi]==0.194.4
|
||||
uvicorn==0.22.0
|
||||
sqlalchemy[asyncio] >= 2.0.9
|
||||
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
|
||||
fastapi==0.115.7
|
||||
fastapi-jwt[authlib]==0.3.0
|
||||
bcrypt==4.2.1
|
||||
fastapi-mail==1.4.2
|
||||
strawberry-graphql[fastapi]==0.258.0
|
||||
|
||||
pillow
|
||||
exif
|
||||
aiocache
|
||||
aiohttp
|
||||
lxml
|
||||
gunicorn == 23.0.0
|
||||
uvicorn[standard] == 0.34.0
|
||||
|
||||
sqlalchemy[asyncio] == 2.0.54
|
||||
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
|
||||
|
||||
APP_DEBUG = True
|
||||
APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
|
||||
GRAPHIQL = True
|
||||
|
||||
REFRESH_TOKEN_VALIDITY_DAYS = 30
|
||||
|
||||
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()
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
class BaseEndpoint(object):
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
super().__init__()
|
||||
|
||||
|
||||
class AuthEndpoint:
|
||||
def __init__(self, *args, **kwargs):
|
||||
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
|
||||
}
|
||||
+16
-10
@@ -4,7 +4,8 @@ from passlib.hash import bcrypt
|
||||
from sqlalchemy import select
|
||||
from starlette.responses import Response
|
||||
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
|
||||
|
||||
|
||||
@@ -13,21 +14,27 @@ class LoginInput(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
class LoginEndpoint(AuthEndpoint, BaseEndpoint):
|
||||
class LoginEndpoint(AuthEndpoint):
|
||||
async def on_post(self, user_data: LoginInput, resp: Response) -> dict:
|
||||
query = select(User).filter_by(email=user_data.email)
|
||||
logged_user = (await self.db.scalars(query)).first()
|
||||
|
||||
if not logged_user:
|
||||
raise HTTPException(status_code=401, detail="Invalid user")
|
||||
async with get_session() as db:
|
||||
logged_user = (await db.scalars(query)).first()
|
||||
if not logged_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")
|
||||
|
||||
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)
|
||||
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.refresh_security.set_refresh_cookie(
|
||||
resp, refresh_token,
|
||||
@@ -35,9 +42,8 @@ class LoginEndpoint(AuthEndpoint, BaseEndpoint):
|
||||
)
|
||||
|
||||
return {
|
||||
"user": logged_user.as_dict(),
|
||||
"access_token": access_token,
|
||||
"access_token_validity": self.access_security.access_expires_delta.total_seconds(),
|
||||
"user": user,
|
||||
"access_token": access_token
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, BackgroundTasks
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, root_validator, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from background_jobs.send_email import send_email
|
||||
from database.models import User
|
||||
from endpoints.base import BaseEndpoint
|
||||
from database.transaction import get_session
|
||||
from passlib.hash import bcrypt
|
||||
from emails.emails import WelcomeEmail
|
||||
|
||||
|
||||
class RegistrationInput(BaseModel):
|
||||
email: str = Field(..., min_length=4)
|
||||
name: Optional[str]
|
||||
name: str | None
|
||||
password: str
|
||||
|
||||
@root_validator()
|
||||
@model_validator(mode="before")
|
||||
def validate_email(cls, values):
|
||||
email = values.get("email") or ""
|
||||
|
||||
@@ -23,22 +24,23 @@ class RegistrationInput(BaseModel):
|
||||
return values
|
||||
|
||||
|
||||
class RegistrationEndpoint(BaseEndpoint):
|
||||
|
||||
async def on_post(self, user_data: RegistrationInput) -> User:
|
||||
class RegistrationEndpoint:
|
||||
@staticmethod
|
||||
async def on_post(user_data: RegistrationInput, backgorund_tasks: BackgroundTasks) -> User:
|
||||
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:
|
||||
raise HTTPException(status_code=422, detail="User already exists")
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=422, detail="User already exists")
|
||||
|
||||
model = await User.create(self.db, {
|
||||
"name": user_data.name,
|
||||
"email": user_data.email,
|
||||
"password_hashed": bcrypt.hash(user_data.password),
|
||||
"description": ""
|
||||
})
|
||||
model = await User.create(db, {
|
||||
"name": user_data.name,
|
||||
"email": user_data.email,
|
||||
"password_hashed": bcrypt.hash(user_data.password),
|
||||
"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
|
||||
|
||||
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:
|
||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||
ELEVATION_ENDPOINT: str
|
||||
|
||||
def get_request(self, points: List[Dict[str, float]]):
|
||||
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
...
|
||||
|
||||
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 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()
|
||||
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)
|
||||
|
||||
return [{
|
||||
"lat": loc['latitude'],
|
||||
"lng": loc['longitude'],
|
||||
"elevation": loc['elevation']
|
||||
} for loc in response['results']]
|
||||
elevation = []
|
||||
for i, point in enumerate(points):
|
||||
response_point = response['results'][i]
|
||||
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
|
||||
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 typing import List, Dict
|
||||
|
||||
from aiocache import cached
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any
|
||||
from lxml import etree
|
||||
from lxml.etree import _ElementTree
|
||||
|
||||
|
||||
class GPXParser:
|
||||
@@ -12,8 +10,55 @@ class GPXParser:
|
||||
self.gpx = etree.parse(self.file)
|
||||
self.namespace = None
|
||||
self.precision_digits = 6
|
||||
self.max_samples_per_minute = 2
|
||||
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):
|
||||
namespace = self.gpx.getroot().nsmap.get(None)
|
||||
self.namespace = {'gpx': namespace}
|
||||
@@ -21,73 +66,57 @@ class GPXParser:
|
||||
def run_xpath(self, path: str):
|
||||
return self.gpx.xpath(path, namespaces=self.namespace)
|
||||
|
||||
@cached()
|
||||
async def get_times(self):
|
||||
nodes = self.run_xpath("//gpx:time")
|
||||
async def get_times_all(self):
|
||||
nodes = self.run_xpath("//gpx:trkpt/gpx:time")
|
||||
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]]:
|
||||
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")
|
||||
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
|
||||
|
||||
@cached()
|
||||
async def get_speed(self) -> List[int]:
|
||||
async def get_speed(self) -> List[float]:
|
||||
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[int]:
|
||||
async def get_magnetic_variation(self) -> List[float]:
|
||||
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]:
|
||||
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]:
|
||||
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):
|
||||
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):
|
||||
speeds = await self.get_speed()
|
||||
if not speeds:
|
||||
return 0
|
||||
|
||||
return round(sum(speeds) / len(speeds), 2)
|
||||
|
||||
@cached()
|
||||
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):
|
||||
altitudes = await self.get_altitude()
|
||||
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
+18
-9
@@ -3,6 +3,7 @@ import urllib.parse
|
||||
from typing import Tuple, Dict
|
||||
import aiohttp
|
||||
from aiocache import cached
|
||||
from decorators.retryable import retryable
|
||||
|
||||
|
||||
class Weather:
|
||||
@@ -37,22 +38,30 @@ class Weather:
|
||||
query_string = urllib.parse.urlencode(params)
|
||||
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]):
|
||||
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
resp.raise_for_status()
|
||||
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)
|
||||
|
||||
# TODO: kontrola timezone!
|
||||
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
|
||||
idx = date_time.hour
|
||||
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
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user