Skip to content

Commit ecf3a3f

Browse files
committed
API & Client - do not allow uploading media when file is an archive
1 parent aea39e8 commit ecf3a3f

6 files changed

Lines changed: 100 additions & 15 deletions

File tree

fittrackee/tests/workouts/test_workouts_api_1_post.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1645,7 +1645,9 @@ def test_expected_scope_is_workouts_write(
16451645
)
16461646

16471647

1648-
class TestPostWorkoutWithZipArchive(UserTaskMixin, WorkoutApiTestCaseMixin):
1648+
class TestPostWorkoutWithZipArchive(
1649+
UserTaskMixin, WorkoutApiTestCaseMixin, MediaMixin
1650+
):
16491651
def test_it_adds_workouts_synchronously_with_zip_archive(
16501652
self, app: "Flask", user_1: "User", sport_1_cycling: "Sport"
16511653
) -> None:
@@ -2044,6 +2046,44 @@ def test_it_adds_workouts_when_zip_contains_files_with_multiple_extensions(
20442046
assert len(data["data"]["workouts"]) == 3
20452047
assert Workout.query.count() == 3
20462048

2049+
def test_it_returns_400_when_media_attachments_are_provided(
2050+
self,
2051+
app: "Flask",
2052+
user_1: "User",
2053+
sport_1_cycling: "Sport",
2054+
gpx_file: str,
2055+
) -> None:
2056+
media = self.create_media(user_1)
2057+
file_path = os.path.join(app.root_path, "tests/files/gpx_test.zip")
2058+
with open(file_path, "rb") as zip_file:
2059+
client, auth_token = self.get_test_client_and_auth_token(
2060+
app, user_1.email
2061+
)
2062+
2063+
response = client.post(
2064+
"/api/workouts",
2065+
data=dict(
2066+
file=(zip_file, "gpx_test.zip"),
2067+
data=(
2068+
json.dumps(
2069+
{
2070+
"sport_id": 1,
2071+
"media_attachment_ids": [media.short_id],
2072+
}
2073+
)
2074+
),
2075+
),
2076+
headers=dict(
2077+
content_type="multipart/form-data",
2078+
Authorization=f"Bearer {auth_token}",
2079+
),
2080+
)
2081+
2082+
self.assert_400(
2083+
response,
2084+
"media attachments can not be associated with a .zip archive",
2085+
)
2086+
20472087

20482088
class TestPostAndGetWorkoutWithFile(WorkoutApiTestCaseMixin):
20492089
def workout_assertion(

fittrackee/workouts/workouts.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,16 @@ def get_string_duration_value(row_value: Optional[timedelta]) -> Optional[str]:
139139

140140

141141
def check_media_attachments(
142-
workout_data: dict,
142+
workout_data: dict, is_zip_archive: bool = False
143143
) -> Optional["InvalidPayloadErrorResponse"]:
144-
if (
145-
len(workout_data.get("media_attachment_ids", []))
146-
> MAX_MEDIA_ATTACHMENTS
147-
):
144+
media_count = len(workout_data.get("media_attachment_ids", []))
145+
146+
if media_count > 0 and is_zip_archive:
147+
return InvalidPayloadErrorResponse(
148+
"media attachments can not be associated with a .zip archive"
149+
)
150+
151+
if media_count > MAX_MEDIA_ATTACHMENTS:
148152
return InvalidPayloadErrorResponse(
149153
f"up to {MAX_MEDIA_ATTACHMENTS} media attachments can be "
150154
f"associated with a workout"
@@ -2317,6 +2321,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
23172321
- ``equipment with id <equipment_id> is inactive``
23182322
- ``one or more values, entered or calculated, exceed the limits``
23192323
- ``up to 20 media attachments can be associated with a workout``
2324+
- ``media attachments can not be associated with a .zip archive``
23202325
:statuscode 401:
23212326
- ``provide a valid auth token``
23222327
- ``signature expired, please log in again``
@@ -2349,11 +2354,16 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
23492354
if not workout_data or workout_data.get("sport_id") is None:
23502355
return InvalidPayloadErrorResponse()
23512356

2352-
error_response = check_media_attachments(workout_data)
2357+
workout_file = request.files["file"]
2358+
is_zip_archive = (
2359+
workout_file is not None
2360+
and workout_file.filename is not None
2361+
and workout_file.filename.endswith(".zip")
2362+
)
2363+
error_response = check_media_attachments(workout_data, is_zip_archive)
23532364
if error_response:
23542365
return error_response
23552366

2356-
workout_file = request.files["file"]
23572367
try:
23582368
service = WorkoutsFromFileCreationService(
23592369
auth_user, workout_data, workout_file

fittrackee_client/src/components/Workout/WorkoutEdition.vue

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@
436436
:workout-media-attachments="
437437
workout?.media_attachments ? workout.media_attachments : []
438438
"
439+
:is-archive="isArchive"
439440
/>
440441
</div>
441442
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
@@ -516,7 +517,7 @@
516517
517518
const { appConfig, errorMessages } = useApp()
518519
519-
let workoutFile: File | null = null
520+
const workoutFile: Ref<File | undefined> = ref(undefined)
520521
521522
const workoutForm = reactive({
522523
sport_id: '',
@@ -573,6 +574,13 @@
573574
appConfig.value.file_sync_limit_import !=
574575
appConfig.value.file_limit_import
575576
)
577+
const isArchive: ComputedRef<boolean> = computed(
578+
() =>
579+
isCreation.value &&
580+
withFile.value &&
581+
workoutFile.value !== undefined &&
582+
workoutFile.value.name.endsWith('.zip')
583+
)
576584
const equipments: ComputedRef<IEquipment[]> = computed(
577585
() => store.getters[EQUIPMENTS_STORE.GETTERS.EQUIPMENTS]
578586
)
@@ -651,7 +659,9 @@
651659
}
652660
function updateFile(event: Event) {
653661
if ((event.target as HTMLInputElement).files) {
654-
workoutFile = ((event.target as HTMLInputElement).files as FileList)[0]
662+
workoutFile.value = (
663+
(event.target as HTMLInputElement).files as FileList
664+
)[0]
655665
}
656666
}
657667
function formatWorkoutForm(workout: IWorkout) {
@@ -775,7 +785,7 @@
775785
equipment_ids: workoutForm.equipment_ids,
776786
title: workoutForm.title,
777787
workout_visibility: workoutForm.workoutVisibility,
778-
media_attachment_ids: mediaAttachementIds.value,
788+
media_attachment_ids: isArchive.value ? [] : mediaAttachementIds.value,
779789
media_visibility: workoutForm.mediaVisibility,
780790
}
781791
if (props.workout.id) {
@@ -798,12 +808,12 @@
798808
}
799809
} else {
800810
if (withFile.value) {
801-
if (!workoutFile) {
811+
if (!workoutFile.value) {
802812
const errorMessage = 'workouts.NO_FILE_PROVIDED'
803813
store.commit(ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES, errorMessage)
804814
return
805815
}
806-
payload.file = workoutFile
816+
payload.file = workoutFile.value
807817
payload.analysis_visibility = workoutForm.analysisVisibility
808818
payload.map_visibility = workoutForm.mapVisibility
809819
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT, payload)
@@ -858,6 +868,7 @@
858868
function updateSelectedEquipmentPieces(selectedIds: string[]) {
859869
workoutForm.equipment_ids = selectedIds
860870
}
871+
861872
watch(
862873
() => props.workout,
863874
async (
@@ -898,6 +909,14 @@
898909
}
899910
}
900911
)
912+
watch(
913+
() => withFile.value,
914+
async (newValue: boolean) => {
915+
if (!newValue) {
916+
workoutFile.value = undefined
917+
}
918+
}
919+
)
901920
902921
onMounted(() => {
903922
let element

fittrackee_client/src/components/Workout/WorkoutMediaAttachementsUpload.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
@change="uploadMediaAttachment"
1616
/>
1717
</div>
18+
<div v-if="isArchive" class="attachments-info-box info-box">
19+
<i class="fa fa-info-circle" aria-hidden="true" />
20+
<div>{{ $t('workouts.MEDIA_INFORMATION') }}</div>
21+
</div>
1822
<div class="loading-media">
1923
<i
2024
v-if="mediaLoading === 'new'"
@@ -24,7 +28,7 @@
2428
{{ ' ' }}
2529
<span v-if="mediaLoading === 'new'">{{ $t('common.LOADING') }}</span>
2630
</div>
27-
<div class="media-attachments">
31+
<div class="media-attachments" v-if="!isArchive">
2832
<div
2933
v-for="media in mediaAttachments"
3034
:key="media.id"
@@ -88,9 +92,10 @@
8892
interface Props {
8993
loading: boolean
9094
workoutMediaAttachments: IMediaAttachment[]
95+
isArchive: boolean
9196
}
9297
const props = defineProps<Props>()
93-
const { loading, workoutMediaAttachments } = toRefs(props)
98+
const { isArchive, loading, workoutMediaAttachments } = toRefs(props)
9499
95100
const store = useStore()
96101
@@ -153,6 +158,15 @@
153158

154159
<style scoped lang="scss">
155160
@use '~@/scss/vars.scss' as *;
161+
162+
.attachments-info-box {
163+
display: flex;
164+
flex-direction: row;
165+
align-items: center;
166+
gap: $default-padding * 0.5;
167+
margin: $default-margin $default-margin 0;
168+
}
169+
156170
.media-attachments {
157171
display: flex;
158172
flex-direction: column;

fittrackee_client/src/locales/en/workouts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"MAX_SIZE": "max. size",
7878
"MAX_SPEED": "max. speed",
7979
"MAX_SYNC_FILES_IN_ZIP": "max. number of files for synchronous upload",
80+
"MEDIA_INFORMATION": "photos can not be added when uploading a .zip archive.",
8081
"MIN_ALTITUDE": "min. altitude",
8182
"ELEVATION_DATA_SOURCE": {
8283
"LABEL": "Elevations updated from",

fittrackee_client/src/locales/fr/workouts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"MAX_SIZE": "taille max.",
7878
"MAX_SPEED": "vitesse max.",
7979
"MAX_SYNC_FILES_IN_ZIP": "nombre max. de fichiers pour un chargement synchrone ",
80+
"MEDIA_INFORMATION": "les photos ne peuvent pas être ajoutées si le fichier de la séance est une archive zip",
8081
"MIN_ALTITUDE": "altitude min.",
8182
"ELEVATION_DATA_SOURCE": {
8283
"LABEL": "Altitudes mises à jour à partir de",

0 commit comments

Comments
 (0)