From 1ab95df08430291c0037f7f41108a77c3837b374 Mon Sep 17 00:00:00 2001 From: Mike Colagrosso Date: Fri, 13 Dec 2024 11:45:14 -0600 Subject: [PATCH] Add placeholders for ebooks --- config/sql/se/Benefits.sql | 1 + config/sql/se/EbookPlaceholders.sql | 11 + config/sql/se/Ebooks.sql | 20 +- lib/Benefits.php | 14 +- lib/Constants.php | 3 +- lib/Contributor.php | 22 ++ lib/Ebook.php | 259 +++++++++++++----- lib/EbookPlaceholder.php | 88 ++++++ lib/Enums/EbookPlaceholderDifficulty.php | 8 + lib/Enums/EbookPlaceholderStatus.php | 7 + lib/Enums/EbookReleaseStatusFilter.php | 8 + lib/Exceptions/DuplicateEbookException.php | 10 + .../EbookCreatedDatetimeRequiredException.php | 7 - .../EbookDescriptionRequiredException.php | 7 - .../EbookLanguageRequiredException.php | 7 - .../EbookLongDescriptionRequiredException.php | 7 - .../EbookMissingPlaceholderException.php | 7 + .../EbookReadingEaseRequiredException.php | 7 - ...ookRepoFilesystemPathRequiredException.php | 7 - ...xtSinglePageByteCountRequiredException.php | 7 - .../EbookUnexpectedPlaceholderException.php | 7 + .../EbookUpdatedDatetimeRequiredException.php | 7 - .../EbookWordCountRequiredException.php | 7 - ...bookWwwFilesystemPathRequiredException.php | 7 - .../InvalidEbookIdentifierException.php | 5 + ...EbookPlaceholderYearPublishedException.php | 7 + scripts/generate-bulk-downloads | 4 + scripts/generate-feeds | 4 + templates/ArtworkForm.php | 12 +- templates/ArtworkStatus.php | 2 +- templates/EbookGrid.php | 28 +- templates/EbookPlaceholderForm.php | 223 +++++++++++++++ templates/SearchForm.php | 8 +- templates/UserForm.php | 9 +- www/artworks/get.php | 10 +- www/artworks/new.php | 7 +- www/css/artwork.css | 15 +- www/css/core.css | 183 ++++++++++--- www/css/ebook-placeholder.css | 92 +++++++ www/css/manual.css | 1 - www/ebook-placeholders/get.php | 88 ++++++ www/ebook-placeholders/new.php | 62 +++++ www/ebook-placeholders/post.php | 111 ++++++++ www/ebooks/download.php | 4 + www/ebooks/get.php | 5 + www/ebooks/index.php | 2 +- www/feeds/atom/search.php | 2 +- www/feeds/opds/search.php | 2 +- www/feeds/rss/search.php | 2 +- www/fonts/fork-awesome-subset.woff2 | Bin 4576 -> 3608 bytes www/newsletter/subscriptions/new.php | 2 +- www/users/get.php | 4 + 52 files changed, 1192 insertions(+), 237 deletions(-) create mode 100644 config/sql/se/EbookPlaceholders.sql create mode 100644 lib/EbookPlaceholder.php create mode 100644 lib/Enums/EbookPlaceholderDifficulty.php create mode 100644 lib/Enums/EbookPlaceholderStatus.php create mode 100644 lib/Enums/EbookReleaseStatusFilter.php create mode 100644 lib/Exceptions/DuplicateEbookException.php delete mode 100644 lib/Exceptions/EbookCreatedDatetimeRequiredException.php delete mode 100644 lib/Exceptions/EbookDescriptionRequiredException.php delete mode 100644 lib/Exceptions/EbookLanguageRequiredException.php delete mode 100644 lib/Exceptions/EbookLongDescriptionRequiredException.php create mode 100644 lib/Exceptions/EbookMissingPlaceholderException.php delete mode 100644 lib/Exceptions/EbookReadingEaseRequiredException.php delete mode 100644 lib/Exceptions/EbookRepoFilesystemPathRequiredException.php delete mode 100644 lib/Exceptions/EbookTextSinglePageByteCountRequiredException.php create mode 100644 lib/Exceptions/EbookUnexpectedPlaceholderException.php delete mode 100644 lib/Exceptions/EbookUpdatedDatetimeRequiredException.php delete mode 100644 lib/Exceptions/EbookWordCountRequiredException.php delete mode 100644 lib/Exceptions/EbookWwwFilesystemPathRequiredException.php create mode 100644 lib/Exceptions/InvalidEbookIdentifierException.php create mode 100644 lib/Exceptions/InvalidEbookPlaceholderYearPublishedException.php create mode 100644 templates/EbookPlaceholderForm.php create mode 100644 www/css/ebook-placeholder.css create mode 100644 www/ebook-placeholders/get.php create mode 100644 www/ebook-placeholders/new.php create mode 100644 www/ebook-placeholders/post.php diff --git a/config/sql/se/Benefits.sql b/config/sql/se/Benefits.sql index adaabd8e..e11a7e62 100644 --- a/config/sql/se/Benefits.sql +++ b/config/sql/se/Benefits.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS `Benefits` ( `CanReviewArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanReviewOwnArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditUsers` tinyint(1) unsigned NOT NULL DEFAULT 0, + `CanCreateEbookPlaceholders` tinyint(1) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`UserId`), KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/config/sql/se/EbookPlaceholders.sql b/config/sql/se/EbookPlaceholders.sql new file mode 100644 index 00000000..409591ec --- /dev/null +++ b/config/sql/se/EbookPlaceholders.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `EbookPlaceholders` ( + `EbookId` int(10) unsigned NOT NULL, + `YearPublished` smallint unsigned NULL, + `Status` enum('wanted', 'in_progress') NULL, + `Difficulty` enum('beginner', 'intermediate', 'advanced') NULL, + `TranscriptionUrl` varchar(511) NULL, + `IsWanted` boolean NOT NULL DEFAULT FALSE, + `IsPatron` boolean NOT NULL DEFAULT FALSE, + `Notes` TEXT NULL DEFAULT NULL, + PRIMARY KEY (`EbookId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/config/sql/se/Ebooks.sql b/config/sql/se/Ebooks.sql index dc6bde60..9eda89e9 100644 --- a/config/sql/se/Ebooks.sql +++ b/config/sql/se/Ebooks.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS `Ebooks` ( `Identifier` varchar(511) NOT NULL, `Created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `Updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `WwwFilesystemPath` varchar(511) NOT NULL, - `RepoFilesystemPath` varchar(511) NOT NULL, + `WwwFilesystemPath` varchar(511) NULL, + `RepoFilesystemPath` varchar(511) NULL, `KindleCoverUrl` varchar(511) NULL, `EpubUrl` varchar(511) NULL, `AdvancedEpubUrl` varchar(511) NULL, @@ -14,16 +14,16 @@ CREATE TABLE IF NOT EXISTS `Ebooks` ( `Title` varchar(255) NOT NULL, `FullTitle` varchar(255) NULL, `AlternateTitle` varchar(255) NULL, - `Description` text NOT NULL, - `LongDescription` text NOT NULL, - `Language` varchar(10) NOT NULL, - `WordCount` int(10) unsigned NOT NULL, - `ReadingEase` float NOT NULL, + `Description` text NULL, + `LongDescription` text NULL, + `Language` varchar(10) NULL, + `WordCount` int(10) unsigned NULL, + `ReadingEase` float NULL, `GitHubUrl` varchar(255) NULL, `WikipediaUrl` varchar(255) NULL, - `EbookCreated` datetime NOT NULL, - `EbookUpdated` datetime NOT NULL, - `TextSinglePageByteCount` bigint unsigned NOT NULL, + `EbookCreated` datetime NULL, + `EbookUpdated` datetime NULL, + `TextSinglePageByteCount` bigint unsigned NULL, `IndexableText` text NOT NULL, PRIMARY KEY (`EbookId`), UNIQUE KEY `index1` (`Identifier`), diff --git a/lib/Benefits.php b/lib/Benefits.php index e439b570..9de908fd 100644 --- a/lib/Benefits.php +++ b/lib/Benefits.php @@ -15,6 +15,7 @@ class Benefits{ public bool $CanReviewArtwork = false; public bool $CanReviewOwnArtwork = false; public bool $CanEditUsers = false; + public bool $CanCreateEbookPlaceholders = false; protected bool $_HasBenefits; @@ -27,6 +28,8 @@ class Benefits{ $this->CanReviewOwnArtwork || $this->CanEditUsers + || + $this->CanCreateEbookPlaceholders ){ return true; } @@ -58,18 +61,18 @@ class Benefits{ public function Create(): void{ Db::Query(' - INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers) - values (?, ?, ?, ?, ?, ?, ?, ?) - ', [$this->UserId, $this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers]); + INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers, CanCreateEbookPlaceholders) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + ', [$this->UserId, $this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanCreateEbookPlaceholders]); } public function Save(): void{ Db::Query(' UPDATE Benefits - set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ? + set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ?, CanCreateEbookPlaceholders = ? where UserId = ? - ', [$this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->UserId]); + ', [$this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanCreateEbookPlaceholders, $this->UserId]); } public function FillFromHttpPost(): void{ @@ -80,5 +83,6 @@ class Benefits{ $this->PropertyFromHttp('CanReviewArtwork'); $this->PropertyFromHttp('CanReviewOwnArtwork'); $this->PropertyFromHttp('CanEditUsers'); + $this->PropertyFromHttp('CanCreateEbookPlaceholders'); } } diff --git a/lib/Constants.php b/lib/Constants.php index 6b27bb16..be3641bc 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -29,7 +29,8 @@ const MANUAL_PATH = WEB_ROOT . '/manual'; const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/'; const COVER_ART_UPLOAD_PATH = '/images/cover-uploads/'; -const EBOOKS_IDENTIFIER_PREFIX = 'url:https://standardebooks.org/ebooks/'; +const EBOOKS_IDENTIFIER_ROOT = 'url:https://standardebooks.org'; +const EBOOKS_IDENTIFIER_PREFIX = EBOOKS_IDENTIFIER_ROOT . '/ebooks/'; const DATABASE_DEFAULT_DATABASE = 'se'; const DATABASE_DEFAULT_HOST = 'localhost'; diff --git a/lib/Contributor.php b/lib/Contributor.php index 4513b594..33ab0b45 100644 --- a/lib/Contributor.php +++ b/lib/Contributor.php @@ -41,6 +41,28 @@ class Contributor{ // METHODS // ******* + /** + * @return array + */ + public static function GetAllAuthorNames(): array{ + return Db::Query(' + SELECT DISTINCT Name + from Contributors + where MarcRole = "aut" + order by Name asc', [], Contributor::class); + } + + /** + * @return array + */ + public static function GetAllTranslatorNames(): array{ + return Db::Query(' + SELECT DISTINCT Name + from Contributors + where MarcRole = "trl" + order by Name asc', [], Contributor::class); + } + /** * @throws Exceptions\ValidationException */ diff --git a/lib/Ebook.php b/lib/Ebook.php index 1b007aef..4cf7dec0 100644 --- a/lib/Ebook.php +++ b/lib/Ebook.php @@ -41,14 +41,15 @@ use function Safe\shell_exec; * @property string $TextSinglePageUrl * @property string $TextSinglePageSizeFormatted * @property string $IndexableText + * @property EbookPlaceholder $EbookPlaceholder */ class Ebook{ use Traits\Accessor; public int $EbookId; public string $Identifier; - public string $WwwFilesystemPath; - public string $RepoFilesystemPath; + public ?string $WwwFilesystemPath = null; + public ?string $RepoFilesystemPath = null; public ?string $KindleCoverUrl = null; public ?string $EpubUrl = null; public ?string $AdvancedEpubUrl = null; @@ -58,17 +59,17 @@ class Ebook{ public string $Title; public ?string $FullTitle = null; public ?string $AlternateTitle = null; - public string $Description; - public string $LongDescription; - public string $Language; - public int $WordCount; - public float $ReadingEase; + public ?string $Description = null; + public ?string $LongDescription = null; + public ?string $Language = null; + public ?int $WordCount = null; + public ?float $ReadingEase = null; public ?string $GitHubUrl = null; public ?string $WikipediaUrl = null; /** When the ebook was published. */ - public DateTimeImmutable $EbookCreated; + public ?DateTimeImmutable $EbookCreated = null; /** When the ebook was updated. */ - public DateTimeImmutable $EbookUpdated; + public ?DateTimeImmutable $EbookUpdated = null; /** When the database row was created. */ public DateTimeImmutable $Created; /** When the database row was updated. */ @@ -116,6 +117,7 @@ class Ebook{ protected string $_TextSinglePageUrl; protected string $_TextSinglePageSizeFormatted; protected string $_IndexableText; + protected ?EbookPlaceholder $_EbookPlaceholder = null; // ******* // GETTERS @@ -318,7 +320,7 @@ class Ebook{ protected function GetUrl(): string{ if(!isset($this->_Url)){ - $this->_Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath); + $this->_Url = str_replace(EBOOKS_IDENTIFIER_ROOT, '', $this->Identifier); } return $this->_Url; @@ -554,7 +556,7 @@ class Ebook{ protected function GetTextSinglePageSizeFormatted(): string{ if(!isset($this->_TextSinglePageSizeFormatted)){ $bytes = $this->TextSinglePageByteCount; - $sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); + $sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; $index = 0; while($bytes >= 1024 && $index < count($sizes) - 1){ @@ -608,6 +610,18 @@ class Ebook{ return $this->_IndexableText; } + protected function GetEbookPlaceholder(): ?EbookPlaceholder{ + if(!isset($this->_EbookPlaceholder)){ + $this->_EbookPlaceholder = Db::Query(' + SELECT * + from EbookPlaceholders + where EbookId = ? + ', [$this->EbookId], EbookPlaceholder::class)[0] ?? null; + } + + return $this->_EbookPlaceholder; + } + // *********** // ORM METHODS @@ -970,6 +984,59 @@ class Ebook{ return $ebook; } + /** + * Joins the `Name` properites of `Contributor` objects as a URL slug, e.g., + * + * ``` + * ([0] => Contributor Object ([Name] => William Wordsworth), ([1] => Contributor Object ([Name] => Samuel Coleridge))) + * ``` + * + * returns `william-wordsworth_samuel-taylor-coleridge`. + * + * @param array $contributors + */ + protected static function GetContributorsUrlSlug(array $contributors): string{ + return implode('_', array_map('Formatter::MakeUrlSafe', array_column($contributors, 'Name'))); + } + + /** + * Populates the `Identifier` property based on the `Title`, `Authors`, `Translators`, and `Illustrators`. Used when creating ebook placeholders. + * + * @throws Exceptions\InvalidEbookIdentifierException + */ + public function FillIdentifierFromTitleAndContributors(): void{ + if(!isset($this->Authors) || sizeof($this->Authors) == 0){ + throw new Exceptions\InvalidEbookIdentifierException('Authors required'); + } + + if(!isset($this->Title)){ + throw new Exceptions\InvalidEbookIdentifierException('Title required'); + } + + $authorString = Ebook::GetContributorsUrlSlug($this->Authors); + $titleString = Formatter::MakeUrlSafe($this->Title); + $translatorString = ''; + $illustratorString = ''; + + if(isset($this->Translators) && sizeof($this->Translators) > 0){ + $translatorString = Ebook::GetContributorsUrlSlug($this->Translators); + } + + if(isset($this->Illustrators) && sizeof($this->Illustrators) > 0){ + $illustratorString = Ebook::GetContributorsUrlSlug($this->Illustrators); + } + + $this->Identifier = EBOOKS_IDENTIFIER_PREFIX . $authorString . '/' . $titleString; + + if($translatorString != ''){ + $this->Identifier .= '/' . $translatorString; + } + + if($illustratorString != ''){ + $this->Identifier .= '/' . $illustratorString; + } + } + // ******* // METHODS @@ -996,13 +1063,12 @@ class Ebook{ $error->Add(new Exceptions\EbookIdentifierRequiredException()); } + $this->WwwFilesystemPath = trim($this->WwwFilesystemPath ?? ''); + if($this->WwwFilesystemPath == ''){ + $this->WwwFilesystemPath = null; + } + if(isset($this->WwwFilesystemPath)){ - $this->WwwFilesystemPath = trim($this->WwwFilesystemPath); - - if($this->WwwFilesystemPath == ''){ - $error->Add(new Exceptions\EbookWwwFilesystemPathRequiredException()); - } - if(strlen($this->WwwFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){ $error->Add(new Exceptions\StringTooLongException('Ebook WwwFilesystemPath')); } @@ -1011,17 +1077,13 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookWwwFilesystemPathException($this->WwwFilesystemPath)); } } - else{ - $error->Add(new Exceptions\EbookWwwFilesystemPathRequiredException()); + + $this->RepoFilesystemPath = trim($this->RepoFilesystemPath ?? ''); + if($this->RepoFilesystemPath == ''){ + $this->RepoFilesystemPath = null; } if(isset($this->RepoFilesystemPath)){ - $this->RepoFilesystemPath = trim($this->RepoFilesystemPath); - - if($this->RepoFilesystemPath == ''){ - $error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException()); - } - if(strlen($this->RepoFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){ $error->Add(new Exceptions\StringTooLongException('Ebook RepoFilesystemPath')); } @@ -1030,9 +1092,6 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookRepoFilesystemPathException($this->RepoFilesystemPath)); } } - else{ - $error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException()); - } $this->KindleCoverUrl = trim($this->KindleCoverUrl ?? ''); if($this->KindleCoverUrl == ''){ @@ -1157,51 +1216,36 @@ class Ebook{ $error->Add(new Exceptions\StringTooLongException('Ebook AlternateTitle')); } - if(isset($this->Description)){ - $this->Description = trim($this->Description); - - if($this->Description == ''){ - $error->Add(new Exceptions\EbookDescriptionRequiredException()); - } - } - else{ - $error->Add(new Exceptions\EbookDescriptionRequiredException()); + $this->Description = trim($this->Description ?? ''); + if($this->Description == ''){ + $this->Description = null; } - if(isset($this->LongDescription)){ - $this->LongDescription = trim($this->LongDescription); - - if($this->LongDescription == ''){ - $error->Add(new Exceptions\EbookLongDescriptionRequiredException()); - } + if(isset($this->Description) && strlen($this->Description) > EBOOKS_MAX_STRING_LENGTH){ + $error->Add(new Exceptions\StringTooLongException('Ebook Description')); } - else{ - $error->Add(new Exceptions\EbookLongDescriptionRequiredException()); + + $this->LongDescription = trim($this->LongDescription ?? ''); + if($this->LongDescription == ''){ + $this->LongDescription = null; + } + + $this->Language = trim($this->Language ?? ''); + if($this->Language == ''){ + $this->Language = null; } if(isset($this->Language)){ - $this->Language = trim($this->Language); - - if($this->Language == ''){ - $error->Add(new Exceptions\EbookLanguageRequiredException()); - } - if(strlen($this->Language) > 10){ $error->Add(new Exceptions\StringTooLongException('Ebook Language: ' . $this->Language)); } } - else{ - $error->Add(new Exceptions\EbookLanguageRequiredException()); - } if(isset($this->WordCount)){ if($this->WordCount <= 0){ $error->Add(new Exceptions\InvalidEbookWordCountException('Invalid Ebook WordCount: ' . $this->WordCount)); } } - else{ - $error->Add(new Exceptions\EbookWordCountRequiredException()); - } if(isset($this->ReadingEase)){ // In theory, Flesch reading ease can be negative, but in practice it's positive. @@ -1209,9 +1253,6 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookReadingEaseException('Invalid Ebook ReadingEase: ' . $this->ReadingEase)); } } - else{ - $error->Add(new Exceptions\EbookReadingEaseRequiredException()); - } $this->GitHubUrl = trim($this->GitHubUrl ?? ''); if($this->GitHubUrl == ''){ @@ -1248,9 +1289,6 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookCreatedDatetimeException($this->EbookCreated)); } } - else{ - $error->Add(new Exceptions\EbookCreatedDatetimeRequiredException()); - } if(isset($this->EbookUpdated)){ if($this->EbookUpdated > NOW){ @@ -1258,18 +1296,12 @@ class Ebook{ } } - else{ - $error->Add(new Exceptions\EbookUpdatedDatetimeRequiredException()); - } if(isset($this->TextSinglePageByteCount)){ if($this->TextSinglePageByteCount <= 0){ $error->Add(new Exceptions\InvalidEbookTextSinglePageByteCountException('Invalid Ebook TextSinglePageByteCount: ' . $this->TextSinglePageByteCount)); } } - else{ - $error->Add(new Exceptions\EbookTextSinglePageByteCountRequiredException()); - } if(isset($this->IndexableText)){ $this->IndexableText = trim($this->IndexableText ?? ''); @@ -1282,6 +1314,23 @@ class Ebook{ $error->Add(new Exceptions\EbookIndexableTextRequiredException()); } + if(isset($this->EbookPlaceholder)){ + try{ + $this->EbookPlaceholder->Validate(); + } + catch(Exceptions\ValidationException $ex){ + $error->Add($ex); + } + } + + if($this->IsPlaceholder() && !isset($this->EbookPlaceholder)){ + $error->Add(new Exceptions\EbookMissingPlaceholderException()); + } + + if(!$this->IsPlaceholder() && isset($this->EbookPlaceholder)){ + $error->Add(new Exceptions\EbookUnexpectedPlaceholderException()); + } + if($error->HasExceptions){ throw $error; } @@ -1289,6 +1338,7 @@ class Ebook{ /** * @throws Exceptions\ValidationException + * @throws Exceptions\DuplicateEbookException */ public function CreateOrUpdate(): void{ try{ @@ -1550,6 +1600,10 @@ class Ebook{ return $string; } + public function IsPlaceholder(): bool{ + return $this->WwwFilesystemPath === null; + } + /** * If the given list of elements has an element that is not `''`, return that value; otherwise, return `null`. * @@ -1572,10 +1626,19 @@ class Ebook{ /** * @throws Exceptions\ValidationException + * @throws Exceptions\DuplicateEbookException If an `Ebook` with the given identifier already exists. */ public function Create(): void{ $this->Validate(); + try{ + Ebook::GetByIdentifier($this->Identifier); + throw new Exceptions\DuplicateEbookException($this->Identifier); + } + catch(Exceptions\EbookNotFoundException){ + // Pass. + } + $this->CreateTags(); $this->CreateLocSubjects(); $this->CreateCollections(); @@ -1623,6 +1686,7 @@ class Ebook{ $this->AddSources(); $this->AddContributors(); $this->AddTocEntries(); + $this->AddEbookPlaceholder(); } /** @@ -1690,6 +1754,9 @@ class Ebook{ $this->RemoveTocEntries(); $this->AddTocEntries(); + + $this->RemoveEbookPlaceholder(); + $this->AddEbookPlaceholder(); } private function RemoveTags(): void{ @@ -1848,6 +1915,24 @@ class Ebook{ } } + private function RemoveEbookPlaceholder(): void{ + Db::Query(' + DELETE from EbookPlaceholders + where EbookId = ? + ', [$this->EbookId] + ); + } + + /** + * @throws Exceptions\ValidationException + */ + private function AddEbookPlaceholder(): void{ + if(isset($this->EbookPlaceholder)){ + $this->EbookPlaceholder->EbookId = $this->EbookId; + $this->EbookPlaceholder->Create(); + } + } + // *********** // ORM METHODS // *********** @@ -1929,6 +2014,9 @@ class Ebook{ } /** + * Queries for books in a collection. + * + * Puts ebooks without a `SequenceNumber` at the end of the list, which is more common in a collection with both published and placeholder ebooks. * @return array */ public static function GetAllByCollection(string $collection): array{ @@ -1938,13 +2026,16 @@ class Ebook{ inner join CollectionEbooks ce using (EbookId) inner join Collections c using (CollectionId) where c.UrlName = ? - order by ce.SequenceNumber, e.EbookCreated desc + order by ce.SequenceNumber is null, ce.SequenceNumber, e.EbookCreated desc ', [$collection], Ebook::class); return $ebooks; } /** + * Queries for related to books to be shown, e.g., in a carousel. + * + * Filters out placeholder books because they are not useful for browsing. * @return array */ public static function GetAllByRelated(Ebook $ebook, int $count, ?EbookTag $relatedTag): array{ @@ -1955,6 +2046,7 @@ class Ebook{ inner join EbookTags et using (EbookId) where et.TagId = ? and et.EbookId != ? + and e.WwwFilesystemPath is not null order by rand() limit ? ', [$relatedTag->TagId, $ebook->EbookId, $count], Ebook::class); @@ -1964,6 +2056,7 @@ class Ebook{ SELECT * from Ebooks where EbookId != ? + and WwwFilesystemPath is not null order by rand() limit ? ', [$ebook->EbookId, $count], Ebook::class); @@ -1977,25 +2070,43 @@ class Ebook{ * * @return array{ebooks: array, ebooksCount: int} */ - public static function GetAllByFilter(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE): array{ + public static function GetAllByFilter(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE, Enums\EbookReleaseStatusFilter $releaseStatusFilter = Enums\EbookReleaseStatusFilter::All): array{ $limit = $perPage; $offset = (($page - 1) * $perPage); $joinContributors = ''; $joinTags = ''; $params = []; - $whereCondition = 'where true'; + + switch($releaseStatusFilter){ + case Enums\EbookReleaseStatusFilter::Released: + $whereCondition = 'where e.WwwFilesystemPath is not null'; + break; + case Enums\EbookReleaseStatusFilter::Placeholder: + $whereCondition = 'where e.WwwFilesystemPath is null'; + break; + case Enums\EbookReleaseStatusFilter::All: + default: + if($query !== null && $query != ''){ + // If the query is present, show both released and placeholder ebooks. + $whereCondition = 'where true'; + }else{ + // If there is no query, hide placeholder ebooks. + $whereCondition = 'where e.WwwFilesystemPath is not null'; + } + break; + } $orderBy = 'e.EbookCreated desc'; if($sort == Enums\EbookSortType::AuthorAlpha){ $joinContributors = 'inner join Contributors con using (EbookId)'; $whereCondition .= ' and con.MarcRole = "aut"'; - $orderBy = 'con.SortName, e.EbookCreated desc'; + $orderBy = 'e.WwwFilesystemPath is null, con.SortName, e.EbookCreated desc'; // Put placeholders at the end } elseif($sort == Enums\EbookSortType::ReadingEase){ $orderBy = 'e.ReadingEase desc'; } elseif($sort == Enums\EbookSortType::Length){ - $orderBy = 'e.WordCount'; + $orderBy = 'e.WwwFilesystemPath is null, e.WordCount'; // Put placeholders at the end } if(sizeof($tags) > 0 && !in_array('all', $tags)){ // 0 tags means "all ebooks" diff --git a/lib/EbookPlaceholder.php b/lib/EbookPlaceholder.php new file mode 100644 index 00000000..653bac5d --- /dev/null +++ b/lib/EbookPlaceholder.php @@ -0,0 +1,88 @@ +_IsPublicDomain)){ + $this->_IsPublicDomain = $this->YearPublished === null ? true : $this->YearPublished <= PD_YEAR; + } + + return $this->_IsPublicDomain; + } + + public function FillFromHttpPost(): void{ + $this->PropertyFromHttp('YearPublished'); + $this->PropertyFromHttp('IsWanted'); + + // These properties apply only to books on the SE wanted list. + if($this->IsWanted){ + $this->PropertyFromHttp('Status'); + $this->PropertyFromHttp('Difficulty'); + $this->PropertyFromHttp('TranscriptionUrl'); + $this->PropertyFromHttp('IsPatron'); + $this->PropertyFromHttp('Notes'); + } + } + + /** + * @throws Exceptions\ValidationException + */ + public function Validate(): void{ + $thisYear = intval(NOW->format('Y')); + $error = new Exceptions\ValidationException(); + + if(isset($this->YearPublished) && ($this->YearPublished <= 0 || $this->YearPublished > $thisYear)){ + $error->Add(new Exceptions\InvalidEbookPlaceholderYearPublishedException()); + } + + $this->TranscriptionUrl = trim($this->TranscriptionUrl ?? ''); + if($this->TranscriptionUrl == ''){ + $this->TranscriptionUrl = null; + } + + $this->Notes = trim($this->Notes ?? ''); + if($this->Notes == ''){ + $this->Notes = null; + } + + if($error->HasExceptions){ + throw $error; + } + } + + /** + * @throws Exceptions\ValidationException + */ + public function Create(): void{ + $this->Validate(); + Db::Query(' + INSERT into EbookPlaceholders (EbookId, YearPublished, Status, Difficulty, TranscriptionUrl, + IsWanted, IsPatron, Notes) + values (?, + ?, + ?, + ?, + ?, + ?, + ?, + ?) + ', [$this->EbookId, $this->YearPublished, $this->Status, $this->Difficulty, $this->TranscriptionUrl, + $this->IsWanted, $this->IsPatron, $this->Notes]); + } +} diff --git a/lib/Enums/EbookPlaceholderDifficulty.php b/lib/Enums/EbookPlaceholderDifficulty.php new file mode 100644 index 00000000..6d633f2c --- /dev/null +++ b/lib/Enums/EbookPlaceholderDifficulty.php @@ -0,0 +1,8 @@ +message = 'Ebook already exists with identifier: ' . $identifier; + + parent::__construct($this->message); + } +} diff --git a/lib/Exceptions/EbookCreatedDatetimeRequiredException.php b/lib/Exceptions/EbookCreatedDatetimeRequiredException.php deleted file mode 100644 index bd3f3914..00000000 --- a/lib/Exceptions/EbookCreatedDatetimeRequiredException.php +++ /dev/null @@ -1,7 +0,0 @@ -IsPlaceholder()){ + continue; + } + $timestamp = $ebook->EbookCreated->format('Y-m'); $updatedTimestamp = $ebook->EbookUpdated->getTimestamp(); diff --git a/scripts/generate-feeds b/scripts/generate-feeds index 907e3a0b..ed45a09e 100755 --- a/scripts/generate-feeds +++ b/scripts/generate-feeds @@ -100,6 +100,10 @@ foreach($dirs as $dir){ // Iterate over all ebooks to build the various feeds. foreach(Ebook::GetAll() as $ebook){ + if($ebook->IsPlaceholder()){ + continue; + } + $allEbooks[] = $ebook; $newestEbooks[] = $ebook; diff --git a/templates/ArtworkForm.php b/templates/ArtworkForm.php index e668a1fd..dbf4dde4 100644 --- a/templates/ArtworkForm.php +++ b/templates/ArtworkForm.php @@ -12,7 +12,7 @@ $isEditForm = $isEditForm ?? false; ?>
Artist details -
Artwork details -
diff --git a/www/artworks/get.php b/www/artworks/get.php index db959911..477eb22e 100644 --- a/www/artworks/get.php +++ b/www/artworks/get.php @@ -102,7 +102,15 @@ catch(Exceptions\InvalidPermissionsException){ Tags - + +
    + Tags as $tag){ ?> +
  • + Name) ?> +
  • + +
+ Dimensions diff --git a/www/artworks/new.php b/www/artworks/new.php index e68bc531..8dc275f5 100644 --- a/www/artworks/new.php +++ b/www/artworks/new.php @@ -16,15 +16,14 @@ try{ throw new Exceptions\InvalidPermissionsException(); } - // We got here because an artwork was successfully submitted. if($isCreated){ + // We got here because an `Artwork` was successfully submitted. http_response_code(Enums\HttpCode::Created->value); $artwork = null; session_unset(); } - - // We got here because an artwork submission had errors and the user has to try again. - if($exception){ + elseif($exception){ + // We got here because an `Artwork` submission had errors and the user has to try again. http_response_code(Enums\HttpCode::UnprocessableContent->value); session_unset(); } diff --git a/www/css/artwork.css b/www/css/artwork.css index 5e61c243..6c7eae2e 100644 --- a/www/css/artwork.css +++ b/www/css/artwork.css @@ -166,6 +166,10 @@ form[action^="/artworks/"]{ grid-column: 1 / span 4; } +.year + label:has(input[type="checkbox"]){ + margin-top: .5rem; +} + label.picture::before{ bottom: 2.6rem; /* Fix alignment caused by "circa" checkbox */ } @@ -226,17 +230,6 @@ form.create-update-artwork fieldset p:first-of-type{ margin-top: 0; } -form.create-update-artwork legend{ - font-size: 1.4rem; - font-family: "League Spartan", Arial, sans-serif; - margin-top: 2rem; - margin-bottom: 1rem; - line-height: 1.2; - letter-spacing: 1px; - text-transform: uppercase; - border-bottom: none; -} - form.create-update-artwork label{ display: block; } diff --git a/www/css/core.css b/www/css/core.css index 4b52d829..65e262eb 100644 --- a/www/css/core.css +++ b/www/css/core.css @@ -840,7 +840,8 @@ label:has(input[type="checkbox"]):focus-within, select:focus, button:focus, nav a[rel]:focus, -textarea:focus{ +textarea:focus, +summary:focus{ outline: 1px dashed var(--input-outline); } @@ -1572,6 +1573,19 @@ ol.ebooks-list > li p.author a{ justify-content: center; } +.placeholder-cover{ + background-color: transparent; + border-radius: .25rem; + border: 2px dashed var(--sub-text); + height: 331px; + width: 221px; +} + +ol.ebooks-list.list .placeholder-cover{ + height: 191px; + width: 126px; +} + article nav ol, main nav.pagination ol{ list-style: none; @@ -1790,12 +1804,9 @@ input[type="file"]{ label:has(input[type="email"]) input, label:has(input[type="search"]) input, label:has(input[type="url"]) input, -label.captcha input, -label.year input, -label.tags input, -label.picture input, -label.user input, -label:has(input[type="password"]) input{ +label.icon input, +label:has(input[type="password"]) input, +label.icon select{ padding-left: 2.5rem; } @@ -1850,8 +1861,8 @@ label:has(input[type="radio"]):has(> span) input, label:has(input[type="checkbox"]):has(> span) input{ grid-row: 1 / span 2; justify-self: center; - align-self: start; - margin-top: 10px; + align-self: center; + margin: 0; margin-right: .5rem; } @@ -1895,11 +1906,7 @@ label:has(select) > span + span, label:has(input[type="email"]), label:has(input[type="search"]), label:has(input[type="url"]), -label.captcha, -label.year, -label.tags, -label.picture, -label.user, +label.icon, label:has(input[type="password"]){ display: block; position: relative; @@ -1908,11 +1915,7 @@ label:has(input[type="password"]){ label:has(input[type="email"])::before, label:has(input[type="search"])::before, -label.captcha::before, -label.year::before, -label.tags::before, -label.picture::before, -label.user::before, +label.icon::before, label:has(input[type="url"])::before, label:has(input[type="password"])::before{ display: block; @@ -1954,6 +1957,26 @@ label.year::before{ content: "\f073"; } +label.meter::before{ + content: "\f0e4"; +} + +label.collection::before{ + content: "\f0e8"; +} + +label.book::before{ + content: "\f02d"; +} + +label.ordered-list::before{ + content: "\f0cb"; +} + +label.hourglass::before{ + content: "\f254"; +} + label.tags:not(:has(select))::before{ content: "\f02c"; } @@ -2489,15 +2512,37 @@ form[action="/newsletter/subscriptions"] fieldset{ fieldset{ padding: 0; + margin: 0; border: none; } -fieldset p, -fieldset legend{ +label.controls-following-fieldset + fieldset, +details summary ~ *{ + margin-left: 1rem; +} + +fieldset p{ border-bottom: 1px dashed var(--input-border); } -fieldset legend, +fieldset legend{ + font-size: 1.4rem; + font-family: "League Spartan", Arial, sans-serif; + margin-top: 2rem; + margin-bottom: 1rem; + line-height: 1.2; + letter-spacing: 1px; + text-transform: uppercase; +} + +summary{ + cursor: pointer; +} + +summary + fieldset{ + margin-top: 1rem; +} + fieldset p:has(+ ul){ font-weight: bold; } @@ -2604,8 +2649,8 @@ aside header{ font-size: 1.5rem; } -.meter, -.progress{ +div.meter, +div.progress{ position: relative; font-size: 0; } @@ -2677,13 +2722,13 @@ aside header{ border-top: 1px solid #000; } -.progress + p{ +div.progress + p{ margin-top: 2rem; hyphens: auto; } -.meter p, -.progress p{ +div.meter p, +div.progress p{ font-size: 1rem; font-family: "League Spartan", Arial, sans-serif; left: 0; @@ -2701,7 +2746,7 @@ aside header{ margin-top: 0; } -.progress p.start{ +div.progress p.start{ margin-right: auto; margin-left: 1rem; border: none; @@ -2710,7 +2755,7 @@ aside header{ font-size: .75rem; } -.progress p.target{ +div.progress p.target{ margin-left: auto; margin-right: 1rem; border: none; @@ -2719,8 +2764,8 @@ aside header{ font-size: .75rem; } -.meter > div, -.progress > div{ +div.meter > div, +div.progress > div{ position: absolute; width: 100%; height: 100%; @@ -3121,6 +3166,74 @@ form[action="/settings"] label{ display: inline-block; } +ol.ebooks-list > li a.wanted, +ol.ebooks-list > li.wanted .placeholder-cover{ + position: relative; +} + +ol.ebooks-list > li.wanted .placeholder-cover::before, +ol.ebooks-list > li.wanted a::after{ + /* Ribbon */ + font-family: "League Spartan", sans-serif; + position: absolute; + left: -.5rem; + top: calc(.25rem + 3px); + padding: .5rem 1rem .5rem .5rem; + line-height: 1; + color: #ffffff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, .75); + text-transform: uppercase; + font-size: .5rem; + white-space: nowrap; + font-weight: bold; + clip-path: polygon(0 0, 100% 0, 85% 100%, 0 100%); + z-index: 2; +} + +ol.ebooks-list > li[property="schema:hasPart"][value].wanted .placeholder-cover::before, +ol.ebooks-list > li[property="schema:hasPart"][value].wanted a::after{ + top: calc(.25rem + 3px + 2rem); +} + +ol.ebooks-list > li.wanted .placeholder-cover::after{ + /* Ribbon bottom wrap-around */ + content: ""; + position: absolute; + top: calc(.5rem + .5rem + .5rem + .5rem - 2px); + height: .5rem; + width: .5rem; + left: -.5rem; + clip-path: polygon(0 0, 100% 0, 100% 100%, 0 0); + z-index: 1; + filter: brightness(50%); +} + +ol.ebooks-list > li[property="schema:hasPart"][value].wanted .placeholder-cover::after{ + top: calc(.25rem + 3px + 2rem + .5rem + .5rem + .5rem); +} + +ol.ebooks-list > li.wanted a::after{ + /* Ribbon shadow */ + left: calc(-.5rem + 4px); + top: calc(.5rem + 2px); + background: rgba(0, 0, 0, .4); + z-index: auto; +} + +ol.ebooks-list > li[property="schema:hasPart"][value].wanted a::after{ + top: calc(.25rem + 3px + 2rem + 4px); +} + +ol.ebooks-list > li.wanted .placeholder-cover::before, +ol.ebooks-list > li.wanted .placeholder-cover::after{ + background: #861d1d; +} + +ol.ebooks-list > li.wanted .placeholder-cover::before, +ol.ebooks-list > li.wanted a::after{ + content: "wanted"; +} + @media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */ /* For iPad, unset the height so it matches the other elements */ select[multiple]{ @@ -3717,13 +3830,13 @@ form[action="/settings"] label{ line-height: 1.4; } - .progress p.start, - .progress p.target, - .progress p.stretch-base{ + div.progress p.start, + div.progress p.target, + div.progress p.stretch-base{ display: none; } - .progress p{ + div.progress p{ border: none; background: transparent; } diff --git a/www/css/ebook-placeholder.css b/www/css/ebook-placeholder.css new file mode 100644 index 00000000..5e164e3d --- /dev/null +++ b/www/css/ebook-placeholder.css @@ -0,0 +1,92 @@ +form.create-update-ebook-placeholder fieldset{ + display: grid; + gap: 2rem; +} + +form.create-update-ebook-placeholder details + fieldset, +form.create-update-ebook-placeholder fieldset + fieldset{ + margin-top: 2rem; +} + +form.create-update-ebook-placeholder > fieldset:nth-of-type(1), +form.create-update-ebook-placeholder details:nth-of-type(1) fieldset{ + grid-template-columns: 1fr 1fr; +} + +form.create-update-ebook-placeholder > fieldset:nth-of-type(2), +form.create-update-ebook-placeholder details:nth-of-type(2) fieldset{ + grid-template-columns: 1fr 200px; +} + +form.create-update-ebook-placeholder fieldset label:has(input[name="ebook-placeholder-transcription-url"]), +form.create-update-ebook-placeholder fieldset label:has(textarea[name="ebook-placeholder-notes"]){ + grid-column: 1 / span 2; +} + +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="sequence-number-collection-name-1"]), +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="sequence-number-collection-name-2"]), +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="sequence-number-collection-name-3"]), +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="ebook-placeholder-year-published"]){ + display: grid; + grid-template-columns: 200px 1fr; + gap: 2rem; +} + +form.create-update-ebook-placeholder fieldset label:has(input[type="checkbox"]){ + grid-column: 1 / span 2; +} + +form.create-update-ebook-placeholder details{ + margin-top: 1rem; +} + +form.create-update-ebook-placeholder summary{ + font-style: italic; +} + +form.create-update-ebook-placeholder fieldset p{ + font-style: italic; + margin: 0; + border: none; +} + +form.create-update-ebook-placeholder fieldset p:first-of-type{ + margin-top: 0; +} + +form.create-update-ebook-placeholder legend{ + font-size: 1.4rem; + font-family: "League Spartan", Arial, sans-serif; + margin-bottom: 1rem; + line-height: 1.2; + letter-spacing: 1px; + text-transform: uppercase; +} + +form.create-update-ebook-placeholder label{ + display: block; +} + +form div.footer{ + margin-top: 1rem; + text-align: right; +} + +/* Hide the next fieldset unless the ebook-placeholder-is-wanted checkbox is checked. */ +form.create-update-ebook-placeholder fieldset:has(input[name="ebook-placeholder-is-wanted"]) fieldset{ + display: none; + grid-column: 1 / span 2; +} + +form.create-update-ebook-placeholder fieldset:has(input[name="ebook-placeholder-is-wanted"]:checked) fieldset{ + display: grid; +} + +article.ebook.ebook-placeholder > header{ + border: 2px dashed var(--sub-text); + border-top: 0; +} + +article.ebook.ebook-placeholder .placeholder-details{ + padding-top: 2rem; +} diff --git a/www/css/manual.css b/www/css/manual.css index 54cd0cff..e77d4274 100644 --- a/www/css/manual.css +++ b/www/css/manual.css @@ -349,7 +349,6 @@ code.full .utf{ } .step-by-step-guide summary{ - cursor: pointer; /* reproduce H2 styling: */ font-size: 1.4rem; font-family: "League Spartan", Arial, sans-serif; diff --git a/www/ebook-placeholders/get.php b/www/ebook-placeholders/get.php new file mode 100644 index 00000000..5455bd9c --- /dev/null +++ b/www/ebook-placeholders/get.php @@ -0,0 +1,88 @@ + strip_tags($ebook->TitleWithCreditsHtml), + 'css' => ['/css/ebook-placeholder.css'], + 'highlight' => 'ebooks', + 'canonicalUrl' => SITE_URL . $ebook->Url + ]) +?> +
+
+
+
+

Title) ?>

+ Authors as $author){ ?> + + Name != 'Anonymous'){ ?> +

+ + Name) ?> + + NacoafUrl){ ?> + + + WikipediaUrl){ ?> + + + +

+ + +
+
+ + + +
+ EbookPlaceholder->IsPublicDomain){ ?> +

We don’t have this ebook in our catalog yet.

+

You can sponsor the production of this ebook and we’ll get working on it immediately!

+ EbookPlaceholder->YearPublished !== null){ ?> +

This book was published in EbookPlaceholder->YearPublished ?>, and will therefore enter the U.S. public domain on January 1, EbookPlaceholder->YearPublished + 96 ?>.

+

We can’t work on it any earlier than that.

+ +

This book is not yet in the U.S. public domain. We can’t offer it until it is.

+ +
+
+
+ diff --git a/www/ebook-placeholders/new.php b/www/ebook-placeholders/new.php new file mode 100644 index 00000000..5021c3e1 --- /dev/null +++ b/www/ebook-placeholders/new.php @@ -0,0 +1,62 @@ +Benefits->CanCreateEbookPlaceholders){ + throw new Exceptions\InvalidPermissionsException(); + } + + if($isCreated){ + // We got here because an `Ebook` was successfully created. + http_response_code(Enums\HttpCode::Created->value); + $createdEbook = $ebook; + $ebook = null; + session_unset(); + } + elseif($exception){ + // We got here because an `Ebook` submission had errors and the user has to try again. + http_response_code(Enums\HttpCode::UnprocessableContent->value); + session_unset(); + } +} +catch(Exceptions\LoginRequiredException){ + Template::RedirectToLogin(); +} +catch(Exceptions\InvalidPermissionsException){ + Template::Emit403(); // No permissions to create an ebook placeholder. +} +?> + 'Create an Ebook Placeholder', + 'css' => ['/css/ebook-placeholder.css'], + 'highlight' => '', + 'description' => 'Create a placeholder for an ebook not yet in the collection.' + ] +) ?> +
+
+

Create an Ebook Placeholder

+ + $exception]) ?> + + +

Ebook Placeholder created: Title) ?>

+ + + + $ebook]) ?> + +
+
+ diff --git a/www/ebook-placeholders/post.php b/www/ebook-placeholders/post.php new file mode 100644 index 00000000..e363d8cb --- /dev/null +++ b/www/ebook-placeholders/post.php @@ -0,0 +1,111 @@ +Benefits->CanCreateEbookPlaceholders){ + throw new Exceptions\InvalidPermissionsException(); + } + + $ebook = new Ebook(); + + $title = HttpInput::Str(POST, 'ebook-title'); + if(isset($title)){ + $ebook->Title = $title; + } + + $authors = []; + $authorFields = ['author-name-1', 'author-name-2', 'author-name-3']; + foreach($authorFields as $authorField){ + $authorName = HttpInput::Str(POST, $authorField); + if(!isset($authorName)){ + continue; + } + $author = new Contributor(); + $author->Name = $authorName; + $author->UrlName = Formatter::MakeUrlSafe($author->Name); + $author->MarcRole = Enums\MarcRole::Author; + $authors[] = $author; + } + $ebook->Authors = $authors; + + $translators = []; + $translatorFields = ['translator-name-1', 'translator-name-2']; + foreach($translatorFields as $translatorField){ + $translatorName = HttpInput::Str(POST, $translatorField); + if(!isset($translatorName)){ + continue; + } + $translator = new Contributor(); + $translator->Name = $translatorName; + $translator->UrlName = Formatter::MakeUrlSafe($translator->Name); + $translator->MarcRole = Enums\MarcRole::Translator; + $translators[] = $translator; + } + $ebook->Translators = $translators; + + $collectionMemberships = []; + $collectionNameFields = ['collection-name-1', 'collection-name-2', 'collection-name-3']; + foreach($collectionNameFields as $collectionNameField){ + $collectionName = HttpInput::Str(POST, $collectionNameField); + if(!isset($collectionName)){ + continue; + } + $collectionSequenceNumber = HttpInput::Int(POST, 'sequence-number-' . $collectionNameField); + $collection = Collection::FromName($collectionName); + + $cm = new CollectionMembership(); + $cm->Collection = $collection; + $cm->SequenceNumber = $collectionSequenceNumber; + $collectionMemberships[] = $cm; + } + $ebook->CollectionMemberships = $collectionMemberships; + + $ebookPlaceholder = new EbookPlaceholder(); + $ebookPlaceholder->FillFromHttpPost(); + $ebook->EbookPlaceholder = $ebookPlaceholder; + + $ebook->FillIdentifierFromTitleAndContributors(); + try{ + $existingEbook = Ebook::GetByIdentifier($ebook->Identifier); + throw new Exceptions\DuplicateEbookException($ebook->Identifier); + } + catch(Exceptions\EbookNotFoundException){ + // Pass and create the placeholder. There is no existing ebook with this identifier. + } + + // These properties must be set before calling `Ebook::Create()` to prevent the getters from triggering DB queries or accessing `Ebook::$EbookId` before it is set. + $ebook->Tags = []; + $ebook->LocSubjects = []; + $ebook->Illustrators = []; + $ebook->Contributors = []; + $ebook->Create(); + + $_SESSION['ebook'] = $ebook; + $_SESSION['is-ebook-placeholder-created'] = true; + + http_response_code(Enums\HttpCode::SeeOther->value); + header('Location: /ebook-placeholders/new'); + } +} +catch(Exceptions\LoginRequiredException){ + Template::RedirectToLogin(); +} +catch(Exceptions\InvalidPermissionsException | Exceptions\InvalidHttpMethodException | Exceptions\HttpMethodNotAllowedException){ + Template::Emit403(); +} +catch(Exceptions\AppException $ex){ + $_SESSION['ebook'] = $ebook; + $_SESSION['exception'] = $ex; + + http_response_code(Enums\HttpCode::SeeOther->value); + header('Location: ' . $exceptionRedirectUrl); +} diff --git a/www/ebooks/download.php b/www/ebooks/download.php index 3eb7cb10..c8a3fa2a 100644 --- a/www/ebooks/download.php +++ b/www/ebooks/download.php @@ -13,6 +13,10 @@ try{ $identifier = EBOOKS_IDENTIFIER_PREFIX . $urlPath; $ebook = Ebook::GetByIdentifier($identifier); + if($ebook->IsPlaceholder()){ + throw new Exceptions\InvalidFileException(); + } + $format = Enums\EbookFormatType::tryFrom(HttpInput::Str(GET, 'format') ?? '') ?? Enums\EbookFormatType::Epub; switch($format){ case Enums\EbookFormatType::Kepub: diff --git a/www/ebooks/get.php b/www/ebooks/get.php index deba68aa..c8b5c1e3 100644 --- a/www/ebooks/get.php +++ b/www/ebooks/get.php @@ -19,6 +19,11 @@ try{ $ebook = Ebook::GetByIdentifier($identifier); + if($ebook->IsPlaceholder()){ + require('/standardebooks.org/web/www/ebook-placeholders/get.php'); + exit(); + } + // Divide our sources into transcriptions and scans. foreach($ebook->Sources as $source){ switch($source->Type){ diff --git a/www/ebooks/index.php b/www/ebooks/index.php index 45ed8829..6254efcc 100644 --- a/www/ebooks/index.php +++ b/www/ebooks/index.php @@ -34,7 +34,7 @@ try{ $tags = []; } - $result = Ebook::GetAllByFilter($query != '' ? $query : null, $tags, $sort, $page, $perPage); + $result = Ebook::GetAllByFilter($query != '' ? $query : null, $tags, $sort, $page, $perPage, Enums\EbookReleaseStatusFilter::All); $ebooks = $result['ebooks']; $totalEbooks = $result['ebooksCount']; $pageTitle = 'Browse Standard Ebooks'; diff --git a/www/feeds/atom/search.php b/www/feeds/atom/search.php index e5e1ad97..ca82f6dd 100644 --- a/www/feeds/atom/search.php +++ b/www/feeds/atom/search.php @@ -7,7 +7,7 @@ try{ $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; if($query !== ''){ - $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count)['ebooks']; + $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count, Enums\EbookReleaseStatusFilter::Released)['ebooks']; } } catch(\Exception){ diff --git a/www/feeds/opds/search.php b/www/feeds/opds/search.php index a138f422..63a5bc5f 100644 --- a/www/feeds/opds/search.php +++ b/www/feeds/opds/search.php @@ -7,7 +7,7 @@ try{ $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; if($query !== ''){ - $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count)['ebooks']; + $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count, Enums\EbookReleaseStatusFilter::Released)['ebooks']; } } catch(\Exception){ diff --git a/www/feeds/rss/search.php b/www/feeds/rss/search.php index ee7a4c9f..5ddd169f 100644 --- a/www/feeds/rss/search.php +++ b/www/feeds/rss/search.php @@ -7,7 +7,7 @@ try{ $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; if($query !== ''){ - $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count)['ebooks']; + $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count, Enums\EbookReleaseStatusFilter::Released)['ebooks']; } } catch(\Exception){ diff --git a/www/fonts/fork-awesome-subset.woff2 b/www/fonts/fork-awesome-subset.woff2 index 3c302eef5c237fde00781c61aff3a604324e3819..61e78d74cc90b67ba393929c756675275850378e 100644 GIT binary patch literal 3608 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 literal 4576 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 Kzkea`0000U&c`hP diff --git a/www/newsletter/subscriptions/new.php b/www/newsletter/subscriptions/new.php index 3ad584a6..12fce89b 100644 --- a/www/newsletter/subscriptions/new.php +++ b/www/newsletter/subscriptions/new.php @@ -36,7 +36,7 @@ if($exception){ -