diff --git a/.phpunit.result.cache b/.phpunit.result.cache index f2ccb6e..0c1ff08 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0,"SystemControllerHelpersTest::testDiskColorBelowWarning":0,"SystemControllerHelpersTest::testDiskColorWarning":0,"SystemControllerHelpersTest::testDiskColorCritical":0,"SystemControllerHelpersTest::testLogLineClassCrit":0,"SystemControllerHelpersTest::testLogLineClassError":0,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0,"SystemControllerHelpersTest::testLogLineClassHttp500":0,"SystemControllerHelpersTest::testLogLineClassHttp300":0,"SystemControllerHelpersTest::testLogLineClassDefault":0,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0,"SystemControllerHelpersTest::testStatusLabelInactive":0,"SystemControllerHelpersTest::testStatusLabelFailed":0,"SystemControllerHelpersTest::testStatusLabelWarn":0,"SystemControllerHelpersTest::testStatusLabelUnknown":0,"SystemControllerHelpersTest::testStatusClassOk":0,"SystemControllerHelpersTest::testStatusClassWarn":0,"SystemControllerHelpersTest::testStatusClassError":0,"SystemControllerHelpersTest::testStatusClassUnknown":0,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0.001,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.002,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0,"DatabaseExtendedTest::testRenameTagUpdatesName":0,"DatabaseExtendedTest::testMergeTagReassignsTheses":0,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.177,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.174,"ShareLinkExtendedTest::testFindBySlugHit":0.174,"ShareLinkExtendedTest::testFindBySlugMiss":0,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.174,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.175,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.174,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.172,"ShareLinkExtendedTest::testCreateWithLockedYear":0.172,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.173,"ShareLinkExtendedTest::testUpdateLockedYear":0.173,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.178,"ShareLinkExtendedTest::testIncrementUsage":0.174,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.173,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.173,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0,"AutofocusFieldForErrorTest::testEditAutofocusYear":0,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0,"ThesisCreateValidationTest::testMissingTitleThrowsException":0,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0,"ThesisCreateValidationTest::testYearZeroRejected":0,"ThesisCreateValidationTest::testYearBefore2000Rejected":0,"ThesisCreateValidationTest::testFarFutureYearRejected":0,"ThesisCreateValidationTest::testCurrentYearAccepted":0,"ThesisCreateValidationTest::testMalformedUrlRejected":0,"ThesisCreateValidationTest::testValidUrlAccepted":0,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.001,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.001,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0,"ErrorHandlerTest::testFkApPrograms":0,"ErrorHandlerTest::testFkFinalityTypes":0,"ErrorHandlerTest::testFkThesisLanguages":0,"ErrorHandlerTest::testFkThesisFormats":0,"ErrorHandlerTest::testFkThesisTags":0,"ErrorHandlerTest::testFkThesisSupervisors":0,"ErrorHandlerTest::testFkAccessTypes":0,"ErrorHandlerTest::testFkLicenseTypes":0,"ErrorHandlerTest::testFkAuthors":0,"ErrorHandlerTest::testFkQuotedTableName":0,"ErrorHandlerTest::testFkQuotedLanguages":0,"ErrorHandlerTest::testFkQuotedFormatTypes":0,"ErrorHandlerTest::testFkReferencesTags":0,"ErrorHandlerTest::testFkReferencesOrientations":0,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0,"ErrorHandlerTest::testUniqueConstraint":0,"ErrorHandlerTest::testNotNullConstraint":0,"ErrorHandlerTest::testGenericPdoError":0,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0,"ErrorHandlerTest::testValidationExceptionPassesThrough":0,"ErrorHandlerTest::testGenericExceptionPassesThrough":0,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0,"ErrorHandlerTest::testLogWithContext":0,"ErrorHandlerTest::testLogWithNullValues":0,"ErrorHandlerTest::testLogWithEmptyExtra":0,"ErrorHandlerTest::testFkQuotedColumnNames":0,"ErrorHandlerTest::testFkUpdateStatement":0,"ErrorHandlerTest::testFkWithReferencesAndInsert":0,"PureLogicTest::testSplitJuryByRoleAllRoles":0,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0,"PureLogicTest::testSplitJuryByRoleEmptyJury":0,"PureLogicTest::testCollectCaptionPathsVttByMime":0,"PureLogicTest::testCollectCaptionPathsVttByExtension":0,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0,"PureLogicTest::testDetectFileTypeByMime":0,"PureLogicTest::testDetectFileTypeByExtensionFallback":0,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.002,"SearchControllerTest::testCoverMapContainsKnownThesis":0.001}} \ No newline at end of file +{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0,"SystemControllerHelpersTest::testDiskColorBelowWarning":0,"SystemControllerHelpersTest::testDiskColorWarning":0,"SystemControllerHelpersTest::testDiskColorCritical":0,"SystemControllerHelpersTest::testLogLineClassCrit":0,"SystemControllerHelpersTest::testLogLineClassError":0,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0,"SystemControllerHelpersTest::testLogLineClassHttp500":0,"SystemControllerHelpersTest::testLogLineClassHttp300":0,"SystemControllerHelpersTest::testLogLineClassDefault":0,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0,"SystemControllerHelpersTest::testStatusLabelInactive":0,"SystemControllerHelpersTest::testStatusLabelFailed":0,"SystemControllerHelpersTest::testStatusLabelWarn":0,"SystemControllerHelpersTest::testStatusLabelUnknown":0,"SystemControllerHelpersTest::testStatusClassOk":0,"SystemControllerHelpersTest::testStatusClassWarn":0,"SystemControllerHelpersTest::testStatusClassError":0,"SystemControllerHelpersTest::testStatusClassUnknown":0,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.003,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0,"DatabaseExtendedTest::testRenameTagUpdatesName":0,"DatabaseExtendedTest::testMergeTagReassignsTheses":0,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.173,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.173,"ShareLinkExtendedTest::testFindBySlugHit":0.172,"ShareLinkExtendedTest::testFindBySlugMiss":0,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.174,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.173,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.173,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.173,"ShareLinkExtendedTest::testCreateWithLockedYear":0.173,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.173,"ShareLinkExtendedTest::testUpdateLockedYear":0.174,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.175,"ShareLinkExtendedTest::testIncrementUsage":0.173,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.173,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.173,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0,"AutofocusFieldForErrorTest::testEditAutofocusYear":0,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0,"ThesisCreateValidationTest::testMissingTitleThrowsException":0,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0,"ThesisCreateValidationTest::testYearZeroRejected":0,"ThesisCreateValidationTest::testYearBefore2000Rejected":0,"ThesisCreateValidationTest::testFarFutureYearRejected":0,"ThesisCreateValidationTest::testCurrentYearAccepted":0,"ThesisCreateValidationTest::testMalformedUrlRejected":0,"ThesisCreateValidationTest::testValidUrlAccepted":0,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.001,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.001,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0,"ErrorHandlerTest::testFkApPrograms":0,"ErrorHandlerTest::testFkFinalityTypes":0,"ErrorHandlerTest::testFkThesisLanguages":0,"ErrorHandlerTest::testFkThesisFormats":0,"ErrorHandlerTest::testFkThesisTags":0,"ErrorHandlerTest::testFkThesisSupervisors":0,"ErrorHandlerTest::testFkAccessTypes":0,"ErrorHandlerTest::testFkLicenseTypes":0,"ErrorHandlerTest::testFkAuthors":0,"ErrorHandlerTest::testFkQuotedTableName":0,"ErrorHandlerTest::testFkQuotedLanguages":0,"ErrorHandlerTest::testFkQuotedFormatTypes":0,"ErrorHandlerTest::testFkReferencesTags":0,"ErrorHandlerTest::testFkReferencesOrientations":0,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0,"ErrorHandlerTest::testUniqueConstraint":0,"ErrorHandlerTest::testNotNullConstraint":0,"ErrorHandlerTest::testGenericPdoError":0,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0,"ErrorHandlerTest::testValidationExceptionPassesThrough":0,"ErrorHandlerTest::testGenericExceptionPassesThrough":0,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0,"ErrorHandlerTest::testLogWithContext":0,"ErrorHandlerTest::testLogWithNullValues":0,"ErrorHandlerTest::testLogWithEmptyExtra":0,"ErrorHandlerTest::testFkQuotedColumnNames":0,"ErrorHandlerTest::testFkUpdateStatement":0,"ErrorHandlerTest::testFkWithReferencesAndInsert":0,"PureLogicTest::testSplitJuryByRoleAllRoles":0,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0,"PureLogicTest::testSplitJuryByRoleEmptyJury":0,"PureLogicTest::testCollectCaptionPathsVttByMime":0,"PureLogicTest::testCollectCaptionPathsVttByExtension":0,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0,"PureLogicTest::testDetectFileTypeByMime":0,"PureLogicTest::testDetectFileTypeByExtensionFallback":0,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.002,"SearchControllerTest::testCoverMapContainsKnownThesis":0.001,"DatabaseExtendedTest::testCreateThesisMinimalFields":0.004,"DatabaseExtendedTest::testCreateThesisAllOptionalFields":0,"DatabaseExtendedTest::testCreateThesisGeneratesUniqueIdentifiers":0}} \ No newline at end of file diff --git a/TODO.md b/TODO.md index ad4f4a7..398dcd8 100644 --- a/TODO.md +++ b/TODO.md @@ -3,3 +3,41 @@ - [x] Simplify `tfe.php` HTML structure (remove `.tfe-layout` wrapper, remove `.tfe-synopsis-column` wrapper) - [x] Simplify `tfe.css` — replace flex-column with grid where possible, merge `.tfe-layout` into `.tfe-content-row`, remove dead wrappers - [x] Commit changes +- [x] Remove "Mo" option from duration — keep only minutes and pages +- [x] Combine pages and minutes as separate fields (both can be set simultaneously) +- [x] Fix minutes input visibility (can't see what's typed) +- [x] Add `has_annexes` checkbox to form + DB column +- [x] Display duration on admin backoffice recap page +- [x] Update public TFE page duration display for new combined format +- [x] Fix `handleAutosaveResponse is not defined` — load `autosave-handler.js` on about_page edit view +- [x] Fix `handleAutosaveResponse is not defined` in admin add/edit — function was scoped inside IIFE bundle, not on window +- [x] Fix search result cards: title and author always below image/gradient, remove text overlay on placeholder gradient +- [x] Change search results to 10 per page with HTMX pagination (no-JS fallback preserved) +- [x] Bump to 15 results/page, make results area vertically scrollable, sticky pagination at viewport bottom +- [x] Fix HTMX targetError: move #search-results wrapper inside fragment so it survives outerHTML swaps +- [x] Add range counter to results header (e.g. "1–15 sur 112 résultats") +- [x] Collapsible filter controls with
/ + active filter badge on mobile +- [x] Responsive paginator: hide first/last buttons on mobile, smaller touch targets +- [x] Compact range counter on mobile: "1–15 / 112" instead of full "1–15 sur 112 résultats" +- [x] Remove orientation display from search result card meta +- [x] Add bulk select/delete to tmp cleanup dialog (like admin/index.php pattern) +- [x] Move cleanup UI from modal to dedicated page (like add.php/edit.php) +- [x] Replace
with
/ in cleanup fragments (like contenus.php) +- [x] Standardise test group recipes: lint-php, lint-css, lint-js, test + add fix recipe + make lint pass clean +- [x] Remove Mot de passe column from acces.php, add key.svg icon button in Actions to copy password to clipboard, reorder actions (URL/password/edit/toggle/archive), switch URL icon to link-simple.svg +- [x] Fix CC2r: removed hidden seeds (were stale/overriding user input), render initial licence-choice from \$formData; Interne want_license unchecked clears CC2r+licence, switching from other modes preserves values; CC2r independent toggle; licence dropdown has no-licence placeholder +- [x] Dans le formulaire admin, le contact privé ne se sauvegarde pas. +- [x] Remove "Rendre le contact visible publiquement sur la fiche du TFE" from admin add/edit forms +- [x] Auto-show contact on public TFE page when contact_visible is filled (no checkbox needed) +- [x] Fix file size limits: align all layers (JS/FilepondHandler/ThesisFileHandler/validate-fragment) — video/audio 5 GB, default 500 MB, PDF 100 MB; fix JS fileValidateSizeFilter gate bug that rejected AV files >1 GB despite 8 GB per-ext cap +- [x] Fix: deleting contact groups in apropos form not persisted (no change event after DOM removal, autosave never triggered) +- [x] Improve h1, h2, h3 size difference in base.css heading scale +- [x] Rework TFE page desktop layout: two columns 35vw/1fr, left = author/title/meta/synopsis, right = files, media 100% width; columns scroll independently, content fits 100vw; no bottom padding on main; add &scrollbar=0 to PDF iframe URLs +- [x] 'Promoteur·ice ULB' remplacé par 'Promoteur·ice université' (changement juste l'étiquette) +- [x] PDF viewer: fix page navigation buttons (prev/next) — build toolbar after canvases exist & query canvases fresh in scroll handler +- [x] PDF viewer: render pages smaller by default (0.85× scale) +- [x] PDF viewer: add dark transparent violet background behind pages when expanded +- [x] pdf-viewer: expand to fill right column (not whole page), center pages, freeze column on expand +- [x] pdf-viewer: fix toolbar width (align-self:stretch), hide sibling file items when expanded +- [x] Fix createThesis SQL: extra `?` placeholder in VALUES (27 values for 26 columns) + add integration tests +- [x] CSV import/export: single source of truth (CSV_COLUMNS), add missing columns (duration, annexes, license_custom, contact_visible, objet, cc2r, exemplaires), fix XamxamInitFilePonds → window.n, generate import hint from headers diff --git a/app/public/admin/actions/export-csv.php b/app/public/admin/actions/export-csv.php index 11708c3..d2e50ca 100644 --- a/app/public/admin/actions/export-csv.php +++ b/app/public/admin/actions/export-csv.php @@ -27,7 +27,7 @@ echo "\xEF\xBB\xBF"; $out = fopen('php://output', 'w'); // Column headers -fputcsv($out, ExportController::CSV_HEADERS, ',', '"', ''); +fputcsv($out, ExportController::csvHeaders(), ',', '"', ''); // Data rows $rows = $controller->exportAllTheses(); diff --git a/app/public/admin/index.php b/app/public/admin/index.php index 4064de9..f22b432 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -37,14 +37,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { // Build colIdx[name] → position map; fall back to positional if header not found. // Matching uses prefix + variant logic so "contact.visible" matches "contact", // "promoteur·ice(s)" matches "promoteur", "Licence" matches "license", etc. + require_once APP_ROOT . '/src/Controllers/ExportController.php'; + $csvCols = ExportController::CSV_COLUMNS; + $csvHeaders = ExportController::csvHeaders(); + $colCount = count($csvCols); + $colIdx = null; $headerRowNum = 0; - $knownHeaders = [ - 'identifiant', 'titre', 'sous-titre', 'auteur', 'contact', - 'promoteur', 'lecteur', 'ulb', 'externe', 'format', 'année', 'ap', 'orientation', 'finalité', - 'mots-clés', 'synopsis', 'contexte', 'remarques', 'langue', - 'autorisation', 'licence', 'license', 'points', 'lien baiu', - ]; + // Build knownHeaders from CSV_COLUMNS import_key column (index 2) + $knownHeaders = array_unique(array_column($csvCols, 2)); + // Add licence/license cross-match aliases + $knownHeaders[] = 'license'; for ($scan = 0; $scan < 8; $scan++) { $hrow = fgetcsv($handle, 0, ',', '"', ''); if ($hrow === false) break; @@ -101,10 +104,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { } } + // Build positional fallback from CSV_COLUMNS (keyed by import_key → position) + $fallbackPositions = []; + foreach ($csvCols as $pos => $col) { + $key = $col[2]; + if (!isset($fallbackPositions[$key])) { + $fallbackPositions[$key] = $pos; + } + } + // Helper: get cell value by column name. - // When header was found: only use mapped column (returns '' if missing from header). - // When no header found: use positional fallback index. - $cell = function(array $row, string $name, int $fallbackPos) use ($colIdx): string { + // When header was found: use mapped column. + // When header was found but key is missing: use positional fallback. + // When no header found: use positional fallback. + $cell = function(array $row, string $name) use ($colIdx, $fallbackPositions): string { if ($colIdx !== null) { $pos = $colIdx[$name] ?? null; if ($pos === null) { @@ -112,13 +125,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { if ($name === 'license') $pos = $colIdx['licence'] ?? null; elseif ($name === 'licence') $pos = $colIdx['license'] ?? null; } + // Fall back to positional if header didn't have this key + if ($pos === null) { + $pos = $fallbackPositions[$name] ?? null; + } if ($pos === null) return ''; } else { - $pos = $fallbackPos; + $pos = $fallbackPositions[$name] ?? null; + if ($pos === null) return ''; } return isset($row[$pos]) ? trim((string)$row[$pos]) : ''; }; + // Helper: parse "Oui"/"Non"/1/0 strings to int + $parseBool = function(string $raw): int { + $raw = strtolower(trim($raw)); + return ($raw === 'oui' || $raw === '1' || $raw === 'yes' || $raw === 'true') ? 1 : 0; + }; + // Code → canonical name (legacy short-code CSV format) $orientationCodeMap = [ 'SC'=>'Sculpture','VI'=>'Vidéographie','CA'=>"Cinéma d'animation", @@ -232,39 +256,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { try { $importDb->beginTransaction(); - $identifier = $cell($row, 'identifiant', 0); - $title = $cell($row, 'titre', 1); - $subtitle = $cell($row, 'sous-titre', 2); - $authorsRaw = $cell($row, 'auteur', 3); - $contact = $cell($row, 'contact', 4); + $identifier = $cell($row, 'identifiant'); + $title = $cell($row, 'titre'); + $subtitle = $cell($row, 'sous-titre'); + $authorsRaw = $cell($row, 'auteur'); + $contact = $cell($row, 'contact'); // Normalise CSV artefacts: OUI/NON → empty (not a valid email) if ($contact !== '' && in_array(strtoupper(trim($contact)), ['NON', 'OUI'], true)) { $contact = ''; } - $supervisorsRaw = $cell($row, 'promoteur', 5); - $lecteursInternesRaw = $cell($row, 'lecteur', 6); // first "lecteur" col = interne - $lecteursExternesRaw = $cell($row, 'externe', 7); // contains "externe" - $promoteursUlbRaw = $cell($row, 'ulb', 8); // contains "ulb" - $formatsRaw = $cell($row, 'format', 9); - $yearRaw = $cell($row, 'année', 10); + $supervisorsRaw = $cell($row, 'promoteur'); + $lecteursInternesRaw = $cell($row, 'lecteur'); // first "lecteur" col = interne + $lecteursExternesRaw = $cell($row, 'externe'); // contains "externe" + $promoteursUlbRaw = $cell($row, 'ulb'); // contains "ulb" + $formatsRaw = $cell($row, 'format'); + $yearRaw = $cell($row, 'année'); $year = $yearRaw !== '' ? intval($yearRaw) : 0; // Fallback: derive year from identifier (e.g. "2024-003" → 2024) if ($year === 0 && $identifier !== '' && preg_match('/^(\d{4})-/', $identifier, $m)) { $year = (int)$m[1]; } - $apCode = $cell($row, 'ap', 11); - $orientationCode = $cell($row, 'orientation', 12); - $finalityName = $cell($row, 'finalité', 13); - $keywordsRaw = $cell($row, 'mots-clés', 14); - $synopsis = $cell($row, 'synopsis', 15); - $context = $cell($row, 'contexte', 16); - $remarks = $cell($row, 'remarques', 17); - $languageRaw = $cell($row, 'langue', 18); - $access = $cell($row, 'autorisation', 19); - $license = $cell($row, 'license', 20); - $juryPointsRaw = $cell($row, 'points', 21); + $apCode = $cell($row, 'ap'); + $orientationCode = $cell($row, 'orientation'); + $finalityName = $cell($row, 'finalité'); + $keywordsRaw = $cell($row, 'mots-clés'); + $synopsis = $cell($row, 'synopsis'); + $context = $cell($row, 'contexte'); + $remarks = $cell($row, 'remarques'); + $languageRaw = $cell($row, 'langue'); + $access = $cell($row, 'autorisation'); + $license = $cell($row, 'licence'); + $juryPointsRaw = $cell($row, 'points'); $juryPoints = $juryPointsRaw !== '' ? floatval($juryPointsRaw) : null; - $baiuLink = $cell($row, 'lien baiu', 22); + $baiuLink = $cell($row, 'lien baiu'); + $cc2rRaw = $cell($row, 'cc2r'); + $cc2r = $parseBool($cc2rRaw); + $exempBaiuRaw = $cell($row, 'exemplaire'); // first "exemplaire" col = BAIU + $exempBaiu = $parseBool($exempBaiuRaw); + $exempErgRaw = $cell($row, 'exemplaire-erg'); // second "exemplaire", key won't match header → positional + $exempErg = $parseBool($exempErgRaw); + $durationPagesRaw = $cell($row, 'pages'); + $durationPages = $durationPagesRaw !== '' ? (int)$durationPagesRaw : null; + $durationMinutesRaw = $cell($row, 'durée'); + $durationMinutes = $durationMinutesRaw !== '' ? (int)$durationMinutesRaw : null; + $hasAnnexesRaw = $cell($row, 'annexes'); + $hasAnnexes = $parseBool($hasAnnexesRaw); + $licenseCustom = $cell($row, 'licence-perso'); // second "licence", key won't match header → positional + $contactVisible = $cell($row, 'contact-visible'); // second "contact", key won't match header → positional + $objetRaw = $cell($row, 'objet'); + $objet = in_array($objetRaw, ['tfe', 'thèse', 'frart'], true) ? $objetRaw : 'tfe'; if ($title === '' || $year === 0) { $missing = []; @@ -303,14 +343,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { } } + // Resolve license name → license_id + $licenseId = null; + if (!empty($license)) { + $ls = $importPdo->prepare("SELECT id FROM license_types WHERE LOWER(name) = LOWER(?)"); + $ls->execute([trim($license)]); + $lr = $ls->fetch(); + $licenseId = $lr ? (int)$lr['id'] : null; + } + // If a custom license string is provided and no DB license matched, store as custom + if ($licenseId === null && !empty($license) && empty($licenseCustom)) { + $licenseCustom = $license; + } + $s = $importPdo->prepare(" INSERT INTO theses ( identifier, title, subtitle, year, orientation_id, ap_program_id, finality_id, synopsis, context_note, remarks, jury_points, baiu_link, - access_type_id, is_published, submitted_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,0,CURRENT_TIMESTAMP) + access_type_id, license_id, license_custom, + cc2r, exemplaire_baiu, exemplaire_erg, + objet, contact_visible, + duration_pages, duration_minutes, has_annexes, + is_published, status, submitted_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,'active',CURRENT_TIMESTAMP) "); $s->execute([ !empty($identifier) ? $identifier : null, $title, @@ -322,6 +379,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { $juryPoints, !empty($baiuLink) ? $baiuLink : null, $accessTypeId, + $licenseId, + !empty($licenseCustom) ? $licenseCustom : null, + $cc2r, + $exempBaiu, + $exempErg, + $objet, + !empty($contactVisible) ? $contactVisible : null, + $durationPages, + $durationMinutes, + $hasAnnexes, ]); $thesisId = $importPdo->lastInsertId(); diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index c5b5c78..de158c3 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -2508,3 +2508,27 @@ th.admin-ap-col { padding: var(--space-3xs) 0; border: 0; } + +/* ── CSV import file input (admin list page) ──────────────────────────── */ + +.admin-file-input { + display: flex; + flex-direction: column; + gap: var(--space-3xs); +} + +/* FilePond drop-zone border (replaces the raw input border from form-base.css) */ +.admin-file-input .filepond--root { + border: 1px dashed var(--border-primary, #ccc); + border-radius: var(--radius, 8px); + background: var(--bg-elevated, #fafafa); +} + +.admin-file-input .filepond--root:hover { + border-color: var(--accent-primary, #5b2d8e); +} + +.admin-file-hint { + display: block; + margin-top: var(--space-2xs); +} diff --git a/app/public/assets/js/app/admin-entry.js b/app/public/assets/js/app/admin-entry.js index 6519409..ad4ef69 100644 --- a/app/public/assets/js/app/admin-entry.js +++ b/app/public/assets/js/app/admin-entry.js @@ -15,6 +15,7 @@ import "./smtp-error-focus.js"; import "./htmx-global-setup.js"; // Admin features +import "./file-upload-filepond.js"; import "./admin-index-bulk.js"; import "./admin-cleanup-bulk.js"; import "./admin-tags.js"; diff --git a/app/src/Controllers/ExportController.php b/app/src/Controllers/ExportController.php index e83b7be..8762d29 100644 --- a/app/src/Controllers/ExportController.php +++ b/app/src/Controllers/ExportController.php @@ -250,6 +250,59 @@ class ExportController /** * Column headers matching the import format. */ + /** + * Single source of truth for CSV column definitions. + * Each entry: [header_label, field_key, import_search_key] + * + * field_key = key in the SELECT row and export data array + * import_key = lowercase token used to match header rows during import + */ + public const CSV_COLUMNS = [ + ['Identifiant', 'identifier', 'identifiant'], + ['Titre', 'title', 'titre'], + ['Sous-titre', 'subtitle', 'sous-titre'], + ['Auteur·ice(s)', 'authors', 'auteur'], + ['Contact', 'contact', 'contact'], + ['Promoteur·ice(s) interne', 'promoteurs_internes', 'promoteur'], + ['Lecteur·ice(s) interne', 'lecteurs_internes', 'lecteur'], + ['Lecteur·ice(s) externe', 'lecteurs_externes', 'externe'], + ['Promoteur·ice(s) université', 'promoteurs_ulb', 'ulb'], + ['Format(s)', 'formats', 'format'], + ['Année', 'year', 'année'], + ['AP', 'ap_program', 'ap'], + ['Orientation', 'orientation', 'orientation'], + ['Finalité', 'finality', 'finalité'], + ['Mots-clés', 'keywords', 'mots-clés'], + ['Synopsis', 'synopsis', 'synopsis'], + ['Contexte', 'context', 'contexte'], + ['Remarques', 'remarks', 'remarques'], + ['Langue', 'languages', 'langue'], + ['Autorisation', 'access_type', 'autorisation'], + ['Licence', 'license', 'licence'], + ['Points sur 20', 'jury_points', 'points'], + ['Lien BAIU', 'baiu_link', 'lien baiu'], + ['CC2r', 'cc2r', 'cc2r'], + ['Exemplaire BAIU', 'exemplaire_baiu', 'exemplaire'], + ['Exemplaire ERG', 'exemplaire_erg', 'exemplaire-erg'], + ['Pages', 'duration_pages', 'pages'], + ['Durée (minutes)', 'duration_minutes', 'durée'], + ['Annexes', 'has_annexes', 'annexes'], + ['Licence personnalisée', 'license_custom', 'licence-perso'], + ['Contact visible', 'contact_visible', 'contact-visible'], + ['Objet', 'objet', 'objet'], + ]; + + /** + * Derived from CSV_COLUMNS for backward compatibility. + */ + public static function csvHeaders(): array + { + return array_column(self::CSV_COLUMNS, 0); + } + + /** + * @deprecated Use csvHeaders() instead. + */ public const CSV_HEADERS = [ 'Identifiant', 'Titre', @@ -277,6 +330,12 @@ class ExportController 'CC2r', 'Exemplaire BAIU', 'Exemplaire ERG', + 'Pages', + 'Durée (minutes)', + 'Annexes', + 'Licence personnalisée', + 'Contact visible', + 'Objet', ]; /** @@ -396,6 +455,12 @@ class ExportController !empty($t['cc2r']) ? 'Oui' : 'Non', !empty($t['exemplaire_baiu']) ? 'Oui' : 'Non', !empty($t['exemplaire_erg']) ? 'Oui' : 'Non', + isset($t['duration_pages']) && $t['duration_pages'] !== null ? (string) $t['duration_pages'] : '', + isset($t['duration_minutes']) && $t['duration_minutes'] !== null ? (string) $t['duration_minutes'] : '', + !empty($t['has_annexes']) ? 'Oui' : 'Non', + $t['license_custom'] ?? '', + $t['contact_visible'] ?? '', + $t['objet'] ?? '', ]; } diff --git a/app/src/Database.php b/app/src/Database.php index 54e6b5b..9656e29 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2266,7 +2266,7 @@ class Database duration_pages, duration_minutes, has_annexes, submitted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, "draft", ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, "draft", ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) '); $validObjet = ['tfe', 'thèse', 'frart']; @@ -2696,7 +2696,13 @@ class Database t.baiu_link, t.exemplaire_baiu, t.exemplaire_erg, - t.cc2r + t.cc2r, + t.license_custom, + t.contact_visible, + t.duration_pages, + t.duration_minutes, + t.has_annexes, + t.objet FROM theses t LEFT JOIN orientations o ON t.orientation_id = o.id LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id diff --git a/app/templates/admin/partials/dialogs/import.php b/app/templates/admin/partials/dialogs/import.php index 357b5cf..39caf75 100644 --- a/app/templates/admin/partials/dialogs/import.php +++ b/app/templates/admin/partials/dialogs/import.php @@ -1,3 +1,9 @@ +

Importer une liste de TFE

@@ -51,7 +57,7 @@ data-queue-type="csv_import" required> - Colonnes : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU
+ Colonnes :
Quatre premières lignes ignorées — Séparateur : virgule — UTF-8
diff --git a/scripts/build-css.mjs b/scripts/build-css.mjs index 8385831..45f8498 100644 --- a/scripts/build-css.mjs +++ b/scripts/build-css.mjs @@ -102,8 +102,15 @@ async function main() { // 1. Base bundle: resolve style.css @import chain total += await bundleCss("style.css", "base.min.css"); - // 2. Admin bundle: just minify admin.css (standalone) - total += await bundleCss("admin.css", "admin.min.css"); + // 2. Admin bundle: admin.css + filepond vendor CSS (for CSV import dialog) + total += await concatBundle( + [ + "admin.css", + "filepond.min.css", + "filepond-plugin-image-preview.min.css", + ], + "admin.min.css" + ); // 3. Form bundle: concat + minify form-base.css + form-admin.css + filepond vendor CSS total += await concatBundle( diff --git a/tests/phpunit/DatabaseExtendedTest.php b/tests/phpunit/DatabaseExtendedTest.php index 2f10b0e..5360527 100644 --- a/tests/phpunit/DatabaseExtendedTest.php +++ b/tests/phpunit/DatabaseExtendedTest.php @@ -345,4 +345,103 @@ class DatabaseExtendedTest extends TestCase $deleted = $pdo->query("SELECT deleted_at FROM tags WHERE id = $tagB")->fetchColumn(); $this->assertNotNull($deleted); } + + // ── createThesis ──────────────────────────────────────────────────────── + + public function testCreateThesisMinimalFields(): void + { + $id = $this->db->createThesis([ + 'title' => 'Minimal Thesis', + 'year' => 2025, + 'synopsis' => 'A minimal synopsis.', + ]); + + $this->assertGreaterThan(0, $id); + + $row = TestDatabase::getPDO() + ->query("SELECT * FROM theses WHERE id = $id") + ->fetch(PDO::FETCH_ASSOC); + + $this->assertSame('Minimal Thesis', $row['title']); + $this->assertSame(2025, (int)$row['year']); + $this->assertSame('tfe', $row['objet']); + $this->assertSame('draft', $row['status']); + $this->assertSame(0, (int)$row['is_published']); + $this->assertSame(0, (int)$row['exemplaire_baiu']); + $this->assertSame(0, (int)$row['exemplaire_erg']); + $this->assertSame(0, (int)$row['cc2r']); + $this->assertSame(0, (int)$row['has_annexes']); + $this->assertStringStartsWith('2025-', $row['identifier']); + $this->assertNotNull($row['submitted_at']); + } + + public function testCreateThesisAllOptionalFields(): void + { + $id = $this->db->createThesis([ + 'title' => 'Full Thesis', + 'subtitle' => 'With a subtitle', + 'year' => 2024, + 'synopsis' => 'Full synopsis.', + 'context_note' => 'Some context.', + 'contact_visible' => 'Visible info', + 'baiu_link' => 'https://example.com/thesis', + 'license_id' => null, + 'license_custom' => 'CC BY 4.0', + 'access_type_id' => 1, + 'objet' => 'tfe', + 'remarks' => 'No remarks.', + 'jury_points' => '16.5', + 'exemplaire_baiu' => '1', + 'exemplaire_erg' => '1', + 'cc2r' => '1', + 'duration_pages' => '42', + 'duration_minutes' => '30', + 'has_annexes' => '1', + ]); + + $this->assertGreaterThan(0, $id); + + $row = TestDatabase::getPDO() + ->query("SELECT * FROM theses WHERE id = $id") + ->fetch(PDO::FETCH_ASSOC); + + $this->assertSame('Full Thesis', $row['title']); + $this->assertSame('With a subtitle', $row['subtitle']); + $this->assertSame(2024, (int)$row['year']); + $this->assertSame('Full synopsis.', $row['synopsis']); + $this->assertSame('Some context.', $row['context_note']); + $this->assertSame('Visible info', $row['contact_visible']); + $this->assertSame('https://example.com/thesis', $row['baiu_link']); + $this->assertSame('CC BY 4.0', $row['license_custom']); + $this->assertSame(1, (int)$row['exemplaire_baiu']); + $this->assertSame(1, (int)$row['exemplaire_erg']); + $this->assertSame(1, (int)$row['cc2r']); + $this->assertSame(42, (int)$row['duration_pages']); + $this->assertSame(30, (int)$row['duration_minutes']); + $this->assertSame(1, (int)$row['has_annexes']); + $this->assertSame('No remarks.', $row['remarks']); + $this->assertEquals(16.5, (float)$row['jury_points']); + } + + public function testCreateThesisGeneratesUniqueIdentifiers(): void + { + $id1 = $this->db->createThesis([ + 'title' => 'First', + 'year' => 2025, + 'synopsis' => 'One.', + ]); + $id2 = $this->db->createThesis([ + 'title' => 'Second', + 'year' => 2025, + 'synopsis' => 'Two.', + ]); + + $pdo = TestDatabase::getPDO(); + $ident1 = $pdo->query("SELECT identifier FROM theses WHERE id = $id1")->fetchColumn(); + $ident2 = $pdo->query("SELECT identifier FROM theses WHERE id = $id2")->fetchColumn(); + + $this->assertNotSame($ident1, $ident2); + $this->assertSame('2025-001', $ident1); + $this->assertSame('2025-002', $ident2); + } }