geovisio 2.9.0__py3-none-any.whl → 2.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geovisio/__init__.py +6 -1
- geovisio/config_app.py +5 -5
- geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +55 -2
- geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +193 -139
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +53 -4
- geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +91 -3
- geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
- geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/messages.pot +185 -129
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +292 -63
- geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/oc/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
- geovisio/utils/annotations.py +14 -17
- geovisio/utils/auth.py +14 -13
- geovisio/utils/cql2.py +2 -2
- geovisio/utils/fields.py +14 -2
- geovisio/utils/items.py +44 -0
- geovisio/utils/model_query.py +2 -2
- geovisio/utils/pic_shape.py +1 -1
- geovisio/utils/pictures.py +111 -18
- geovisio/utils/semantics.py +32 -3
- geovisio/utils/sentry.py +1 -1
- geovisio/utils/sequences.py +51 -34
- geovisio/utils/upload_set.py +285 -198
- geovisio/utils/website.py +1 -1
- geovisio/web/annotations.py +209 -68
- geovisio/web/auth.py +1 -1
- geovisio/web/collections.py +26 -22
- geovisio/web/configuration.py +24 -4
- geovisio/web/docs.py +93 -11
- geovisio/web/items.py +197 -121
- geovisio/web/params.py +44 -31
- geovisio/web/pictures.py +34 -0
- geovisio/web/tokens.py +49 -1
- geovisio/web/upload_set.py +150 -32
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +2 -2
- geovisio/workers/runner_pictures.py +128 -23
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/METADATA +13 -13
- geovisio-2.10.0.dist-info/RECORD +105 -0
- geovisio-2.9.0.dist-info/RECORD +0 -98
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/WHEEL +0 -0
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
geovisio/utils/upload_set.py
CHANGED
|
@@ -6,6 +6,7 @@ from geovisio.utils.extent import TemporalExtent
|
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
from typing import Optional, List, Dict, Any
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from geovisio.utils import cql2, db, sequences
|
|
10
11
|
from geovisio import errors
|
|
11
12
|
from geovisio.utils.link import make_link, Link
|
|
@@ -16,6 +17,7 @@ from psycopg.rows import class_row, dict_row
|
|
|
16
17
|
from flask import current_app
|
|
17
18
|
from flask_babel import gettext as _
|
|
18
19
|
from geopic_tag_reader import sequence as geopic_sequence, reader
|
|
20
|
+
from geovisio.utils.tags import SemanticTag
|
|
19
21
|
|
|
20
22
|
from geovisio.utils.loggers import getLoggerWithExtra
|
|
21
23
|
|
|
@@ -73,15 +75,21 @@ class UploadSet(BaseModel):
|
|
|
73
75
|
title: str
|
|
74
76
|
estimated_nb_files: Optional[int] = None
|
|
75
77
|
sort_method: geopic_sequence.SortMethod
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
no_split: Optional[bool] = None
|
|
79
|
+
split_distance: Optional[int] = None
|
|
80
|
+
split_time: Optional[timedelta] = None
|
|
81
|
+
no_deduplication: Optional[bool] = None
|
|
82
|
+
duplicate_distance: Optional[float] = None
|
|
83
|
+
duplicate_rotation: Optional[int] = None
|
|
80
84
|
metadata: Optional[Dict[str, Any]]
|
|
81
85
|
user_agent: Optional[str] = Field(exclude=True)
|
|
82
86
|
associated_collections: List[AssociatedCollection] = []
|
|
83
87
|
nb_items: int = 0
|
|
84
88
|
items_status: Optional[AggregatedStatus] = None
|
|
89
|
+
semantics: List[SemanticTag] = Field(default_factory=list)
|
|
90
|
+
"""Semantic tags associated to the upload_set"""
|
|
91
|
+
relative_heading: Optional[int] = None
|
|
92
|
+
"""The relative heading (in degrees), offset based on movement path (0° = looking forward, -90° = looking left, 90° = looking right). For single picture upload_sets, 0° is heading north). Is applied to all associated collections if set."""
|
|
85
93
|
|
|
86
94
|
@computed_field
|
|
87
95
|
@property
|
|
@@ -128,19 +136,13 @@ class FileRejectionStatus(Enum):
|
|
|
128
136
|
"""other_error means there was an error that is not related to the picture itself"""
|
|
129
137
|
|
|
130
138
|
|
|
131
|
-
class FileRejectionDetails(BaseModel):
|
|
132
|
-
|
|
133
|
-
missing_fields: List[str]
|
|
134
|
-
"""Mandatory metadata missing from the file. Metadata can be `datetime` or `location`."""
|
|
135
|
-
|
|
136
|
-
|
|
137
139
|
class FileRejection(BaseModel):
|
|
138
140
|
"""Details about a file rejection"""
|
|
139
141
|
|
|
140
142
|
reason: str
|
|
141
143
|
severity: FileRejectionStatusSeverity
|
|
142
144
|
message: Optional[str]
|
|
143
|
-
details: Optional[
|
|
145
|
+
details: Optional[Dict[str, Any]]
|
|
144
146
|
|
|
145
147
|
model_config = ConfigDict(use_enum_values=True, use_attribute_docstrings=True)
|
|
146
148
|
|
|
@@ -231,8 +233,8 @@ def get_upload_set(id: UUID) -> Optional[UploadSet]:
|
|
|
231
233
|
SQL(
|
|
232
234
|
"""WITH picture_last_job AS (
|
|
233
235
|
SELECT p.id as picture_id,
|
|
234
|
-
-- Note: to know if a picture is
|
|
235
|
-
-- If there is no finished_at, the picture is still
|
|
236
|
+
-- Note: to know if a picture is being processed, check the latest job_history entry for this picture
|
|
237
|
+
-- If there is no finished_at, the picture is still being processed
|
|
236
238
|
(MAX(ARRAY [started_at, finished_at])) AS last_job,
|
|
237
239
|
p.preparing_status,
|
|
238
240
|
p.status,
|
|
@@ -243,13 +245,13 @@ def get_upload_set(id: UUID) -> Optional[UploadSet]:
|
|
|
243
245
|
GROUP BY p.id
|
|
244
246
|
),
|
|
245
247
|
picture_statuses AS (
|
|
246
|
-
SELECT
|
|
248
|
+
SELECT
|
|
247
249
|
*,
|
|
248
250
|
(last_job[1] IS NOT NULL AND last_job[2] IS NULL) AS is_job_running
|
|
249
251
|
FROM picture_last_job psj
|
|
250
252
|
),
|
|
251
253
|
associated_collections AS (
|
|
252
|
-
SELECT
|
|
254
|
+
SELECT
|
|
253
255
|
ps.upload_set_id,
|
|
254
256
|
COUNT(ps.picture_id) FILTER (WHERE ps.preparing_status = 'broken') AS nb_broken,
|
|
255
257
|
COUNT(ps.picture_id) FILTER (WHERE ps.preparing_status = 'prepared') AS nb_prepared,
|
|
@@ -268,6 +270,15 @@ associated_collections AS (
|
|
|
268
270
|
GROUP BY ps.upload_set_id,
|
|
269
271
|
s.id
|
|
270
272
|
),
|
|
273
|
+
semantics AS (
|
|
274
|
+
SELECT upload_set_id, json_agg(json_strip_nulls(json_build_object(
|
|
275
|
+
'key', key,
|
|
276
|
+
'value', value
|
|
277
|
+
)) ORDER BY key, value) AS semantics
|
|
278
|
+
FROM upload_sets_semantics
|
|
279
|
+
WHERE upload_set_id = %(id)s
|
|
280
|
+
GROUP BY upload_set_id
|
|
281
|
+
),
|
|
271
282
|
upload_set_statuses AS (
|
|
272
283
|
SELECT ps.upload_set_id,
|
|
273
284
|
COUNT(ps.picture_id) AS nb_items,
|
|
@@ -280,13 +291,14 @@ upload_set_statuses AS (
|
|
|
280
291
|
)
|
|
281
292
|
SELECT u.*,
|
|
282
293
|
COALESCE(us.nb_items, 0) AS nb_items,
|
|
294
|
+
COALESCE(s.semantics, '[]'::json) AS semantics,
|
|
283
295
|
json_build_object(
|
|
284
296
|
'broken', COALESCE(us.nb_broken, 0),
|
|
285
297
|
'prepared', COALESCE(us.nb_prepared, 0),
|
|
286
298
|
'not_processed', COALESCE(us.nb_not_processed, 0),
|
|
287
299
|
'preparing', COALESCE(us.nb_preparing, 0),
|
|
288
300
|
'rejected', (
|
|
289
|
-
SELECT count(*) FROM files
|
|
301
|
+
SELECT count(*) FROM files
|
|
290
302
|
WHERE upload_set_id = %(id)s AND rejection_status IS NOT NULL
|
|
291
303
|
)
|
|
292
304
|
) AS items_status,
|
|
@@ -327,6 +339,7 @@ SELECT u.*,
|
|
|
327
339
|
) AS associated_collections
|
|
328
340
|
FROM upload_sets u
|
|
329
341
|
LEFT JOIN upload_set_statuses us on us.upload_set_id = u.id
|
|
342
|
+
LEFT JOIN semantics s on s.upload_set_id = u.id
|
|
330
343
|
WHERE u.id = %(id)s"""
|
|
331
344
|
),
|
|
332
345
|
{"id": id},
|
|
@@ -365,30 +378,23 @@ def list_upload_sets(account_id: UUID, limit: int = 100, filter: Optional[str] =
|
|
|
365
378
|
l = db.fetchall(
|
|
366
379
|
current_app,
|
|
367
380
|
SQL(
|
|
368
|
-
"""SELECT
|
|
381
|
+
"""SELECT
|
|
369
382
|
u.*,
|
|
370
383
|
COALESCE(
|
|
371
384
|
(
|
|
372
|
-
SELECT
|
|
385
|
+
SELECT
|
|
373
386
|
json_agg(json_build_object(
|
|
374
|
-
'id',
|
|
375
|
-
'nb_items',
|
|
387
|
+
'id', s.id,
|
|
388
|
+
'nb_items', s.nb_pictures
|
|
376
389
|
))
|
|
377
|
-
FROM
|
|
378
|
-
|
|
379
|
-
sp.seq_id as collection_id,
|
|
380
|
-
count(sp.pic_id) AS nb_items
|
|
381
|
-
FROM pictures p
|
|
382
|
-
JOIN sequences_pictures sp ON sp.pic_id = p.id
|
|
383
|
-
WHERE p.upload_set_id = u.id
|
|
384
|
-
GROUP BY sp.seq_id
|
|
385
|
-
) ac
|
|
390
|
+
FROM sequences s
|
|
391
|
+
WHERE s.upload_set_id = u.id
|
|
386
392
|
),
|
|
387
393
|
'[]'::json
|
|
388
394
|
) AS associated_collections,
|
|
389
395
|
(
|
|
390
396
|
SELECT count(*) AS nb
|
|
391
|
-
FROM pictures p
|
|
397
|
+
FROM pictures p
|
|
392
398
|
WHERE p.upload_set_id = u.id
|
|
393
399
|
) AS nb_items
|
|
394
400
|
FROM upload_sets u
|
|
@@ -408,7 +414,7 @@ def ask_for_dispatch(upload_set_id: UUID):
|
|
|
408
414
|
"""Add a dispatch task to the job queue for the upload set. If there is already a task, postpone it."""
|
|
409
415
|
with db.conn(current_app) as conn:
|
|
410
416
|
conn.execute(
|
|
411
|
-
"""INSERT INTO
|
|
417
|
+
"""INSERT INTO
|
|
412
418
|
job_queue(sequence_id, task)
|
|
413
419
|
VALUES (%(upload_set_id)s, 'dispatch')
|
|
414
420
|
ON CONFLICT (upload_set_id) DO UPDATE SET ts = CURRENT_TIMESTAMP""",
|
|
@@ -416,7 +422,13 @@ def ask_for_dispatch(upload_set_id: UUID):
|
|
|
416
422
|
)
|
|
417
423
|
|
|
418
424
|
|
|
419
|
-
|
|
425
|
+
@dataclass
|
|
426
|
+
class PicToDelete:
|
|
427
|
+
picture_id: UUID
|
|
428
|
+
detail: Optional[Dict] = None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def dispatch(conn: psycopg.Connection, upload_set_id: UUID):
|
|
420
432
|
"""Finalize an upload set.
|
|
421
433
|
|
|
422
434
|
For the moment we only create a collection around all the items of the upload set, but later we'll split the items into several collections
|
|
@@ -429,13 +441,15 @@ def dispatch(upload_set_id: UUID):
|
|
|
429
441
|
raise Exception(f"Upload set {upload_set_id} not found")
|
|
430
442
|
|
|
431
443
|
logger = getLoggerWithExtra("geovisio.upload_set", {"upload_set_id": str(upload_set_id)})
|
|
432
|
-
with
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
444
|
+
with conn.transaction(), conn.cursor(row_factory=dict_row) as cursor:
|
|
445
|
+
# we put a lock on the upload set, to avoid new semantics being added while dispatching it
|
|
446
|
+
# Note: I did not find a way to only put a lock on the upload_sets_semantics table, so we lock the whole upload_set row (and any child rows)
|
|
447
|
+
_us_lock = cursor.execute(SQL("SELECT id FROM upload_sets WHERE id = %s FOR UPDATE"), [upload_set_id])
|
|
448
|
+
|
|
449
|
+
# get all the pictures of the upload set
|
|
450
|
+
db_pics = cursor.execute(
|
|
451
|
+
SQL(
|
|
452
|
+
"""SELECT
|
|
439
453
|
p.id,
|
|
440
454
|
p.ts,
|
|
441
455
|
ST_X(p.geom) as lon,
|
|
@@ -444,132 +458,197 @@ def dispatch(upload_set_id: UUID):
|
|
|
444
458
|
p.metadata->>'originalFileName' as file_name,
|
|
445
459
|
p.metadata,
|
|
446
460
|
s.id as sequence_id,
|
|
447
|
-
f is null as has_no_file
|
|
461
|
+
f is null as has_no_file,
|
|
462
|
+
p.heading_computed
|
|
448
463
|
FROM pictures p
|
|
449
464
|
LEFT JOIN sequences_pictures sp ON sp.pic_id = p.id
|
|
450
465
|
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
451
466
|
LEFT JOIN files f ON f.picture_id = p.id
|
|
452
467
|
WHERE p.upload_set_id = %(upload_set_id)s"""
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
# there is currently a bug where 2 pictures can be uploaded for the same file, so only 1 is associated to it.
|
|
458
|
-
# we want to delete one of them
|
|
459
|
-
# Those duplicates happen when a client send an upload that timeouts, but the client retries the upload and the server is not aware of this timeout (the connection is not closed).
|
|
460
|
-
# Note: later, if we are confident the bug has been removed, we might clean this code.
|
|
461
|
-
pics_to_delete_bug = [p["id"] for p in db_pics if p["has_no_file"]]
|
|
462
|
-
db_pics = [p for p in db_pics if p["has_no_file"] is False] # pictures without files will be deleted, we don't need them
|
|
463
|
-
pics_by_filename = {p["file_name"]: p for p in db_pics}
|
|
464
|
-
|
|
465
|
-
pics = [
|
|
466
|
-
geopic_sequence.Picture(
|
|
467
|
-
p["file_name"],
|
|
468
|
-
reader.GeoPicTags(
|
|
469
|
-
lon=p["lon"],
|
|
470
|
-
lat=p["lat"],
|
|
471
|
-
ts=p["ts"],
|
|
472
|
-
type=p["metadata"]["type"],
|
|
473
|
-
heading=p["heading"],
|
|
474
|
-
make=p["metadata"]["make"],
|
|
475
|
-
model=p["metadata"]["model"],
|
|
476
|
-
focal_length=p["metadata"]["focal_length"],
|
|
477
|
-
crop=p["metadata"]["crop"],
|
|
478
|
-
exif={},
|
|
479
|
-
),
|
|
480
|
-
)
|
|
481
|
-
for p in db_pics
|
|
482
|
-
]
|
|
468
|
+
),
|
|
469
|
+
{"upload_set_id": upload_set_id},
|
|
470
|
+
).fetchall()
|
|
483
471
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
472
|
+
config = cursor.execute(
|
|
473
|
+
SQL(
|
|
474
|
+
"SELECT default_split_distance, default_split_time, default_duplicate_distance, default_duplicate_rotation FROM configurations"
|
|
475
|
+
)
|
|
476
|
+
).fetchone()
|
|
477
|
+
|
|
478
|
+
# there is currently a bug where 2 pictures can be uploaded for the same file, so only 1 is associated to it.
|
|
479
|
+
# we want to delete one of them
|
|
480
|
+
# Those duplicates happen when a client send an upload that timeouts, but the client retries the upload and the server is not aware of this timeout (the connection is not closed).
|
|
481
|
+
# Note: later, if we are confident the bug has been removed, we might clean this code.
|
|
482
|
+
pics_to_delete_bug = [PicToDelete(picture_id=p["id"]) for p in db_pics if p["has_no_file"]]
|
|
483
|
+
db_pics = [p for p in db_pics if p["has_no_file"] is False] # pictures without files will be deleted, we don't need them
|
|
484
|
+
pics_by_filename = {p["file_name"]: p for p in db_pics}
|
|
485
|
+
|
|
486
|
+
pics = [
|
|
487
|
+
geopic_sequence.Picture(
|
|
488
|
+
p["file_name"],
|
|
489
|
+
reader.GeoPicTags(
|
|
490
|
+
lon=p["lon"],
|
|
491
|
+
lat=p["lat"],
|
|
492
|
+
ts=p["ts"],
|
|
493
|
+
type=p["metadata"]["type"],
|
|
494
|
+
heading=p["heading"],
|
|
495
|
+
make=p["metadata"]["make"],
|
|
496
|
+
model=p["metadata"]["model"],
|
|
497
|
+
focal_length=p["metadata"]["focal_length"],
|
|
498
|
+
crop=p["metadata"]["crop"],
|
|
499
|
+
exif={},
|
|
492
500
|
),
|
|
501
|
+
heading_computed=p["heading_computed"],
|
|
493
502
|
)
|
|
494
|
-
|
|
503
|
+
for p in db_pics
|
|
504
|
+
]
|
|
495
505
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
)
|
|
502
|
-
|
|
506
|
+
split_params = None
|
|
507
|
+
if not db_upload_set.no_split:
|
|
508
|
+
distance = db_upload_set.split_distance if db_upload_set.split_distance is not None else config["default_split_distance"]
|
|
509
|
+
t = db_upload_set.split_time if db_upload_set.split_time is not None else config["default_split_time"]
|
|
510
|
+
if t is not None and distance is not None:
|
|
511
|
+
split_params = geopic_sequence.SplitParams(maxDistance=distance, maxTime=t.total_seconds())
|
|
512
|
+
merge_params = None
|
|
513
|
+
if not db_upload_set.no_deduplication:
|
|
514
|
+
distance = (
|
|
515
|
+
db_upload_set.duplicate_distance if db_upload_set.duplicate_distance is not None else config["default_duplicate_distance"]
|
|
516
|
+
)
|
|
517
|
+
rotation = (
|
|
518
|
+
db_upload_set.duplicate_rotation if db_upload_set.duplicate_rotation is not None else config["default_duplicate_rotation"]
|
|
519
|
+
)
|
|
520
|
+
if distance is not None and rotation is not None:
|
|
521
|
+
merge_params = geopic_sequence.MergeParams(maxDistance=distance, maxRotationAngle=rotation)
|
|
503
522
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
523
|
+
report = geopic_sequence.dispatch_pictures(
|
|
524
|
+
pics, mergeParams=merge_params, sortMethod=db_upload_set.sort_method, splitParams=split_params
|
|
525
|
+
)
|
|
526
|
+
reused_sequence = set()
|
|
527
|
+
|
|
528
|
+
pics_to_delete_duplicates = [
|
|
529
|
+
PicToDelete(
|
|
530
|
+
picture_id=pics_by_filename[d.picture.filename]["id"],
|
|
531
|
+
detail={
|
|
532
|
+
"duplicate_of": str(pics_by_filename[d.duplicate_of.filename]["id"]),
|
|
533
|
+
"distance": d.distance,
|
|
534
|
+
"angle": d.angle,
|
|
535
|
+
},
|
|
536
|
+
)
|
|
537
|
+
for d in report.duplicate_pictures
|
|
538
|
+
]
|
|
539
|
+
pics_to_delete = pics_to_delete_duplicates + pics_to_delete_bug
|
|
540
|
+
if pics_to_delete:
|
|
541
|
+
logger.debug(
|
|
542
|
+
f"nb duplicate pictures {len(pics_to_delete_duplicates)} {f' and {len(pics_to_delete_bug)} pictures without files' if pics_to_delete_bug else ''}"
|
|
543
|
+
)
|
|
544
|
+
logger.debug(f"duplicate pictures {[p.picture.filename for p in report.duplicate_pictures]}")
|
|
508
545
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
)
|
|
546
|
+
cursor.execute(SQL("CREATE TEMPORARY TABLE tmp_duplicates(picture_id UUID, details JSONB) ON COMMIT DROP"))
|
|
547
|
+
with cursor.copy("COPY tmp_duplicates(picture_id, details) FROM stdin;") as copy:
|
|
548
|
+
for p in pics_to_delete:
|
|
549
|
+
copy.write_row((p.picture_id, Jsonb(p.detail) if p.detail else None))
|
|
550
|
+
|
|
551
|
+
cursor.execute(
|
|
552
|
+
SQL(
|
|
553
|
+
"""UPDATE files SET
|
|
554
|
+
rejection_status = 'capture_duplicate', rejection_details = d.details
|
|
555
|
+
FROM tmp_duplicates d
|
|
556
|
+
WHERE d.picture_id = files.picture_id"""
|
|
513
557
|
)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
558
|
+
)
|
|
559
|
+
# set all the pictures as waiting for deletion and add background jobs to delete them
|
|
560
|
+
# Note: we do not delte the picture's row because it can cause some deadlocks if some workers are preparing thoses pictures.
|
|
561
|
+
cursor.execute(SQL("UPDATE pictures SET status = 'waiting-for-delete' WHERE id IN (select picture_id FROM tmp_duplicates)"))
|
|
562
|
+
cursor.execute(
|
|
563
|
+
SQL(
|
|
564
|
+
"""INSERT INTO job_queue(picture_to_delete_id, task)
|
|
565
|
+
SELECT picture_id, 'delete' FROM tmp_duplicates"""
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
number_title = len(report.sequences) > 1
|
|
570
|
+
existing_sequences = set(p["sequence_id"] for p in db_pics if p["sequence_id"])
|
|
571
|
+
new_sequence_ids = set()
|
|
572
|
+
for i, s in enumerate(report.sequences, start=1):
|
|
573
|
+
existing_sequence = next(
|
|
574
|
+
(seq for p in s.pictures if (seq := pics_by_filename[p.filename]["sequence_id"]) not in reused_sequence),
|
|
575
|
+
None,
|
|
576
|
+
)
|
|
577
|
+
# if some of the pictures were already in a sequence, we should not create a new one
|
|
578
|
+
if existing_sequence:
|
|
579
|
+
logger.info(f"sequence {existing_sequence} already contains pictures, we will not create a new one")
|
|
580
|
+
# we should wipe the sequences_pictures though
|
|
581
|
+
seq_id = existing_sequence
|
|
582
|
+
cursor.execute(
|
|
583
|
+
SQL("DELETE FROM sequences_pictures WHERE seq_id = %(seq_id)s"),
|
|
584
|
+
{"seq_id": seq_id},
|
|
524
585
|
)
|
|
525
|
-
|
|
526
|
-
if
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
586
|
+
reused_sequence.add(seq_id)
|
|
587
|
+
# Note: we do not update the sequences_semantics if reusing a sequence, because the sequence semantics's updates are reported to the existing sequences if there are some
|
|
588
|
+
else:
|
|
589
|
+
new_title = f"{db_upload_set.title}{f'-{i}' if number_title else ''}"
|
|
590
|
+
seq_id = cursor.execute(
|
|
591
|
+
SQL(
|
|
592
|
+
"""INSERT INTO sequences(account_id, metadata, user_agent, upload_set_id)
|
|
593
|
+
VALUES (%(account_id)s, %(metadata)s, %(user_agent)s, %(upload_set_id)s)
|
|
594
|
+
RETURNING id"""
|
|
595
|
+
),
|
|
596
|
+
{
|
|
597
|
+
"account_id": db_upload_set.account_id,
|
|
598
|
+
"metadata": Jsonb({"title": new_title}),
|
|
599
|
+
"user_agent": db_upload_set.user_agent,
|
|
600
|
+
"upload_set_id": db_upload_set.id,
|
|
601
|
+
},
|
|
602
|
+
).fetchone()
|
|
603
|
+
seq_id = seq_id["id"]
|
|
604
|
+
|
|
605
|
+
# Pass all semantics to the new sequence
|
|
606
|
+
copy_upload_set_semantics_to_sequence(cursor, db_upload_set.id, seq_id)
|
|
607
|
+
new_sequence_ids.add(seq_id)
|
|
608
|
+
|
|
609
|
+
with cursor.copy("COPY sequences_pictures(seq_id, pic_id, rank) FROM stdin;") as copy:
|
|
610
|
+
for i, p in enumerate(s.pictures, 1):
|
|
611
|
+
copy.write_row(
|
|
612
|
+
(seq_id, pics_by_filename[p.filename]["id"], i),
|
|
533
613
|
)
|
|
534
|
-
reused_sequence.add(seq_id)
|
|
535
|
-
else:
|
|
536
|
-
new_title = f"{db_upload_set.title}{f'-{i}' if number_title else ''}"
|
|
537
|
-
seq_id = cursor.execute(
|
|
538
|
-
SQL(
|
|
539
|
-
"""INSERT INTO sequences(account_id, metadata, user_agent)
|
|
540
|
-
VALUES (%(account_id)s, %(metadata)s, %(user_agent)s)
|
|
541
|
-
RETURNING id"""
|
|
542
|
-
),
|
|
543
|
-
{
|
|
544
|
-
"account_id": db_upload_set.account_id,
|
|
545
|
-
"metadata": Jsonb({"title": new_title}),
|
|
546
|
-
"user_agent": db_upload_set.user_agent,
|
|
547
|
-
},
|
|
548
|
-
).fetchone()
|
|
549
|
-
seq_id = seq_id["id"]
|
|
550
|
-
|
|
551
|
-
new_sequence_ids.add(seq_id)
|
|
552
|
-
|
|
553
|
-
with cursor.copy("COPY sequences_pictures(seq_id, pic_id, rank) FROM stdin;") as copy:
|
|
554
|
-
for i, p in enumerate(s.pictures, 1):
|
|
555
|
-
copy.write_row(
|
|
556
|
-
(seq_id, pics_by_filename[p.filename]["id"], i),
|
|
557
|
-
)
|
|
558
614
|
|
|
559
|
-
|
|
615
|
+
sequences.add_finalization_job(cursor=cursor, seqId=seq_id)
|
|
560
616
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
SQL("UPDATE sequences SET status = 'deleted' WHERE id = ANY(%(seq_ids)s)"), {"seq_ids": list(sequences_to_delete)}
|
|
568
|
-
)
|
|
617
|
+
# we can delete all the old sequences
|
|
618
|
+
sequences_to_delete = existing_sequences - new_sequence_ids
|
|
619
|
+
if sequences_to_delete:
|
|
620
|
+
logger.debug(f"sequences to delete = {sequences_to_delete} (existing = {existing_sequences}, new = {new_sequence_ids})")
|
|
621
|
+
conn.execute(SQL("DELETE FROM sequences_pictures WHERE seq_id = ANY(%(seq_ids)s)"), {"seq_ids": list(sequences_to_delete)})
|
|
622
|
+
conn.execute(SQL("UPDATE sequences SET status = 'deleted' WHERE id = ANY(%(seq_ids)s)"), {"seq_ids": list(sequences_to_delete)})
|
|
569
623
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
624
|
+
for s in report.sequences_splits or []:
|
|
625
|
+
logger.debug(f"split = {s.prevPic.filename} -> {s.nextPic.filename} : {s.reason}")
|
|
626
|
+
conn.execute(SQL("UPDATE upload_sets SET dispatched = true WHERE id = %(upload_set_id)s"), {"upload_set_id": db_upload_set.id})
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def copy_upload_set_semantics_to_sequence(cursor, db_upload_id: UUID, seq_id: UUID):
|
|
630
|
+
cursor.execute(
|
|
631
|
+
SQL(
|
|
632
|
+
"""WITH upload_set_semantics AS (
|
|
633
|
+
SELECT key, value, upload_set_id, account_id
|
|
634
|
+
FROM upload_sets_semantics
|
|
635
|
+
WHERE upload_set_id = %(upload_set_id)s
|
|
636
|
+
),
|
|
637
|
+
seq_sem AS (
|
|
638
|
+
INSERT INTO sequences_semantics(sequence_id, key, value)
|
|
639
|
+
SELECT %(seq_id)s, key, value
|
|
640
|
+
FROM upload_set_semantics
|
|
641
|
+
)
|
|
642
|
+
INSERT INTO sequences_semantics_history(sequence_id, account_id, ts, updates)
|
|
643
|
+
SELECT %(seq_id)s, account_id, NOW(), jsonb_build_object('key', key, 'value', value, 'action', 'add')
|
|
644
|
+
FROM upload_set_semantics
|
|
645
|
+
"""
|
|
646
|
+
),
|
|
647
|
+
{
|
|
648
|
+
"upload_set_id": db_upload_id,
|
|
649
|
+
"seq_id": seq_id,
|
|
650
|
+
},
|
|
651
|
+
)
|
|
573
652
|
|
|
574
653
|
|
|
575
654
|
def insertFileInDatabase(
|
|
@@ -589,51 +668,59 @@ def insertFileInDatabase(
|
|
|
589
668
|
|
|
590
669
|
# we check if there is already a file with this name in the upload set with an associated picture.
|
|
591
670
|
# If there is no picture (because the picture has been rejected), we accept that the file is overridden
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
671
|
+
with cursor.connection.transaction():
|
|
672
|
+
existing_file = cursor.execute(
|
|
673
|
+
SQL(
|
|
674
|
+
"""SELECT picture_id, rejection_status
|
|
675
|
+
FROM files
|
|
676
|
+
WHERE upload_set_id = %(upload_set_id)s AND file_name = %(file_name)s AND picture_id IS NOT NULL"""
|
|
677
|
+
),
|
|
678
|
+
params={
|
|
679
|
+
"upload_set_id": upload_set_id,
|
|
680
|
+
"file_name": file_name,
|
|
681
|
+
},
|
|
682
|
+
).fetchone()
|
|
683
|
+
if existing_file:
|
|
684
|
+
raise errors.InvalidAPIUsage(
|
|
685
|
+
_("A different picture with the same name has already been added to this uploadset"),
|
|
686
|
+
status_code=409,
|
|
687
|
+
payload={"existing_item": {"id": existing_file["picture_id"]}},
|
|
688
|
+
)
|
|
609
689
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
VALUES (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
ON CONFLICT (upload_set_id, file_name)
|
|
619
|
-
DO UPDATE SET picture_id = %(picture_id)s, size = %(size)s, content_md5 = %(content_md5)s,
|
|
620
|
-
|
|
621
|
-
WHERE files.picture_id IS NULL -- check again that we do not override an existing picture
|
|
622
|
-
RETURNING *"""
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
690
|
+
f = cursor.execute(
|
|
691
|
+
SQL(
|
|
692
|
+
"""INSERT INTO files(
|
|
693
|
+
upload_set_id, picture_id, file_type, file_name,
|
|
694
|
+
size, content_md5, rejection_status, rejection_message, rejection_details)
|
|
695
|
+
VALUES (
|
|
696
|
+
%(upload_set_id)s, %(picture_id)s, %(type)s, %(file_name)s,
|
|
697
|
+
%(size)s, %(content_md5)s, %(rejection_status)s, %(rejection_message)s, %(rejection_details)s)
|
|
698
|
+
ON CONFLICT (upload_set_id, file_name)
|
|
699
|
+
DO UPDATE SET picture_id = %(picture_id)s, size = %(size)s, content_md5 = %(content_md5)s,
|
|
700
|
+
rejection_status = %(rejection_status)s, rejection_message = %(rejection_message)s, rejection_details = %(rejection_details)s
|
|
701
|
+
WHERE files.picture_id IS NULL -- check again that we do not override an existing picture
|
|
702
|
+
RETURNING *"""
|
|
703
|
+
),
|
|
704
|
+
params={
|
|
705
|
+
"upload_set_id": upload_set_id,
|
|
706
|
+
"type": file_type,
|
|
707
|
+
"picture_id": picture_id,
|
|
708
|
+
"file_name": file_name,
|
|
709
|
+
"size": size,
|
|
710
|
+
"content_md5": content_md5,
|
|
711
|
+
"rejection_status": rejection_status,
|
|
712
|
+
"rejection_message": rejection_message,
|
|
713
|
+
"rejection_details": Jsonb(rejection_details),
|
|
714
|
+
},
|
|
715
|
+
)
|
|
716
|
+
u = f.fetchone()
|
|
717
|
+
if u is None:
|
|
718
|
+
logging.error(f"Impossible to add file {file_name} to uploadset {upload_set_id}")
|
|
719
|
+
raise errors.InvalidAPIUsage(
|
|
720
|
+
_("Impossible to add the picture to this uploadset"),
|
|
721
|
+
status_code=500,
|
|
722
|
+
)
|
|
723
|
+
return UploadSetFile(**u)
|
|
637
724
|
|
|
638
725
|
|
|
639
726
|
def get_upload_set_files(upload_set_id: UUID) -> UploadSetFiles:
|
|
@@ -642,15 +729,15 @@ def get_upload_set_files(upload_set_id: UUID) -> UploadSetFiles:
|
|
|
642
729
|
current_app,
|
|
643
730
|
SQL(
|
|
644
731
|
"""SELECT
|
|
645
|
-
upload_set_id,
|
|
646
|
-
file_type,
|
|
647
|
-
file_name,
|
|
648
|
-
size,
|
|
649
|
-
content_md5,
|
|
732
|
+
upload_set_id,
|
|
733
|
+
file_type,
|
|
734
|
+
file_name,
|
|
735
|
+
size,
|
|
736
|
+
content_md5,
|
|
650
737
|
rejection_status,
|
|
651
738
|
rejection_message,
|
|
652
739
|
rejection_details,
|
|
653
|
-
picture_id,
|
|
740
|
+
picture_id,
|
|
654
741
|
inserted_at
|
|
655
742
|
FROM files
|
|
656
743
|
WHERE upload_set_id = %(upload_set_id)s
|
geovisio/utils/website.py
CHANGED
|
@@ -14,7 +14,7 @@ class Website:
|
|
|
14
14
|
"""Website associated to the API.
|
|
15
15
|
This wrapper will define the routes we expect from the website.
|
|
16
16
|
|
|
17
|
-
We should limit the
|
|
17
|
+
We should limit the interaction from the api to the website, but for some flow (especially auth flows), it's can be useful to redirect to website's page
|
|
18
18
|
|
|
19
19
|
If the url is:
|
|
20
20
|
* set to `false`, there is no associated website
|