diff --git a/.phpunit.result.cache b/.phpunit.result.cache index fccf374..11ec993 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.029,"CryptoTest::testEncryptDecryptWithUnicode":0.002,"CryptoTest::testEncryptDecryptMultiline":0.002,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0.002,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0.002,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.002,"CryptoTest::testIsEncryptedRejectsPlaintext":0.003,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0.004,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0.002,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0.002,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0.002,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0.002,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0.002,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0.002,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.019,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.005,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0.002,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.002,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0.002,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0.002,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0.002,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.004,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0.004,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0.002,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0.002,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0.002,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0.001,"EmailObfuscatorTest::testMultipleEmailsInOneString":0.002,"EmailObfuscatorTest::testEmailWithPlusSign":0.002,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.069,"StudentEmailTest::testBuildHtmlContainsKeyFields":0.003,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0.005,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0.005,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0.007,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0.005,"StudentEmailTest::testBuildHtmlContainsLabelFields":0.004,"SystemControllerHelpersTest::testHumanBytesZero":0.175,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneMB":0.002,"SystemControllerHelpersTest::testHumanBytesOneGB":0.003,"SystemControllerHelpersTest::testHumanBytes1523MB":0.003,"SystemControllerHelpersTest::testHumanBytes2500GB":0.003,"SystemControllerHelpersTest::testDiskColorBelowWarning":0.003,"SystemControllerHelpersTest::testDiskColorWarning":0.003,"SystemControllerHelpersTest::testDiskColorCritical":0.002,"SystemControllerHelpersTest::testLogLineClassCrit":0.002,"SystemControllerHelpersTest::testLogLineClassError":0.002,"SystemControllerHelpersTest::testLogLineClassWarn":0.004,"SystemControllerHelpersTest::testLogLineClassNotice":0.004,"SystemControllerHelpersTest::testLogLineClassHttp500":0.003,"SystemControllerHelpersTest::testLogLineClassHttp300":0.004,"SystemControllerHelpersTest::testLogLineClassDefault":0.003,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0.002,"SystemControllerHelpersTest::testStatusLabelInactive":0.003,"SystemControllerHelpersTest::testStatusLabelFailed":0.004,"SystemControllerHelpersTest::testStatusLabelWarn":0.002,"SystemControllerHelpersTest::testStatusLabelUnknown":0.002,"SystemControllerHelpersTest::testStatusClassOk":0.002,"SystemControllerHelpersTest::testStatusClassWarn":0.004,"SystemControllerHelpersTest::testStatusClassError":0.002,"SystemControllerHelpersTest::testStatusClassUnknown":0.002,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.002,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0.003,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0.003,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0.002,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0.003,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0.003,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0.003,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0.002,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.005,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0.004,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0.003,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0.003,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.003,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.003,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.004,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.005,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.003,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0.002,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.004,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.002,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0.004,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.003,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0.002,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.003,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.049,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.003,"DatabaseExtendedTest::testRenameTagUpdatesName":0.003,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.003,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.036,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.003,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0.002,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0.002,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.003,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0.002,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.351,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.29,"ShareLinkExtendedTest::testFindBySlugHit":0.292,"ShareLinkExtendedTest::testFindBySlugMiss":0.002,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.291,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0.002,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.291,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.29,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.292,"ShareLinkExtendedTest::testCreateWithLockedYear":0.294,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.292,"ShareLinkExtendedTest::testUpdateLockedYear":0.293,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.294,"ShareLinkExtendedTest::testIncrementUsage":0.296,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.298,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.329,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0.002,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.002,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0.002,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":1.23,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0.004,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.157,"AutofocusFieldForErrorTest::testEditAutofocusYear":0.002,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0.002,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0.002,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0.004,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.005,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0.002,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0.004,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0.003,"ThesisCreateValidationTest::testYearZeroRejected":0.003,"ThesisCreateValidationTest::testYearBefore2000Rejected":0.003,"ThesisCreateValidationTest::testFarFutureYearRejected":0.003,"ThesisCreateValidationTest::testCurrentYearAccepted":0.004,"ThesisCreateValidationTest::testMalformedUrlRejected":0.004,"ThesisCreateValidationTest::testValidUrlAccepted":0.004,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0.004,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0.006,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0.004,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0.006,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0.006,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0.006,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0.009,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0.011,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0.006,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0.008,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.008,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.01,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0.011,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0.014,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0.007,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0.006,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0.004,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0.002,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0.002,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.007,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0.005,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.004,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.032,"ErrorHandlerTest::testFkApPrograms":0.002,"ErrorHandlerTest::testFkFinalityTypes":0.002,"ErrorHandlerTest::testFkThesisLanguages":0.002,"ErrorHandlerTest::testFkThesisFormats":0.002,"ErrorHandlerTest::testFkThesisTags":0.003,"ErrorHandlerTest::testFkThesisSupervisors":0.002,"ErrorHandlerTest::testFkAccessTypes":0.002,"ErrorHandlerTest::testFkLicenseTypes":0.002,"ErrorHandlerTest::testFkAuthors":0.002,"ErrorHandlerTest::testFkQuotedTableName":0.002,"ErrorHandlerTest::testFkQuotedLanguages":0.003,"ErrorHandlerTest::testFkQuotedFormatTypes":0.002,"ErrorHandlerTest::testFkReferencesTags":0.002,"ErrorHandlerTest::testFkReferencesOrientations":0.002,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0.002,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0.002,"ErrorHandlerTest::testUniqueConstraint":0.002,"ErrorHandlerTest::testNotNullConstraint":0.002,"ErrorHandlerTest::testGenericPdoError":0.002,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0.006,"ErrorHandlerTest::testValidationExceptionPassesThrough":0.002,"ErrorHandlerTest::testGenericExceptionPassesThrough":0.002,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0.001,"ErrorHandlerTest::testLogWithContext":0.003,"ErrorHandlerTest::testLogWithNullValues":0.002,"ErrorHandlerTest::testLogWithEmptyExtra":0.003,"ErrorHandlerTest::testFkQuotedColumnNames":0.003,"ErrorHandlerTest::testFkUpdateStatement":0.002,"ErrorHandlerTest::testFkWithReferencesAndInsert":0.002,"PureLogicTest::testSplitJuryByRoleAllRoles":0.046,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0.002,"PureLogicTest::testSplitJuryByRoleEmptyJury":0.002,"PureLogicTest::testCollectCaptionPathsVttByMime":0.003,"PureLogicTest::testCollectCaptionPathsVttByExtension":0.002,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0.002,"PureLogicTest::testDetectFileTypeByMime":0.002,"PureLogicTest::testDetectFileTypeByExtensionFallback":0.003,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.059,"SearchControllerTest::testCoverMapContainsKnownThesis":0.005}} \ 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.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 diff --git a/TODO.md b/TODO.md index e4f1206..e8c2725 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,6 @@ -- [x] Fix markdown cheatsheet modal 500 error — missing `require_once bootstrap.php` in `app/public/admin/markdown-cheatsheet-fragment.php` -- [x] Add `padding-bottom` to `.md-cheatsheet-dialog` so last table row isn't flush against dialog edge -- [x] Add independent scrollbar for TOC in charte/licence/apropos + admin pages (max-height + overflow-y: auto on .toc desktop) +- [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 diff --git a/app/migrations/applied/041_combined_duration.sql b/app/migrations/applied/041_combined_duration.sql new file mode 100644 index 0000000..1a0e3b2 --- /dev/null +++ b/app/migrations/applied/041_combined_duration.sql @@ -0,0 +1,88 @@ +-- 041_combined_duration.sql +-- Replace single duration_value/duration_unit with separate pages + minutes fields. +-- Allows a TFE to have both a page count and a time duration simultaneously. + +-- Add new columns to theses table +ALTER TABLE theses ADD COLUMN duration_pages INTEGER; +ALTER TABLE theses ADD COLUMN duration_minutes INTEGER; + +-- Add has_annexes flag for optional annexes checkbox +ALTER TABLE theses ADD COLUMN has_annexes INTEGER NOT NULL DEFAULT 0; + +-- Migrate existing data: +-- duration_unit='pages' → duration_pages = duration_value +-- duration_unit='durée' → duration_minutes = ROUND(duration_value * 60) +UPDATE theses SET duration_pages = CAST(duration_value AS INTEGER) WHERE duration_unit = 'pages' AND duration_value IS NOT NULL; +UPDATE theses SET duration_minutes = CAST(ROUND(duration_value * 60) AS INTEGER) WHERE duration_unit = 'durée' AND duration_value IS NOT NULL; + +-- Drop and recreate views to include the new columns +DROP VIEW IF EXISTS v_theses_full; +DROP VIEW IF EXISTS v_theses_public; + +CREATE VIEW IF NOT EXISTS v_theses_full AS +SELECT + t.id, + t.identifier, + t.title, + t.subtitle, + t.year, + t.is_doctoral, + t.objet, + o.name as orientation, + ap.name as ap_program, + ft.name as finality_type, + t.synopsis, + t.context_note, + t.duration_pages, + t.duration_minutes, + t.has_annexes, + at.name as access_type, + lt.name as license_type, + t.license_id, + t.license_custom, + t.access_type_id, + t.jury_points, + t.submitted_at, + t.defense_date, + t.published_at, + t.is_published, + t.baiu_link, + t.exemplaire_baiu, + t.exemplaire_erg, + t.cc2r, + t.remarks, + t.jury_note_added, + t.contact_visible, + GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) as authors, + GROUP_CONCAT(DISTINCT s.name) as supervisors, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes, + GROUP_CONCAT(DISTINCT l.name) as languages, + GROUP_CONCAT(DISTINCT fmt.name) as formats, + GROUP_CONCAT(DISTINCT tg.name) as keywords, + (SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_email, + (SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_show_contact +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 +LEFT JOIN finality_types ft ON t.finality_id = ft.id +LEFT JOIN access_types at ON t.access_type_id = at.id +LEFT JOIN license_types lt ON t.license_id = lt.id +LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id +LEFT JOIN authors a ON ta.author_id = a.id +LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id +LEFT JOIN supervisors s ON ts.supervisor_id = s.id +LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id +LEFT JOIN languages l ON tl.language_id = l.id +LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id +LEFT JOIN format_types fmt ON tf.format_id = fmt.id +LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id +LEFT JOIN tags tg ON tt.tag_id = tg.id +GROUP BY t.id; + +CREATE VIEW IF NOT EXISTS v_theses_public AS +SELECT * FROM v_theses_full +WHERE is_published = 1; diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php index f849573..4e953fb 100644 --- a/app/public/admin/edit.php +++ b/app/public/admin/edit.php @@ -45,6 +45,9 @@ $formData = array_merge($formData, [ 'lien' => $thesis['baiu_link'] ?? '', 'contact_public' => $contactPublic ?? false, 'contact_interne' => $contactInterne ?? '', + 'duration_pages' => $currentRaw['duration_pages'] ?? null, + 'duration_minutes' => $currentRaw['duration_minutes'] ?? null, + 'has_annexes' => $currentRaw['has_annexes'] ?? false, ]); // Build jury arrays @@ -144,8 +147,7 @@ extract(FormBootstrap::adminFormVariables( 'currentFiles' => $currentFiles ?? [], 'currentContextNote' => $currentContextNote ?? null, 'currentContactVisible' => $currentContactVisible ?? null, - 'currentDurationValue' => $currentDurationValue ?? null, - 'currentDurationUnit' => $currentDurationUnit ?? 'pages', + 'contactInterne' => $contactInterne ?? null, 'contactPublic' => $contactPublic ?? false, 'showCoverPreview' => true, @@ -161,8 +163,9 @@ $formData['license_custom'] = $currentRaw['license_custom'] ?? ''; $formData['cc2r'] = $currentRaw['cc2r'] ?? false; // Duration variables for the form template -$durationValue = $currentDurationValue ?? null; -$durationUnit = $currentDurationUnit ?? 'pages'; +$durationPages = $currentRaw['duration_pages'] ?? null; +$durationMins = $currentRaw['duration_minutes'] ?? null; +$hasAnnexes = $currentRaw['has_annexes'] ?? false; // Asset arrays and page chrome $isAdmin = true; diff --git a/app/public/admin/fragments/fichiers.php b/app/public/admin/fragments/fichiers.php index b5c6a63..a72cdb3 100644 --- a/app/public/admin/fragments/fichiers.php +++ b/app/public/admin/fragments/fichiers.php @@ -61,6 +61,10 @@ if ($thesisId) { $existingFilesJsonForTfe = $buildQueueFilesJson($currentFiles, 'tfe'); $existingFilesJsonForAnnexe = $buildQueueFilesJson($currentFiles, 'annexe'); + // Load has_annexes flag for the checkbox + $rawRow = $db->getThesisRawFields($thesisId); + $_POST['has_annexes'] = !empty($rawRow['has_annexes']) ? '1' : null; + $_POST['edit_mode'] = '1'; } diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index b72387d..ff4078c 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -2160,18 +2160,36 @@ th.admin-ap-col { 100% { transform: scaleX(0); transform-origin: right; } } -/* ── Sidebar TOC (matches public .page-content alignment pattern) ────────── */ +/* ── Sidebar TOC (matches public .page-content grid layout) ────────────── */ .admin-main--toc { - display: flex; + display: grid; + grid-template-columns: 180px 1fr; gap: var(--space-2xl); align-items: start; padding: var(--space-xl) var(--space-m) var(--space-2xl); } -.admin-main--toc > article { - flex: 1; - min-width: 0; +/* Desktop: article in second column */ +@media (min-width: 768px) { + .admin-main--toc > article { + grid-column: 2; + min-width: 0; + max-width: 100%; + } +} + +/* Mobile: single column */ +@media (max-width: 767px) { + .admin-main--toc { + grid-template-columns: 1fr; + gap: var(--space-l); + padding: var(--space-m) var(--space-s) var(--space-xl); + } + + .admin-main--toc > article { + grid-column: 1; + } } /* Align first child of article with TOC heading (same as public) */ @@ -2188,11 +2206,6 @@ th.admin-ap-col { } /* Admin TOC: same
as public pages, positioned sticky */ -#admin-toc { - width: 180px; - flex-shrink: 0; -} - #admin-toc .toc-list a { font-size: var(--step--1); } diff --git a/app/public/assets/css/form-base.css b/app/public/assets/css/form-base.css index 4517206..c4f7c08 100644 --- a/app/public/assets/css/form-base.css +++ b/app/public/assets/css/form-base.css @@ -352,7 +352,11 @@ border-radius: var(--radius); } -.duration-time-inputs { +.duration-field { + margin-bottom: var(--space-xs); +} + +.duration-time-field { display: flex; align-items: flex-end; gap: var(--space-xs); @@ -364,6 +368,11 @@ gap: var(--space-3xs); } +.duration-time-fields input[type="number"] { + width: 6ch; + min-width: 6ch; +} + .duration-time-fields span { font-size: var(--step--1); color: var(--text-secondary); diff --git a/app/public/assets/js/app/admin-toc.js b/app/public/assets/js/app/admin-toc.js index be4bafd..603aaa7 100644 --- a/app/public/assets/js/app/admin-toc.js +++ b/app/public/assets/js/app/admin-toc.js @@ -45,21 +45,63 @@ items.push({ section: sec, link: a }); }); + // Generate a spread of thresholds for smooth IntersectionObserver firing. + function buildThresholds() { + var t = []; + for (var i = 0; i <= 20; i++) t.push(i / 20); + return t; + } + var observer = new IntersectionObserver( - (entries) => { - var best = null, - bestRatio = 0; - entries.forEach((e) => { - if (e.intersectionRatio > bestRatio) { - bestRatio = e.intersectionRatio; - best = e.target; + () => { + // Active section = whose top is closest to the midpoint of the + // viewport from above. At page bottom, the last section wins + // even if its heading can't reach 50%. + var line = window.innerHeight * 0.5; + var best = null; + var bestDist = Infinity; + + // Pass 1: sections whose top is above/at the line + items.forEach((item) => { + var top = item.section.getBoundingClientRect().top; + if (top <= line && (line - top) < bestDist) { + bestDist = line - top; + best = item; } }); + + // Pass 2: no section above the line — pick closest from below + if (!best) { + items.forEach((item) => { + var top = item.section.getBoundingClientRect().top; + var dist = top - line; + if (dist >= 0 && dist < bestDist) { + bestDist = dist; + best = item; + } + }); + } + + // Pass 3: at page bottom, if the last section is visible but + // its heading never crossed 50%, promote it anyway. + if (best) { + var lastItem = items[items.length - 1]; + if (lastItem !== best) { + var lastTop = lastItem.section.getBoundingClientRect().top; + var atBottom = (window.innerHeight + window.scrollY + 5) + >= document.documentElement.scrollHeight; + if (atBottom && lastTop > line && lastTop < window.innerHeight) { + best = lastItem; + } + } + } + + if (!best && items.length > 0) best = items[0]; items.forEach((item) => { - item.link.classList.toggle('toc-active', item.section === best); + item.link.classList.toggle('toc-active', item === best); }); }, - { rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] } + { threshold: buildThresholds() } ); items.forEach((item) => { diff --git a/app/public/assets/js/app/form-duration-toggle.js b/app/public/assets/js/app/form-duration-toggle.js index 9591337..3fe8bc6 100644 --- a/app/public/assets/js/app/form-duration-toggle.js +++ b/app/public/assets/js/app/form-duration-toggle.js @@ -1,51 +1,26 @@ /** - * form-duration-toggle.js — Duration unit toggle on TFE form. + * form-duration-toggle.js — Combined duration helper for TFE form. * - * Switches between integer input (pages/mo) and h/m/s time inputs (durée). - * Updates hidden #duration_value on change. + * Computes duration_minutes from h + m fields on form submit so the server + * receives a single integer (total minutes). */ (() => { - var unit = document.getElementById('duration_unit'); - var hidden = document.getElementById('duration_value'); - var intWrap = document.getElementById('duration-value-integer'); - var intInput = document.getElementById('duration_value_int'); - var intLabel = document.getElementById('duration-value-label'); - var timeWrap = document.getElementById('duration-value-time'); - var hInput = document.getElementById('duration_h'); - var mInput = document.getElementById('duration_m'); - var sInput = document.getElementById('duration_s'); + var form = document.querySelector('form.admin-form'); + if (!form) return; - var LABELS = { pages: 'Nombre :', mo: 'Taille :', durée: 'Durée :' }; - if (!unit || !hidden) return; + form.addEventListener('submit', function() { + var h = parseInt(document.getElementById('duration_h')?.value, 10) || 0; + var m = parseInt(document.getElementById('duration_m')?.value, 10) || 0; + var total = h * 60 + m; - function updateHidden() { - if (unit.value === 'durée') { - var h = parseInt(hInput.value, 10) || 0; - var m = parseInt(mInput.value, 10) || 0; - var s = parseInt(sInput.value, 10) || 0; - var total = h + m / 60 + s / 3600; - hidden.value = total > 0 ? total.toFixed(6) : ''; - } else { - hidden.value = intInput.value; + var hidden = document.getElementById('duration_minutes_hidden'); + if (!hidden) { + hidden = document.createElement('input'); + hidden.type = 'hidden'; + hidden.name = 'duration_minutes'; + hidden.id = 'duration_minutes_hidden'; + form.appendChild(hidden); } - } - - function toggleFields() { - if (unit.value === 'durée') { - intWrap.style.display = 'none'; - timeWrap.style.display = ''; - } else { - timeWrap.style.display = 'none'; - intWrap.style.display = ''; - if (intLabel) intLabel.textContent = LABELS[unit.value] || 'Valeur :'; - } - updateHidden(); - } - - unit.addEventListener('change', toggleFields); - if (intInput) intInput.addEventListener('input', updateHidden); - if (hInput) hInput.addEventListener('input', updateHidden); - if (mInput) mInput.addEventListener('input', updateHidden); - if (sInput) sInput.addEventListener('input', updateHidden); - toggleFields(); + hidden.value = total > 0 ? String(total) : ''; + }); })(); diff --git a/app/public/partage/recapitulatif.php b/app/public/partage/recapitulatif.php index ce10165..0632d6c 100644 --- a/app/public/partage/recapitulatif.php +++ b/app/public/partage/recapitulatif.php @@ -137,6 +137,22 @@ $pageTitle = 'Merci — TFE enregistré';
+ +
Durée
+
+ +
Promoteur·ice(s) interne
diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index 0816213..2a8e9a8 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -162,8 +162,9 @@ class ThesisCreateController 'exemplaire_baiu' => $data['exemplaireBaiu'], 'exemplaire_erg' => $data['exemplaireErg'], 'cc2r' => $data['cc2r'], - 'duration_value' => $data['durationValue'], - 'duration_unit' => $data['durationUnit'], + 'duration_pages' => $data['durationPages'], + 'duration_minutes' => $data['durationMins'], + 'has_annexes' => $data['hasAnnexes'] ? 1 : 0, ]); $identifier = $this->db->getThesisIdentifier($thesisId); @@ -555,20 +556,22 @@ class ThesisCreateController $exemplaireErg = !empty($post['exemplaire_erg']); $cc2r = !empty($post['cc2r']); - // Duration: numeric value + unit (optional, admin-validated) - $validDurationUnits = ['pages', 'mo', 'durée']; - $durationValue = $post['duration_value'] ?? null; - $durationUnit = $post['duration_unit'] ?? 'pages'; - if ($durationValue !== null && $durationValue !== '') { - $durationValue = filter_var($durationValue, FILTER_VALIDATE_FLOAT); - if ($durationValue === false || $durationValue <= 0) { - $durationValue = null; // ignore invalid + // Duration: separate pages (int) and time (h/m → total minutes) + $durationPages = null; + $durationPagesRaw = $post['duration_pages'] ?? null; + if ($durationPagesRaw !== null && $durationPagesRaw !== '') { + $durationPages = filter_var($durationPagesRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]); + if ($durationPages === false || $durationPages <= 0) { + $durationPages = null; } - } else { - $durationValue = null; } - if (!in_array($durationUnit, $validDurationUnits, true)) { - $durationUnit = 'pages'; + $durationMins = null; + $durationMinsRaw = $post['duration_minutes'] ?? null; + if ($durationMinsRaw !== null && $durationMinsRaw !== '') { + $durationMins = filter_var($durationMinsRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]); + if ($durationMins === false || $durationMins <= 0) { + $durationMins = null; + } } // Annexes are optional — no validation required @@ -601,8 +604,9 @@ class ThesisCreateController 'exemplaireBaiu', 'exemplaireErg', 'cc2r', - 'durationValue', - 'durationUnit' + 'durationPages', + 'durationMins', + 'hasAnnexes' ); } diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index d65a8fa..fb96408 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -109,8 +109,8 @@ class ThesisEditController $currentAccessTypeId = $rawRow['access_type_id'] ?? null; $currentContextNote = $rawRow['context_note'] ?? ''; $currentContactVisible = $rawRow['contact_visible'] ?? ''; - $currentDurationValue = $rawRow['duration_value'] ?? null; - $currentDurationUnit = $rawRow['duration_unit'] ?? 'pages'; + $currentDurationPages = $rawRow['duration_pages'] ?? null; + $currentDurationMins = $rawRow['duration_minutes'] ?? null; // Author contact info (from view) $contactInterne = $thesis['contact_interne'] ?? ''; @@ -134,8 +134,8 @@ class ThesisEditController 'currentAccessTypeId' => $currentAccessTypeId, 'currentContextNote' => $currentContextNote, 'currentContactVisible' => $currentContactVisible, - 'currentDurationValue' => $currentDurationValue, - 'currentDurationUnit' => $currentDurationUnit, + 'currentDurationPages' => $currentDurationPages, + 'currentDurationMins' => $currentDurationMins, 'contactInterne' => $contactInterne, 'contactPublic' => $contactPublic, 'currentRaw' => $rawRow, @@ -223,8 +223,9 @@ class ThesisEditController 'exemplaire_erg' => !empty($post['exemplaire_erg']), 'cc2r' => !empty($post['cc2r']), 'license_custom' => trim($post['license_custom'] ?? ''), - 'duration_value' => isset($post['duration_value']) && $post['duration_value'] !== '' ? (float)$post['duration_value'] : null, - 'duration_unit' => !empty($post['duration_unit']) ? $post['duration_unit'] : 'pages', + 'duration_pages' => (isset($post['duration_pages']) && $post['duration_pages'] !== '') ? (int)$post['duration_pages'] : null, + 'duration_minutes' => (isset($post['duration_minutes']) && $post['duration_minutes'] !== '') ? (int)$post['duration_minutes'] : null, + 'has_annexes' => !empty($post['has_annexes']) ? 1 : 0, ]; // Regenerate identifier if year changed or if identifier prefix doesn't match year $oldThesis = $this->db->getThesis($thesisId); diff --git a/app/src/Database.php b/app/src/Database.php index 9fbeb87..54e6b5b 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2032,7 +2032,7 @@ class Database public function getThesisRawFields(int $thesisId): ?array { $stmt = $this->pdo->prepare( - 'SELECT license_id, license_custom, access_type_id, context_note, contact_visible, remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r, duration_value, duration_unit, is_published FROM theses WHERE id = ? LIMIT 1' + 'SELECT license_id, license_custom, access_type_id, context_note, contact_visible, remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r, duration_pages, duration_minutes, has_annexes, is_published FROM theses WHERE id = ? LIMIT 1' ); $stmt->execute([$thesisId]); $row = $stmt->fetch(); @@ -2176,8 +2176,9 @@ class Database exemplaire_baiu = ?, exemplaire_erg = ?, cc2r = ?, - duration_value = ?, - duration_unit = ?, + duration_pages = ?, + duration_minutes = ?, + has_annexes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? "); @@ -2209,8 +2210,9 @@ class Database !empty($data['exemplaire_baiu']) ? 1 : 0, !empty($data['exemplaire_erg']) ? 1 : 0, !empty($data['cc2r']) ? 1 : 0, - isset($data['duration_value']) && $data['duration_value'] !== '' ? (float)$data['duration_value'] : null, - !empty($data['duration_unit']) ? $data['duration_unit'] : 'pages', + isset($data['duration_pages']) && $data['duration_pages'] !== '' ? (int)$data['duration_pages'] : null, + isset($data['duration_minutes']) && $data['duration_minutes'] !== '' ? (int)$data['duration_minutes'] : null, + !empty($data['has_annexes']) ? 1 : 0, $thesisId, ]); $stmt->execute($params); @@ -2261,9 +2263,10 @@ class Database remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r, - duration_value, duration_unit, + duration_pages, duration_minutes, + has_annexes, submitted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, "draft", ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, "draft", ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) '); $validObjet = ['tfe', 'thèse', 'frart']; @@ -2296,8 +2299,9 @@ class Database !empty($data['exemplaire_baiu']) ? 1 : 0, !empty($data['exemplaire_erg']) ? 1 : 0, !empty($data['cc2r']) ? 1 : 0, - isset($data['duration_value']) && $data['duration_value'] !== '' ? (float)$data['duration_value'] : null, - !empty($data['duration_unit']) ? $data['duration_unit'] : 'pages', + isset($data['duration_pages']) && $data['duration_pages'] !== '' ? (int)$data['duration_pages'] : null, + isset($data['duration_minutes']) && $data['duration_minutes'] !== '' ? (int)$data['duration_minutes'] : null, + !empty($data['has_annexes']) ? 1 : 0, ]); $newId = (int)$this->pdo->lastInsertId(); diff --git a/app/src/Form/FormBootstrap.php b/app/src/Form/FormBootstrap.php index b8bfe81..06e5d54 100644 --- a/app/src/Form/FormBootstrap.php +++ b/app/src/Form/FormBootstrap.php @@ -203,8 +203,7 @@ class FormBootstrap 'contactPublic' => null, 'currentContextNote' => null, 'currentContactVisible' => null, - 'currentDurationValue' => null, - 'currentDurationUnit' => 'pages', + // Files (edit mode) 'currentCover' => null, diff --git a/app/src/icon.php b/app/src/icon.php index 0fcd6da..626c8ef 100644 --- a/app/src/icon.php +++ b/app/src/icon.php @@ -31,5 +31,8 @@ function icon(string $name, int $size = 0, string $class = ''): string { $svg = str_replace(' tags (e.g., contenus.php inline rename) + $svg = str_replace(["\n", "\r"], '', $svg); return $svg; } diff --git a/app/storage/schema.sql b/app/storage/schema.sql index 3534f62..7369989 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -91,6 +91,9 @@ CREATE TABLE IF NOT EXISTS theses ( remarks TEXT, duration_value REAL, duration_unit TEXT DEFAULT 'pages', + duration_pages INTEGER, + duration_minutes INTEGER, + has_annexes INTEGER NOT NULL DEFAULT 0, access_type_id INTEGER, license_id INTEGER, jury_points DECIMAL(4,2), @@ -416,8 +419,9 @@ SELECT t.synopsis, t.context_note, t.contact_visible, - t.duration_value, - t.duration_unit, + t.duration_pages, + t.duration_minutes, + t.has_annexes, at.name as access_type, lt.name as license_type, t.license_id, diff --git a/app/templates/admin/recapitulatif.php b/app/templates/admin/recapitulatif.php index 4e23e1a..13710e5 100644 --- a/app/templates/admin/recapitulatif.php +++ b/app/templates/admin/recapitulatif.php @@ -131,6 +131,25 @@
Mots-clés
+ +
Durée
+ + + +
Annexes
Oui
+ +
Lien BAIU
diff --git a/app/templates/partials/form/fichiers-fragment.php b/app/templates/partials/form/fichiers-fragment.php index 61795f4..934f3f3 100644 --- a/app/templates/partials/form/fichiers-fragment.php +++ b/app/templates/partials/form/fichiers-fragment.php @@ -185,9 +185,15 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
- +
+ +
- +
Durée
-
- - -
- -
- + - 'pages', - 'mo' => 'Mo', - 'durée' => '', - default => $_dUnit, - }; - if ($_dUnit === 'durée') { - $_hours = (int)floor($_dVal); - $_mins = (int)round(($_dVal - $_hours) * 60); - $_display = ($_mins > 0) ? "{$_hours}h{$_mins}" : "{$_hours}h"; - } else { - $_display = ($_dVal == (int)$_dVal) ? (int)$_dVal : $_dVal; - } + $_hasPages = isset($data["duration_pages"]) && $data["duration_pages"] !== null; + $_hasMins = isset($data["duration_minutes"]) && $data["duration_minutes"] !== null; + if ($_hasPages || $_hasMins): + $_parts = []; + if ($_hasPages) $_parts[] = $data['duration_pages'] . ' pages'; + if ($_hasMins) { + $_h = (int)floor($data['duration_minutes'] / 60); + $_m = $data['duration_minutes'] % 60; + $_parts[] = $_h . 'h' . str_pad((string)$_m, 2, '0', STR_PAD_LEFT); + } ?>

Durée : - +

- +