Add placeholders for ebooks

This commit is contained in:
Mike Colagrosso 2024-12-13 11:45:14 -06:00 committed by Alex Cabal
parent cf5f488cae
commit 1ab95df084
52 changed files with 1192 additions and 237 deletions

View file

@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS `Benefits` (
`CanReviewArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanReviewArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanReviewOwnArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanReviewOwnArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanEditUsers` 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`), PRIMARY KEY (`UserId`),
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`) KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -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;

View file

@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS `Ebooks` (
`Identifier` varchar(511) NOT NULL, `Identifier` varchar(511) NOT NULL,
`Created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `Created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`Updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `Updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`WwwFilesystemPath` varchar(511) NOT NULL, `WwwFilesystemPath` varchar(511) NULL,
`RepoFilesystemPath` varchar(511) NOT NULL, `RepoFilesystemPath` varchar(511) NULL,
`KindleCoverUrl` varchar(511) NULL, `KindleCoverUrl` varchar(511) NULL,
`EpubUrl` varchar(511) NULL, `EpubUrl` varchar(511) NULL,
`AdvancedEpubUrl` varchar(511) NULL, `AdvancedEpubUrl` varchar(511) NULL,
@ -14,16 +14,16 @@ CREATE TABLE IF NOT EXISTS `Ebooks` (
`Title` varchar(255) NOT NULL, `Title` varchar(255) NOT NULL,
`FullTitle` varchar(255) NULL, `FullTitle` varchar(255) NULL,
`AlternateTitle` varchar(255) NULL, `AlternateTitle` varchar(255) NULL,
`Description` text NOT NULL, `Description` text NULL,
`LongDescription` text NOT NULL, `LongDescription` text NULL,
`Language` varchar(10) NOT NULL, `Language` varchar(10) NULL,
`WordCount` int(10) unsigned NOT NULL, `WordCount` int(10) unsigned NULL,
`ReadingEase` float NOT NULL, `ReadingEase` float NULL,
`GitHubUrl` varchar(255) NULL, `GitHubUrl` varchar(255) NULL,
`WikipediaUrl` varchar(255) NULL, `WikipediaUrl` varchar(255) NULL,
`EbookCreated` datetime NOT NULL, `EbookCreated` datetime NULL,
`EbookUpdated` datetime NOT NULL, `EbookUpdated` datetime NULL,
`TextSinglePageByteCount` bigint unsigned NOT NULL, `TextSinglePageByteCount` bigint unsigned NULL,
`IndexableText` text NOT NULL, `IndexableText` text NOT NULL,
PRIMARY KEY (`EbookId`), PRIMARY KEY (`EbookId`),
UNIQUE KEY `index1` (`Identifier`), UNIQUE KEY `index1` (`Identifier`),

View file

@ -15,6 +15,7 @@ class Benefits{
public bool $CanReviewArtwork = false; public bool $CanReviewArtwork = false;
public bool $CanReviewOwnArtwork = false; public bool $CanReviewOwnArtwork = false;
public bool $CanEditUsers = false; public bool $CanEditUsers = false;
public bool $CanCreateEbookPlaceholders = false;
protected bool $_HasBenefits; protected bool $_HasBenefits;
@ -27,6 +28,8 @@ class Benefits{
$this->CanReviewOwnArtwork $this->CanReviewOwnArtwork
|| ||
$this->CanEditUsers $this->CanEditUsers
||
$this->CanCreateEbookPlaceholders
){ ){
return true; return true;
} }
@ -58,18 +61,18 @@ class Benefits{
public function Create(): void{ public function Create(): void{
Db::Query(' Db::Query('
INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers) INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers, CanCreateEbookPlaceholders)
values (?, ?, ?, ?, ?, ?, ?, ?) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
', [$this->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]);
} }
public function Save(): void{ public function Save(): void{
Db::Query(' Db::Query('
UPDATE Benefits UPDATE Benefits
set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ? set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ?, CanCreateEbookPlaceholders = ?
where where
UserId = ? 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{ public function FillFromHttpPost(): void{
@ -80,5 +83,6 @@ class Benefits{
$this->PropertyFromHttp('CanReviewArtwork'); $this->PropertyFromHttp('CanReviewArtwork');
$this->PropertyFromHttp('CanReviewOwnArtwork'); $this->PropertyFromHttp('CanReviewOwnArtwork');
$this->PropertyFromHttp('CanEditUsers'); $this->PropertyFromHttp('CanEditUsers');
$this->PropertyFromHttp('CanCreateEbookPlaceholders');
} }
} }

View file

@ -29,7 +29,8 @@ const MANUAL_PATH = WEB_ROOT . '/manual';
const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/'; const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/';
const COVER_ART_UPLOAD_PATH = '/images/cover-uploads/'; 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_DATABASE = 'se';
const DATABASE_DEFAULT_HOST = 'localhost'; const DATABASE_DEFAULT_HOST = 'localhost';

View file

@ -41,6 +41,28 @@ class Contributor{
// METHODS // METHODS
// ******* // *******
/**
* @return array<Contributor>
*/
public static function GetAllAuthorNames(): array{
return Db::Query('
SELECT DISTINCT Name
from Contributors
where MarcRole = "aut"
order by Name asc', [], Contributor::class);
}
/**
* @return array<Contributor>
*/
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 * @throws Exceptions\ValidationException
*/ */

View file

@ -41,14 +41,15 @@ use function Safe\shell_exec;
* @property string $TextSinglePageUrl * @property string $TextSinglePageUrl
* @property string $TextSinglePageSizeFormatted * @property string $TextSinglePageSizeFormatted
* @property string $IndexableText * @property string $IndexableText
* @property EbookPlaceholder $EbookPlaceholder
*/ */
class Ebook{ class Ebook{
use Traits\Accessor; use Traits\Accessor;
public int $EbookId; public int $EbookId;
public string $Identifier; public string $Identifier;
public string $WwwFilesystemPath; public ?string $WwwFilesystemPath = null;
public string $RepoFilesystemPath; public ?string $RepoFilesystemPath = null;
public ?string $KindleCoverUrl = null; public ?string $KindleCoverUrl = null;
public ?string $EpubUrl = null; public ?string $EpubUrl = null;
public ?string $AdvancedEpubUrl = null; public ?string $AdvancedEpubUrl = null;
@ -58,17 +59,17 @@ class Ebook{
public string $Title; public string $Title;
public ?string $FullTitle = null; public ?string $FullTitle = null;
public ?string $AlternateTitle = null; public ?string $AlternateTitle = null;
public string $Description; public ?string $Description = null;
public string $LongDescription; public ?string $LongDescription = null;
public string $Language; public ?string $Language = null;
public int $WordCount; public ?int $WordCount = null;
public float $ReadingEase; public ?float $ReadingEase = null;
public ?string $GitHubUrl = null; public ?string $GitHubUrl = null;
public ?string $WikipediaUrl = null; public ?string $WikipediaUrl = null;
/** When the ebook was published. */ /** When the ebook was published. */
public DateTimeImmutable $EbookCreated; public ?DateTimeImmutable $EbookCreated = null;
/** When the ebook was updated. */ /** When the ebook was updated. */
public DateTimeImmutable $EbookUpdated; public ?DateTimeImmutable $EbookUpdated = null;
/** When the database row was created. */ /** When the database row was created. */
public DateTimeImmutable $Created; public DateTimeImmutable $Created;
/** When the database row was updated. */ /** When the database row was updated. */
@ -116,6 +117,7 @@ class Ebook{
protected string $_TextSinglePageUrl; protected string $_TextSinglePageUrl;
protected string $_TextSinglePageSizeFormatted; protected string $_TextSinglePageSizeFormatted;
protected string $_IndexableText; protected string $_IndexableText;
protected ?EbookPlaceholder $_EbookPlaceholder = null;
// ******* // *******
// GETTERS // GETTERS
@ -318,7 +320,7 @@ class Ebook{
protected function GetUrl(): string{ protected function GetUrl(): string{
if(!isset($this->_Url)){ if(!isset($this->_Url)){
$this->_Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath); $this->_Url = str_replace(EBOOKS_IDENTIFIER_ROOT, '', $this->Identifier);
} }
return $this->_Url; return $this->_Url;
@ -554,7 +556,7 @@ class Ebook{
protected function GetTextSinglePageSizeFormatted(): string{ protected function GetTextSinglePageSizeFormatted(): string{
if(!isset($this->_TextSinglePageSizeFormatted)){ if(!isset($this->_TextSinglePageSizeFormatted)){
$bytes = $this->TextSinglePageByteCount; $bytes = $this->TextSinglePageByteCount;
$sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); $sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$index = 0; $index = 0;
while($bytes >= 1024 && $index < count($sizes) - 1){ while($bytes >= 1024 && $index < count($sizes) - 1){
@ -608,6 +610,18 @@ class Ebook{
return $this->_IndexableText; 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 // ORM METHODS
@ -970,6 +984,59 @@ class Ebook{
return $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<Contributor> $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 // METHODS
@ -996,13 +1063,12 @@ class Ebook{
$error->Add(new Exceptions\EbookIdentifierRequiredException()); $error->Add(new Exceptions\EbookIdentifierRequiredException());
} }
$this->WwwFilesystemPath = trim($this->WwwFilesystemPath ?? '');
if($this->WwwFilesystemPath == ''){
$this->WwwFilesystemPath = null;
}
if(isset($this->WwwFilesystemPath)){ 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){ if(strlen($this->WwwFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook WwwFilesystemPath')); $error->Add(new Exceptions\StringTooLongException('Ebook WwwFilesystemPath'));
} }
@ -1011,17 +1077,13 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookWwwFilesystemPathException($this->WwwFilesystemPath)); $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)){ 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){ if(strlen($this->RepoFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook RepoFilesystemPath')); $error->Add(new Exceptions\StringTooLongException('Ebook RepoFilesystemPath'));
} }
@ -1030,9 +1092,6 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookRepoFilesystemPathException($this->RepoFilesystemPath)); $error->Add(new Exceptions\InvalidEbookRepoFilesystemPathException($this->RepoFilesystemPath));
} }
} }
else{
$error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException());
}
$this->KindleCoverUrl = trim($this->KindleCoverUrl ?? ''); $this->KindleCoverUrl = trim($this->KindleCoverUrl ?? '');
if($this->KindleCoverUrl == ''){ if($this->KindleCoverUrl == ''){
@ -1157,51 +1216,36 @@ class Ebook{
$error->Add(new Exceptions\StringTooLongException('Ebook AlternateTitle')); $error->Add(new Exceptions\StringTooLongException('Ebook AlternateTitle'));
} }
if(isset($this->Description)){ $this->Description = trim($this->Description ?? '');
$this->Description = trim($this->Description); if($this->Description == ''){
$this->Description = null;
if($this->Description == ''){
$error->Add(new Exceptions\EbookDescriptionRequiredException());
}
}
else{
$error->Add(new Exceptions\EbookDescriptionRequiredException());
} }
if(isset($this->LongDescription)){ if(isset($this->Description) && strlen($this->Description) > EBOOKS_MAX_STRING_LENGTH){
$this->LongDescription = trim($this->LongDescription); $error->Add(new Exceptions\StringTooLongException('Ebook Description'));
if($this->LongDescription == ''){
$error->Add(new Exceptions\EbookLongDescriptionRequiredException());
}
} }
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)){ if(isset($this->Language)){
$this->Language = trim($this->Language);
if($this->Language == ''){
$error->Add(new Exceptions\EbookLanguageRequiredException());
}
if(strlen($this->Language) > 10){ if(strlen($this->Language) > 10){
$error->Add(new Exceptions\StringTooLongException('Ebook Language: ' . $this->Language)); $error->Add(new Exceptions\StringTooLongException('Ebook Language: ' . $this->Language));
} }
} }
else{
$error->Add(new Exceptions\EbookLanguageRequiredException());
}
if(isset($this->WordCount)){ if(isset($this->WordCount)){
if($this->WordCount <= 0){ if($this->WordCount <= 0){
$error->Add(new Exceptions\InvalidEbookWordCountException('Invalid Ebook WordCount: ' . $this->WordCount)); $error->Add(new Exceptions\InvalidEbookWordCountException('Invalid Ebook WordCount: ' . $this->WordCount));
} }
} }
else{
$error->Add(new Exceptions\EbookWordCountRequiredException());
}
if(isset($this->ReadingEase)){ if(isset($this->ReadingEase)){
// In theory, Flesch reading ease can be negative, but in practice it's positive. // 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)); $error->Add(new Exceptions\InvalidEbookReadingEaseException('Invalid Ebook ReadingEase: ' . $this->ReadingEase));
} }
} }
else{
$error->Add(new Exceptions\EbookReadingEaseRequiredException());
}
$this->GitHubUrl = trim($this->GitHubUrl ?? ''); $this->GitHubUrl = trim($this->GitHubUrl ?? '');
if($this->GitHubUrl == ''){ if($this->GitHubUrl == ''){
@ -1248,9 +1289,6 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookCreatedDatetimeException($this->EbookCreated)); $error->Add(new Exceptions\InvalidEbookCreatedDatetimeException($this->EbookCreated));
} }
} }
else{
$error->Add(new Exceptions\EbookCreatedDatetimeRequiredException());
}
if(isset($this->EbookUpdated)){ if(isset($this->EbookUpdated)){
if($this->EbookUpdated > NOW){ if($this->EbookUpdated > NOW){
@ -1258,18 +1296,12 @@ class Ebook{
} }
} }
else{
$error->Add(new Exceptions\EbookUpdatedDatetimeRequiredException());
}
if(isset($this->TextSinglePageByteCount)){ if(isset($this->TextSinglePageByteCount)){
if($this->TextSinglePageByteCount <= 0){ if($this->TextSinglePageByteCount <= 0){
$error->Add(new Exceptions\InvalidEbookTextSinglePageByteCountException('Invalid Ebook TextSinglePageByteCount: ' . $this->TextSinglePageByteCount)); $error->Add(new Exceptions\InvalidEbookTextSinglePageByteCountException('Invalid Ebook TextSinglePageByteCount: ' . $this->TextSinglePageByteCount));
} }
} }
else{
$error->Add(new Exceptions\EbookTextSinglePageByteCountRequiredException());
}
if(isset($this->IndexableText)){ if(isset($this->IndexableText)){
$this->IndexableText = trim($this->IndexableText ?? ''); $this->IndexableText = trim($this->IndexableText ?? '');
@ -1282,6 +1314,23 @@ class Ebook{
$error->Add(new Exceptions\EbookIndexableTextRequiredException()); $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){ if($error->HasExceptions){
throw $error; throw $error;
} }
@ -1289,6 +1338,7 @@ class Ebook{
/** /**
* @throws Exceptions\ValidationException * @throws Exceptions\ValidationException
* @throws Exceptions\DuplicateEbookException
*/ */
public function CreateOrUpdate(): void{ public function CreateOrUpdate(): void{
try{ try{
@ -1550,6 +1600,10 @@ class Ebook{
return $string; 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`. * 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\ValidationException
* @throws Exceptions\DuplicateEbookException If an `Ebook` with the given identifier already exists.
*/ */
public function Create(): void{ public function Create(): void{
$this->Validate(); $this->Validate();
try{
Ebook::GetByIdentifier($this->Identifier);
throw new Exceptions\DuplicateEbookException($this->Identifier);
}
catch(Exceptions\EbookNotFoundException){
// Pass.
}
$this->CreateTags(); $this->CreateTags();
$this->CreateLocSubjects(); $this->CreateLocSubjects();
$this->CreateCollections(); $this->CreateCollections();
@ -1623,6 +1686,7 @@ class Ebook{
$this->AddSources(); $this->AddSources();
$this->AddContributors(); $this->AddContributors();
$this->AddTocEntries(); $this->AddTocEntries();
$this->AddEbookPlaceholder();
} }
/** /**
@ -1690,6 +1754,9 @@ class Ebook{
$this->RemoveTocEntries(); $this->RemoveTocEntries();
$this->AddTocEntries(); $this->AddTocEntries();
$this->RemoveEbookPlaceholder();
$this->AddEbookPlaceholder();
} }
private function RemoveTags(): void{ 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 // 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<Ebook> * @return array<Ebook>
*/ */
public static function GetAllByCollection(string $collection): array{ public static function GetAllByCollection(string $collection): array{
@ -1938,13 +2026,16 @@ class Ebook{
inner join CollectionEbooks ce using (EbookId) inner join CollectionEbooks ce using (EbookId)
inner join Collections c using (CollectionId) inner join Collections c using (CollectionId)
where c.UrlName = ? where c.UrlName = ?
order by ce.SequenceNumber, e.EbookCreated desc order by ce.SequenceNumber is null, ce.SequenceNumber, e.EbookCreated desc
', [$collection], Ebook::class); ', [$collection], Ebook::class);
return $ebooks; 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<Ebook> * @return array<Ebook>
*/ */
public static function GetAllByRelated(Ebook $ebook, int $count, ?EbookTag $relatedTag): array{ public static function GetAllByRelated(Ebook $ebook, int $count, ?EbookTag $relatedTag): array{
@ -1955,6 +2046,7 @@ class Ebook{
inner join EbookTags et using (EbookId) inner join EbookTags et using (EbookId)
where et.TagId = ? where et.TagId = ?
and et.EbookId != ? and et.EbookId != ?
and e.WwwFilesystemPath is not null
order by rand() order by rand()
limit ? limit ?
', [$relatedTag->TagId, $ebook->EbookId, $count], Ebook::class); ', [$relatedTag->TagId, $ebook->EbookId, $count], Ebook::class);
@ -1964,6 +2056,7 @@ class Ebook{
SELECT * SELECT *
from Ebooks from Ebooks
where EbookId != ? where EbookId != ?
and WwwFilesystemPath is not null
order by rand() order by rand()
limit ? limit ?
', [$ebook->EbookId, $count], Ebook::class); ', [$ebook->EbookId, $count], Ebook::class);
@ -1977,25 +2070,43 @@ class Ebook{
* *
* @return array{ebooks: array<Ebook>, ebooksCount: int} * @return array{ebooks: array<Ebook>, 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; $limit = $perPage;
$offset = (($page - 1) * $perPage); $offset = (($page - 1) * $perPage);
$joinContributors = ''; $joinContributors = '';
$joinTags = ''; $joinTags = '';
$params = []; $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'; $orderBy = 'e.EbookCreated desc';
if($sort == Enums\EbookSortType::AuthorAlpha){ if($sort == Enums\EbookSortType::AuthorAlpha){
$joinContributors = 'inner join Contributors con using (EbookId)'; $joinContributors = 'inner join Contributors con using (EbookId)';
$whereCondition .= ' and con.MarcRole = "aut"'; $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){ elseif($sort == Enums\EbookSortType::ReadingEase){
$orderBy = 'e.ReadingEase desc'; $orderBy = 'e.ReadingEase desc';
} }
elseif($sort == Enums\EbookSortType::Length){ 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" if(sizeof($tags) > 0 && !in_array('all', $tags)){ // 0 tags means "all ebooks"

88
lib/EbookPlaceholder.php Normal file
View file

@ -0,0 +1,88 @@
<?
/**
* @property bool $IsPublicDomain
*/
class EbookPlaceholder{
use Traits\Accessor;
use Traits\PropertyFromHttp;
public int $EbookId;
public ?int $YearPublished = null;
public bool $IsWanted = false;
public ?Enums\EbookPlaceholderStatus $Status = null;
public ?Enums\EbookPlaceholderDifficulty $Difficulty = null;
public ?string $TranscriptionUrl = null;
public bool $IsPatron = false;
public ?string $Notes = null;
protected bool $_IsPublicDomain;
protected function GetIsPublicDomain(): bool{
if(!isset($this->_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]);
}
}

View file

@ -0,0 +1,8 @@
<?
namespace Enums;
enum EbookPlaceholderDifficulty: string{
case Beginner = 'beginner';
case Intermediate = 'intermediate';
case Advanced = 'advanced';
}

View file

@ -0,0 +1,7 @@
<?
namespace Enums;
enum EbookPlaceholderStatus: string{
case Wanted = 'wanted';
case InProgress = 'in_progress';
}

View file

@ -0,0 +1,8 @@
<?
namespace Enums;
enum EbookReleaseStatusFilter{
case All;
case Placeholder;
case Released;
}

View file

@ -0,0 +1,10 @@
<?
namespace Exceptions;
class DuplicateEbookException extends AppException{
public function __construct(string $identifier){
$this->message = 'Ebook already exists with identifier: ' . $identifier;
parent::__construct($this->message);
}
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookCreatedDatetimeRequiredException extends AppException{
/** @var string $message */
protected $message = 'EbookCreated datetime required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookDescriptionRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook Description required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookLanguageRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook language required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookLongDescriptionRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook LongDescription required.';
}

View file

@ -0,0 +1,7 @@
<?
namespace Exceptions;
class EbookMissingPlaceholderException extends AppException{
/** @var string $message */
protected $message = 'Ebook is a placeholder, but has no placeholder object.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookReadingEaseRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook ReadingEase required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookRepoFilesystemPathRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook RepoFilesystemPath required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookTextSinglePageByteCountRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook TextSinglePageByteCount required.';
}

View file

@ -0,0 +1,7 @@
<?
namespace Exceptions;
class EbookUnexpectedPlaceholderException extends AppException{
/** @var string $message */
protected $message = 'Ebook is not a placeholder, but has a placeholder object.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookUpdatedDatetimeRequiredException extends AppException{
/** @var string $message */
protected $message = 'EbookUpdated datetime required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookWordCountRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook WordCount required.';
}

View file

@ -1,7 +0,0 @@
<?
namespace Exceptions;
class EbookWwwFilesystemPathRequiredException extends AppException{
/** @var string $message */
protected $message = 'Ebook WwwFilesystemPath required.';
}

View file

@ -0,0 +1,5 @@
<?
namespace Exceptions;
class InvalidEbookIdentifierException extends AppException{
}

View file

@ -0,0 +1,7 @@
<?
namespace Exceptions;
class InvalidEbookPlaceholderYearPublishedException extends AppException{
/** @var string $message */
protected $message = 'Invalid ebook placeholder year published.';
}

View file

@ -106,6 +106,10 @@ function CreateZip(string $filePath, array $ebooks, string $type, string $webRoo
// Iterate over all ebooks and arrange them by publication month. // Iterate over all ebooks and arrange them by publication month.
foreach(Ebook::GetAll() as $ebook){ foreach(Ebook::GetAll() as $ebook){
if($ebook->IsPlaceholder()){
continue;
}
$timestamp = $ebook->EbookCreated->format('Y-m'); $timestamp = $ebook->EbookCreated->format('Y-m');
$updatedTimestamp = $ebook->EbookUpdated->getTimestamp(); $updatedTimestamp = $ebook->EbookUpdated->getTimestamp();

View file

@ -100,6 +100,10 @@ foreach($dirs as $dir){
// Iterate over all ebooks to build the various feeds. // Iterate over all ebooks to build the various feeds.
foreach(Ebook::GetAll() as $ebook){ foreach(Ebook::GetAll() as $ebook){
if($ebook->IsPlaceholder()){
continue;
}
$allEbooks[] = $ebook; $allEbooks[] = $ebook;
$newestEbooks[] = $ebook; $newestEbooks[] = $ebook;

View file

@ -12,7 +12,7 @@ $isEditForm = $isEditForm ?? false;
?> ?>
<fieldset> <fieldset>
<legend>Artist details</legend> <legend>Artist details</legend>
<label class="user"> <label class="icon user">
<span>Name</span> <span>Name</span>
<span>For existing artists, leave the year of death blank.</span> <span>For existing artists, leave the year of death blank.</span>
<datalist id="artist-names"> <datalist id="artist-names">
@ -31,7 +31,7 @@ $isEditForm = $isEditForm ?? false;
value="<?= Formatter::EscapeHtml($artwork->Artist->Name) ?>" value="<?= Formatter::EscapeHtml($artwork->Artist->Name) ?>"
/> />
</label> </label>
<label class="year"> <label class="icon year">
<span>Year of death</span> <span>Year of death</span>
<span>If circa or unknown, enter the latest possible year.</span> <span>If circa or unknown, enter the latest possible year.</span>
<? /* Not using `<input type="number">` for now, see <https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/>. */ ?> <? /* Not using `<input type="number">` for now, see <https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/>. */ ?>
@ -46,13 +46,13 @@ $isEditForm = $isEditForm ?? false;
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Artwork details</legend> <legend>Artwork details</legend>
<label class="picture"> <label class="icon picture">
Name Name
<input type="text" name="artwork-name" required="required" <input type="text" name="artwork-name" required="required"
value="<?= Formatter::EscapeHtml($artwork->Name) ?>"/> value="<?= Formatter::EscapeHtml($artwork->Name) ?>"/>
</label> </label>
<fieldset> <fieldset>
<label class="year"> <label class="icon year">
Year of completion Year of completion
<input <input
type="text" type="text"
@ -70,7 +70,7 @@ $isEditForm = $isEditForm ?? false;
/> Year is circa /> Year is circa
</label> </label>
</fieldset> </fieldset>
<label class="tags"> <label class="icon tags">
<span>Tags</span> <span>Tags</span>
<span>A list of comma-separated tags.</span> <span>A list of comma-separated tags.</span>
<input <input
@ -116,7 +116,7 @@ $isEditForm = $isEditForm ?? false;
<span>This book was published in the U.S.</span> <span>This book was published in the U.S.</span>
<span>Yes, if a U.S. city appears anywhere near the publication year or rights statement.</span> <span>Yes, if a U.S. city appears anywhere near the publication year or rights statement.</span>
</label> </label>
<label class="year"> <label class="icon year">
Year of publication Year of publication
<input <input
type="text" type="text"

View file

@ -14,7 +14,7 @@
<? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?> <? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?>
<i> <i>
<a href="<?= $artwork->Ebook->Url ?>"><?= Formatter::EscapeHtml($artwork->Ebook->Title) ?></a> <a href="<?= $artwork->Ebook->Url ?>"><?= Formatter::EscapeHtml($artwork->Ebook->Title) ?></a>
</i> </i><? if($artwork->Ebook->IsPlaceholder()){ ?>(unreleased)<? } ?>
<? }else{ ?> <? }else{ ?>
<code><?= Formatter::EscapeHtml($artwork->EbookUrl) ?></code> (unreleased) <code><?= Formatter::EscapeHtml($artwork->EbookUrl) ?></code> (unreleased)
<? } ?> <? } ?>

View file

@ -12,17 +12,21 @@ $collection = $collection ?? null;
<meta property="schema:name" content="<?= Formatter::EscapeHtml($collection->Name) ?>"/> <meta property="schema:name" content="<?= Formatter::EscapeHtml($collection->Name) ?>"/>
<? } ?> <? } ?>
<? foreach($ebooks as $ebook){ ?> <? foreach($ebooks as $ebook){ ?>
<li typeof="schema:Book"<? if($collection !== null){ ?> resource="<?= $ebook->Url ?>" property="schema:hasPart"<? if($ebook->GetCollectionPosition($collection) !== null){ ?> value="<?= $ebook->GetCollectionPosition($collection) ?>"<? } ?><? }else{ ?> about="<?= $ebook->Url ?>"<? } ?>> <li typeof="schema:Book"<? if($collection !== null){ ?> resource="<?= $ebook->Url ?>" property="schema:hasPart"<? if($ebook->GetCollectionPosition($collection) !== null){ ?> value="<?= $ebook->GetCollectionPosition($collection) ?>"<? } ?><? }else{ ?> about="<?= $ebook->Url ?>"<? } ?><? if($ebook->EbookPlaceholder?->IsWanted){ ?> class="wanted"<? } ?>>
<? if($collection !== null && $ebook->GetCollectionPosition($collection) !== null){ ?> <? if($collection !== null && $ebook->GetCollectionPosition($collection) !== null){ ?>
<meta property="schema:position" content="<?= $ebook->GetCollectionPosition($collection) ?>"/> <meta property="schema:position" content="<?= $ebook->GetCollectionPosition($collection) ?>"/>
<? } ?> <? } ?>
<div class="thumbnail-container" aria-hidden="true"><? /* We need a container in case the thumb is shorter than the description, so that the focus outline doesn't take up the whole grid space */ ?> <div class="thumbnail-container" aria-hidden="true"><? /* We need a container in case the thumb is shorter than the description, so that the focus outline doesn't take up the whole grid space */ ?>
<a href="<?= $ebook->Url ?>" tabindex="-1" property="schema:url"<? if($collection !== null && $ebook->GetCollectionPosition($collection) !== null){ ?> data-ordinal="<?= $ebook->GetCollectionPosition($collection) ?>"<? } ?>> <a href="<?= $ebook->Url ?>" tabindex="-1" property="schema:url"<? if($collection !== null && $ebook->GetCollectionPosition($collection) !== null){ ?> data-ordinal="<?= $ebook->GetCollectionPosition($collection) ?>"<? } ?>>
<picture> <? if($ebook->IsPlaceholder()){ ?>
<? if($ebook->CoverImage2xAvifUrl !== null){ ?><source srcset="<?= $ebook->CoverImage2xAvifUrl ?> 2x, <?= $ebook->CoverImageAvifUrl ?> 1x" type="image/avif"/><? } ?> <div class="placeholder-cover"></div><? /* Don't self-close as this changes how Chrome renders */ ?>
<source srcset="<?= $ebook->CoverImage2xUrl ?> 2x, <?= $ebook->CoverImageUrl ?> 1x" type="image/jpg"/> <? }else{ ?>
<img src="<?= $ebook->CoverImage2xUrl ?>" alt="" property="schema:image" height="335" width="224"/> <picture>
</picture> <? if($ebook->CoverImage2xAvifUrl !== null){ ?><source srcset="<?= $ebook->CoverImage2xAvifUrl ?> 2x, <?= $ebook->CoverImageAvifUrl ?> 1x" type="image/avif"/><? } ?>
<source srcset="<?= $ebook->CoverImage2xUrl ?> 2x, <?= $ebook->CoverImageUrl ?> 1x" type="image/jpg"/>
<img src="<?= $ebook->CoverImage2xUrl ?>" alt="" property="schema:image" height="335" width="224"/>
</picture>
<? } ?>
</a> </a>
</div> </div>
<p><a href="<?= $ebook->Url ?>" property="schema:url"><span property="schema:name"><?= Formatter::EscapeHtml($ebook->Title) ?></span></a></p> <p><a href="<?= $ebook->Url ?>" property="schema:url"><span property="schema:name"><?= Formatter::EscapeHtml($ebook->Title) ?></span></a></p>
@ -42,8 +46,16 @@ $collection = $collection ?? null;
<p><?= rtrim($ebook->ContributorsHtml, '.') ?></p> <p><?= rtrim($ebook->ContributorsHtml, '.') ?></p>
</div> </div>
<? } ?> <? } ?>
<p><?= number_format($ebook->WordCount) ?> words • <?= $ebook->ReadingEase ?> reading ease</p> <? if(!$ebook->IsPlaceholder()){ ?>
<ul class="tags"><? foreach($ebook->Tags as $tag){ ?><li><a href="<?= $tag->Url ?>"><?= Formatter::EscapeHtml($tag->Name) ?></a></li><? } ?></ul> <p><?= number_format($ebook->WordCount) ?> words • <?= $ebook->ReadingEase ?> reading ease</p>
<ul class="tags">
<? foreach($ebook->Tags as $tag){ ?>
<li>
<a href="<?= $tag->Url ?>"><?= Formatter::EscapeHtml($tag->Name) ?></a>
</li>
<? } ?>
</ul>
<? } ?>
</div> </div>
<? } ?> <? } ?>
</li> </li>

View file

@ -0,0 +1,223 @@
<?
/**
* @var ?Ebook $ebook
*/
$ebook = $ebook ?? new Ebook();
?>
<fieldset>
<legend>Contributors</legend>
<label class="icon user">
<span>Author</span>
<datalist id="author-names">
<? foreach(Contributor::GetAllAuthorNames() as $author){ ?>
<option value="<?= Formatter::EscapeHtml($author->Name) ?>"><?= Formatter::EscapeHtml($author->Name) ?></option>
<? } ?>
</datalist>
<input
type="text"
name="author-name-1"
list="author-names"
required="required"
value="<? if(isset($ebook->Authors) && sizeof($ebook->Authors) > 0){ ?><?= Formatter::EscapeHtml($ebook->Authors[0]->Name) ?><? } ?>"
/>
</label>
</fieldset>
<details>
<summary>Additional contributors</summary>
<fieldset>
<label class="icon user">
<span>Second author</span>
<input
type="text"
name="author-name-2"
list="author-names"
value="<? if(isset($ebook->Authors) && sizeof($ebook->Authors) > 1){ ?><?= Formatter::EscapeHtml($ebook->Authors[1]->Name) ?><? } ?>"
/>
</label>
<label class="icon user">
<span>Third author</span>
<input
type="text"
name="author-name-3"
list="author-names"
value="<? if(isset($ebook->Authors) && sizeof($ebook->Authors) > 2){ ?><?= Formatter::EscapeHtml($ebook->Authors[2]->Name) ?><? } ?>"
/>
</label>
<label class="icon user">
<span>Translator</span>
<datalist id="translator-names">
<? foreach(Contributor::GetAllTranslatorNames() as $translator){ ?>
<option value="<?= Formatter::EscapeHtml($translator->Name) ?>"><?= Formatter::EscapeHtml($translator->Name) ?></option>
<? } ?>
</datalist>
<input
type="text"
name="translator-name-1"
list="translator-names"
value="<? if(isset($ebook->Translators) && sizeof($ebook->Translators) > 0){ ?><?= Formatter::EscapeHtml($ebook->Translators[0]->Name) ?><? } ?>"
/>
</label>
<label class="icon user">
<span>Second translator</span>
<input
type="text"
name="translator-name-2"
list="translator-names"
value="<? if(isset($ebook->Translators) && sizeof($ebook->Translators) > 1){ ?><?= Formatter::EscapeHtml($ebook->Translators[1]->Name) ?><? } ?>"
/>
</label>
</fieldset>
</details>
<fieldset>
<legend>Ebook metadata</legend>
<label class="icon book">
<span>Title</span>
<input type="text" name="ebook-title" required="required"
value="<? if(isset($ebook->Title)){ ?><?= Formatter::EscapeHtml($ebook->Title) ?><? } ?>"/>
</label>
<fieldset>
<label class="icon year">
Year published
<input
type="text"
name="ebook-placeholder-year-published"
inputmode="numeric"
pattern="[0-9]{1,4}"
value="<? if(isset($ebook->EbookPlaceholder)){ ?><?= Formatter::EscapeHtml((string)$ebook->EbookPlaceholder->YearPublished) ?><? } ?>"
/>
</label>
</fieldset>
<label class="icon collection">
<span>Collection</span>
<datalist id="collection-names">
<? foreach(Collection::GetAll() as $collection){ ?>
<option value="<?= Formatter::EscapeHtml($collection->Name) ?>"><?= Formatter::EscapeHtml($collection->Name) ?></option>
<? } ?>
</datalist>
<input
type="text"
name="collection-name-1"
list="collection-names"
value="<? if(isset($ebook->CollectionMemberships)){ ?><?= Formatter::EscapeHtml($ebook->CollectionMemberships[0]->Collection->Name) ?><? } ?>"
/>
</label>
<fieldset>
<label class="icon ordered-list">
<span>Number in collection</span>
<input
type="text"
name="sequence-number-collection-name-1"
inputmode="numeric"
pattern="[0-9]{1,3}"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 0){ ?><?= Formatter::EscapeHtml((string)$ebook->CollectionMemberships[0]->SequenceNumber) ?><? } ?>"
/>
</label>
</fieldset>
</fieldset>
<details>
<summary>Additional collections</summary>
<fieldset>
<label class="icon collection">
<span>Second Collection</span>
<input
type="text"
name="collection-name-2"
list="collection-names"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 1){ ?><?= Formatter::EscapeHtml($ebook->CollectionMemberships[1]->Collection->Name) ?><? } ?>"
/>
</label>
<fieldset>
<label class="icon ordered-list">
<span>Number in collection</span>
<input
type="text"
name="sequence-number-collection-name-2"
inputmode="numeric"
pattern="[0-9]{1,3}"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 1){ ?><?= Formatter::EscapeHtml((string)$ebook->CollectionMemberships[1]->SequenceNumber) ?><? } ?>"
/>
</label>
</fieldset>
</fieldset>
<fieldset>
<label class="icon collection">
<span>Third Collection</span>
<input
type="text"
name="collection-name-3"
list="collection-names"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 2){ ?><?= Formatter::EscapeHtml($ebook->CollectionMemberships[2]->Collection->Name) ?><? } ?>"
/>
</label>
<fieldset>
<label class="icon ordered-list">
<span>Number in collection</span>
<input
type="text"
name="sequence-number-collection-name-3"
inputmode="numeric"
pattern="[0-9]{1,3}"
value="<? if(isset($ebook->CollectionMemberships) && sizeof($ebook->CollectionMemberships) > 2){ ?><?= Formatter::EscapeHtml((string)$ebook->CollectionMemberships[2]->SequenceNumber) ?><? } ?>"
/>
</label>
</fieldset>
</fieldset>
</details>
<fieldset>
<legend>Wanted list</legend>
<label class="controls-following-fieldset">
<span>On the wanted list?</span>
<input
type="checkbox"
name="ebook-placeholder-is-wanted"
<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->IsWanted){ ?>checked="checked"<? } ?>
/>
</label>
<fieldset>
<label>
<span>Did a Patron request this book?</span>
<input
type="checkbox"
name="ebook-placeholder-is-patron"
<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->IsPatron){ ?>checked="checked"<? } ?>
/>
</label>
<label class="icon meter">
<span>Difficulty</span>
<span>
<select name="ebook-placeholder-difficulty">
<option value=""></option>
<option value="<?= Enums\EbookPlaceholderDifficulty::Beginner->value ?>"<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->Difficulty == Enums\EbookPlaceholderDifficulty::Beginner){ ?> selected="selected"<? } ?>>Beginner</option>
<option value="<?= Enums\EbookPlaceholderDifficulty::Intermediate->value ?>"<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->Difficulty == Enums\EbookPlaceholderDifficulty::Intermediate){ ?> selected="selected"<? } ?>>Intermediate</option>
<option value="<?= Enums\EbookPlaceholderDifficulty::Advanced->value ?>"<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->Difficulty == Enums\EbookPlaceholderDifficulty::Advanced){ ?> selected="selected"<? } ?>>Advanced</option>
</select>
</span>
</label>
<label class="icon hourglass">
<span>Wanted list status</span>
<span>
<select name="ebook-placeholder-status">
<option value="<?= Enums\EbookPlaceholderStatus::Wanted->value ?>"<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->Status == Enums\EbookPlaceholderStatus::Wanted){ ?> selected="selected"<? } ?>>Wanted</option>
<option value="<?= Enums\EbookPlaceholderStatus::InProgress->value ?>"<? if(isset($ebook->EbookPlaceholder) && $ebook->EbookPlaceholder->Status == Enums\EbookPlaceholderStatus::InProgress){ ?> selected="selected"<? } ?>>In progress</option>
</select>
</span>
</label>
<label>
<span>Transcription URL</span>
<input
type="url"
name="ebook-placeholder-transcription-url"
value="<? if(isset($ebook->EbookPlaceholder)){ ?><?= Formatter::EscapeHtml($ebook->EbookPlaceholder->TranscriptionUrl) ?><? } ?>"
/>
</label>
<label>
<span>Notes</span>
<span>Markdown accepted.</span>
<textarea maxlength="1024" name="ebook-placeholder-notes"><? if(isset($ebook->EbookPlaceholder)){ ?><?= Formatter::EscapeHtml($ebook->EbookPlaceholder->Notes) ?><? } ?></textarea>
</label>
</fieldset>
</fieldset>
<div class="footer">
<button>Submit</button>
</div>

View file

@ -9,12 +9,12 @@
$isAllSelected = sizeof($tags) == 0 || in_array('all', $tags); $isAllSelected = sizeof($tags) == 0 || in_array('all', $tags);
?> ?>
<form action="/ebooks" method="get" rel="search"> <form action="/ebooks" method="get" rel="search">
<label class="tags">Subjects <label class="icon tags">Subjects
<select <? if(!Template::IsEreaderBrowser()){ ?> multiple="multiple"<? } ?> name="tags[]" size="1"> <select <? if(!Template::IsEreaderBrowser()){ ?> multiple="multiple"<? } ?> name="tags[]" size="1">
<option value="all">All</option> <option value="all">All</option>
<? foreach(EbookTag::GetAll() as $tag){ ?> <? foreach(EbookTag::GetAll() as $tag){ ?>
<option value="<?= $tag->UrlName ?>"<? if(!$isAllSelected && in_array($tag->UrlName, $tags)){ ?> selected="selected"<? } ?>><?= Formatter::EscapeHtml($tag->Name) ?></option> <option value="<?= $tag->UrlName ?>"<? if(!$isAllSelected && in_array($tag->UrlName, $tags)){ ?> selected="selected"<? } ?>><?= Formatter::EscapeHtml($tag->Name) ?></option>
<? } ?> <? } ?>
</select> </select>
</label> </label>
<label>Keywords <label>Keywords

View file

@ -15,7 +15,7 @@ $passwordAction = $passwordAction ?? Enums\PasswordActionType::None;
/> />
</label> </label>
<label class="user"> <label class="icon user">
Name Name
<input <input
type="text" type="text"
@ -135,6 +135,13 @@ $passwordAction = $passwordAction ?? Enums\PasswordActionType::None;
Can edit users Can edit users
</label> </label>
</li> </li>
<li>
<label>
<input type="hidden" name="benefits-can-create-ebook-placeholder" value="false" />
<input type="checkbox" name="benefits-can-create-ebook-placeholder" value="true"<? if($user->Benefits->CanCreateEbookPlaceholders){ ?> checked="checked"<? } ?> />
Can create ebook placeholders
</label>
</li>
</ul> </ul>
</fieldset> </fieldset>

View file

@ -102,7 +102,15 @@ catch(Exceptions\InvalidPermissionsException){
</tr> </tr>
<tr> <tr>
<td>Tags</td> <td>Tags</td>
<td><ul class="tags"><? foreach($artwork->Tags as $tag){ ?><li><a href="<?= $tag->Url ?>"><?= Formatter::EscapeHtml($tag->Name) ?></a></li><? } ?></ul></td> <td>
<ul class="tags">
<? foreach($artwork->Tags as $tag){ ?>
<li>
<a href="<?= $tag->Url ?>"><?= Formatter::EscapeHtml($tag->Name) ?></a>
</li>
<? } ?>
</ul>
</td>
</tr> </tr>
<tr> <tr>
<td>Dimensions</td> <td>Dimensions</td>

View file

@ -16,15 +16,14 @@ try{
throw new Exceptions\InvalidPermissionsException(); throw new Exceptions\InvalidPermissionsException();
} }
// We got here because an artwork was successfully submitted.
if($isCreated){ if($isCreated){
// We got here because an `Artwork` was successfully submitted.
http_response_code(Enums\HttpCode::Created->value); http_response_code(Enums\HttpCode::Created->value);
$artwork = null; $artwork = null;
session_unset(); session_unset();
} }
elseif($exception){
// We got here because an artwork submission had errors and the user has to try again. // We got here because an `Artwork` submission had errors and the user has to try again.
if($exception){
http_response_code(Enums\HttpCode::UnprocessableContent->value); http_response_code(Enums\HttpCode::UnprocessableContent->value);
session_unset(); session_unset();
} }

View file

@ -166,6 +166,10 @@ form[action^="/artworks/"]{
grid-column: 1 / span 4; grid-column: 1 / span 4;
} }
.year + label:has(input[type="checkbox"]){
margin-top: .5rem;
}
label.picture::before{ label.picture::before{
bottom: 2.6rem; /* Fix alignment caused by "circa" checkbox */ 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; 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{ form.create-update-artwork label{
display: block; display: block;
} }

View file

@ -840,7 +840,8 @@ label:has(input[type="checkbox"]):focus-within,
select:focus, select:focus,
button:focus, button:focus,
nav a[rel]:focus, nav a[rel]:focus,
textarea:focus{ textarea:focus,
summary:focus{
outline: 1px dashed var(--input-outline); outline: 1px dashed var(--input-outline);
} }
@ -1572,6 +1573,19 @@ ol.ebooks-list > li p.author a{
justify-content: center; 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, article nav ol,
main nav.pagination ol{ main nav.pagination ol{
list-style: none; list-style: none;
@ -1790,12 +1804,9 @@ input[type="file"]{
label:has(input[type="email"]) input, label:has(input[type="email"]) input,
label:has(input[type="search"]) input, label:has(input[type="search"]) input,
label:has(input[type="url"]) input, label:has(input[type="url"]) input,
label.captcha input, label.icon input,
label.year input, label:has(input[type="password"]) input,
label.tags input, label.icon select{
label.picture input,
label.user input,
label:has(input[type="password"]) input{
padding-left: 2.5rem; padding-left: 2.5rem;
} }
@ -1850,8 +1861,8 @@ label:has(input[type="radio"]):has(> span) input,
label:has(input[type="checkbox"]):has(> span) input{ label:has(input[type="checkbox"]):has(> span) input{
grid-row: 1 / span 2; grid-row: 1 / span 2;
justify-self: center; justify-self: center;
align-self: start; align-self: center;
margin-top: 10px; margin: 0;
margin-right: .5rem; margin-right: .5rem;
} }
@ -1895,11 +1906,7 @@ label:has(select) > span + span,
label:has(input[type="email"]), label:has(input[type="email"]),
label:has(input[type="search"]), label:has(input[type="search"]),
label:has(input[type="url"]), label:has(input[type="url"]),
label.captcha, label.icon,
label.year,
label.tags,
label.picture,
label.user,
label:has(input[type="password"]){ label:has(input[type="password"]){
display: block; display: block;
position: relative; position: relative;
@ -1908,11 +1915,7 @@ label:has(input[type="password"]){
label:has(input[type="email"])::before, label:has(input[type="email"])::before,
label:has(input[type="search"])::before, label:has(input[type="search"])::before,
label.captcha::before, label.icon::before,
label.year::before,
label.tags::before,
label.picture::before,
label.user::before,
label:has(input[type="url"])::before, label:has(input[type="url"])::before,
label:has(input[type="password"])::before{ label:has(input[type="password"])::before{
display: block; display: block;
@ -1954,6 +1957,26 @@ label.year::before{
content: "\f073"; 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{ label.tags:not(:has(select))::before{
content: "\f02c"; content: "\f02c";
} }
@ -2489,15 +2512,37 @@ form[action="/newsletter/subscriptions"] fieldset{
fieldset{ fieldset{
padding: 0; padding: 0;
margin: 0;
border: none; border: none;
} }
fieldset p, label.controls-following-fieldset + fieldset,
fieldset legend{ details summary ~ *{
margin-left: 1rem;
}
fieldset p{
border-bottom: 1px dashed var(--input-border); 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){ fieldset p:has(+ ul){
font-weight: bold; font-weight: bold;
} }
@ -2604,8 +2649,8 @@ aside header{
font-size: 1.5rem; font-size: 1.5rem;
} }
.meter, div.meter,
.progress{ div.progress{
position: relative; position: relative;
font-size: 0; font-size: 0;
} }
@ -2677,13 +2722,13 @@ aside header{
border-top: 1px solid #000; border-top: 1px solid #000;
} }
.progress + p{ div.progress + p{
margin-top: 2rem; margin-top: 2rem;
hyphens: auto; hyphens: auto;
} }
.meter p, div.meter p,
.progress p{ div.progress p{
font-size: 1rem; font-size: 1rem;
font-family: "League Spartan", Arial, sans-serif; font-family: "League Spartan", Arial, sans-serif;
left: 0; left: 0;
@ -2701,7 +2746,7 @@ aside header{
margin-top: 0; margin-top: 0;
} }
.progress p.start{ div.progress p.start{
margin-right: auto; margin-right: auto;
margin-left: 1rem; margin-left: 1rem;
border: none; border: none;
@ -2710,7 +2755,7 @@ aside header{
font-size: .75rem; font-size: .75rem;
} }
.progress p.target{ div.progress p.target{
margin-left: auto; margin-left: auto;
margin-right: 1rem; margin-right: 1rem;
border: none; border: none;
@ -2719,8 +2764,8 @@ aside header{
font-size: .75rem; font-size: .75rem;
} }
.meter > div, div.meter > div,
.progress > div{ div.progress > div{
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -3121,6 +3166,74 @@ form[action="/settings"] label{
display: inline-block; 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 */ @media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
/* For iPad, unset the height so it matches the other elements */ /* For iPad, unset the height so it matches the other elements */
select[multiple]{ select[multiple]{
@ -3717,13 +3830,13 @@ form[action="/settings"] label{
line-height: 1.4; line-height: 1.4;
} }
.progress p.start, div.progress p.start,
.progress p.target, div.progress p.target,
.progress p.stretch-base{ div.progress p.stretch-base{
display: none; display: none;
} }
.progress p{ div.progress p{
border: none; border: none;
background: transparent; background: transparent;
} }

View file

@ -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;
}

View file

@ -349,7 +349,6 @@ code.full .utf{
} }
.step-by-step-guide summary{ .step-by-step-guide summary{
cursor: pointer;
/* reproduce H2 styling: */ /* reproduce H2 styling: */
font-size: 1.4rem; font-size: 1.4rem;
font-family: "League Spartan", Arial, sans-serif; font-family: "League Spartan", Arial, sans-serif;

View file

@ -0,0 +1,88 @@
<?
use function Safe\preg_replace;
/** @var string $identifier Passed from script this is included from. */
$ebook = null;
try{
$ebook = Ebook::GetByIdentifier($identifier);
}
catch(Exceptions\EbookNotFoundException){
Template::Emit404();
}
?><?= Template::Header(
[
'title' => strip_tags($ebook->TitleWithCreditsHtml),
'css' => ['/css/ebook-placeholder.css'],
'highlight' => 'ebooks',
'canonicalUrl' => SITE_URL . $ebook->Url
])
?>
<main>
<article class="ebook ebook-placeholder" typeof="schema:Book" about="<?= $ebook->Url ?>">
<header>
<hgroup>
<h1 property="schema:name"><?= Formatter::EscapeHtml($ebook->Title) ?></h1>
<? foreach($ebook->Authors as $author){ ?>
<?
/* We include the `resource` attr here because we can have multiple authors, and in that case their href URLs will link to their combined corpus.
For example, William Wordsworth & Samuel Coleridge will both link to `/ebooks/william-wordsworth_samuel-taylor-coleridge`.
But, each author is an individual, so we have to differentiate them in RDFa with `resource`.
*/ ?>
<? if($author->Name != 'Anonymous'){ ?>
<h2>
<a property="schema:author" typeof="schema:Person" href="<?= Formatter::EscapeHtml($ebook->AuthorsUrl) ?>" resource="<?= '/ebooks/' . $author->UrlName ?>">
<span property="schema:name"><?= Formatter::EscapeHtml($author->Name) ?></span>
<meta property="schema:url" content="<?= SITE_URL . Formatter::EscapeHtml($ebook->AuthorsUrl) ?>"/>
<? if($author->NacoafUrl){ ?>
<meta property="schema:sameAs" content="<?= Formatter::EscapeHtml($author->NacoafUrl) ?>"/>
<? } ?>
<? if($author->WikipediaUrl){ ?>
<meta property="schema:sameAs" content="<?= Formatter::EscapeHtml($author->WikipediaUrl) ?>"/>
<? } ?>
</a>
</h2>
<? } ?>
<? } ?>
</hgroup>
</header>
<aside id="reading-ease">
<? if($ebook->ContributorsHtml != ''){ ?>
<p><?= $ebook->ContributorsHtml ?></p>
<? } ?>
<? if(sizeof($ebook->CollectionMemberships) > 0){ ?>
<? foreach($ebook->CollectionMemberships as $collectionMembership){ ?>
<? $collection = $collectionMembership->Collection; ?>
<? $sequenceNumber = $collectionMembership->SequenceNumber; ?>
<p>
<? if($sequenceNumber !== null){ ?>№ <?= number_format($sequenceNumber) ?> in the<? }else{ ?>Part of the<? } ?> <a href="<?= $collection->Url ?>" property="schema:isPartOf"><?= Formatter::EscapeHtml(preg_replace('/^The /ius', '', (string)$collection->Name)) ?></a>
<? if($collection->Type !== null){ ?>
<? if(substr_compare(mb_strtolower($collection->Name), mb_strtolower($collection->Type->value), -strlen(mb_strtolower($collection->Type->value))) !== 0){ ?>
<?= $collection->Type->value ?>.
<? } ?>
<? }else{ ?>
collection.
<? } ?>
</p>
<? } ?>
<? } ?>
</aside>
<section class="placeholder-details">
<? if($ebook->EbookPlaceholder->IsPublicDomain){ ?>
<p>We dont have this ebook in our catalog yet.</p>
<p>You can <a href="/donate#sponsor-an-ebook">sponsor the production of this ebook</a> and well get working on it immediately!</p>
<? }elseif($ebook->EbookPlaceholder->YearPublished !== null){ ?>
<p>This book was published in <?= $ebook->EbookPlaceholder->YearPublished ?>, and will therefore enter the U.S. public domain on <b>January 1, <?= $ebook->EbookPlaceholder->YearPublished + 96 ?>.</b></p>
<p>We cant work on it any earlier than that.</p>
<? }else{ ?>
<p>This book is not yet in the U.S. public domain. We cant offer it until it is.</p>
<? } ?>
</section>
</article>
</main>
<?= Template::Footer() ?>

View file

@ -0,0 +1,62 @@
<?
use function Safe\session_unset;
session_start();
$isCreated = HttpInput::Bool(SESSION, 'is-ebook-placeholder-created') ?? false;
$exception = HttpInput::SessionObject('exception', Exceptions\AppException::class);
$ebook = HttpInput::SessionObject('ebook', Ebook::class);
try{
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->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.
}
?>
<?= Template::Header(
[
'title' => 'Create an Ebook Placeholder',
'css' => ['/css/ebook-placeholder.css'],
'highlight' => '',
'description' => 'Create a placeholder for an ebook not yet in the collection.'
]
) ?>
<main>
<section class="narrow">
<h1>Create an Ebook Placeholder</h1>
<?= Template::Error(['exception' => $exception]) ?>
<? if($isCreated && isset($createdEbook)){ ?>
<p class="message success">Ebook Placeholder created: <a href="<?= $createdEbook->Url ?>"><?= Formatter::EscapeHtml($createdEbook->Title) ?></a></p>
<? } ?>
<form class="create-update-ebook-placeholder" method="<?= Enums\HttpMethod::Post->value ?>" action="/ebook-placeholders" autocomplete="off">
<?= Template::EbookPlaceholderForm(['ebook' => $ebook]) ?>
</form>
</section>
</main>
<?= Template::Footer() ?>

View file

@ -0,0 +1,111 @@
<?
try{
session_start();
$httpMethod = HttpInput::ValidateRequestMethod([Enums\HttpMethod::Post]);
$exceptionRedirectUrl = '/ebook-placeholders/new';
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
// POSTing a new ebook placeholder.
if($httpMethod == Enums\HttpMethod::Post){
if(!Session::$User->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);
}

View file

@ -13,6 +13,10 @@ try{
$identifier = EBOOKS_IDENTIFIER_PREFIX . $urlPath; $identifier = EBOOKS_IDENTIFIER_PREFIX . $urlPath;
$ebook = Ebook::GetByIdentifier($identifier); $ebook = Ebook::GetByIdentifier($identifier);
if($ebook->IsPlaceholder()){
throw new Exceptions\InvalidFileException();
}
$format = Enums\EbookFormatType::tryFrom(HttpInput::Str(GET, 'format') ?? '') ?? Enums\EbookFormatType::Epub; $format = Enums\EbookFormatType::tryFrom(HttpInput::Str(GET, 'format') ?? '') ?? Enums\EbookFormatType::Epub;
switch($format){ switch($format){
case Enums\EbookFormatType::Kepub: case Enums\EbookFormatType::Kepub:

View file

@ -19,6 +19,11 @@ try{
$ebook = Ebook::GetByIdentifier($identifier); $ebook = Ebook::GetByIdentifier($identifier);
if($ebook->IsPlaceholder()){
require('/standardebooks.org/web/www/ebook-placeholders/get.php');
exit();
}
// Divide our sources into transcriptions and scans. // Divide our sources into transcriptions and scans.
foreach($ebook->Sources as $source){ foreach($ebook->Sources as $source){
switch($source->Type){ switch($source->Type){

View file

@ -34,7 +34,7 @@ try{
$tags = []; $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']; $ebooks = $result['ebooks'];
$totalEbooks = $result['ebooksCount']; $totalEbooks = $result['ebooksCount'];
$pageTitle = 'Browse Standard Ebooks'; $pageTitle = 'Browse Standard Ebooks';

View file

@ -7,7 +7,7 @@ try{
$count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE;
if($query !== ''){ 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){ catch(\Exception){

View file

@ -7,7 +7,7 @@ try{
$count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE;
if($query !== ''){ 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){ catch(\Exception){

View file

@ -7,7 +7,7 @@ try{
$count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE;
if($query !== ''){ 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){ catch(\Exception){

Binary file not shown.

View file

@ -36,7 +36,7 @@ if($exception){
<label>Your email address <label>Your email address
<input type="email" name="email" value="<?= Formatter::EscapeHtml($subscription->User->Email ?? '') ?>" maxlength="80" required="required" /> <input type="email" name="email" value="<?= Formatter::EscapeHtml($subscription->User->Email ?? '') ?>" maxlength="80" required="required" />
</label> </label>
<label class="captcha"> <label class="icon captcha">
Type the letters in the <abbr class="acronym">CAPTCHA</abbr> image Type the letters in the <abbr class="acronym">CAPTCHA</abbr> image
<div> <div>
<input type="text" name="captcha" required="required" autocomplete="off" /> <input type="text" name="captcha" required="required" autocomplete="off" />

View file

@ -169,6 +169,10 @@ catch(Exceptions\SeeOtherException $ex){
<td>Can edit users:</td> <td>Can edit users:</td>
<td><? if($user->Benefits->CanEditUsers){ ?>☑<? }else{ ?>☐<? } ?></td> <td><? if($user->Benefits->CanEditUsers){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr> </tr>
<tr>
<td>Can create ebook placeholders:</td>
<td><? if($user->Benefits->CanCreateEbookPlaceholders){ ?>☑<? }else{ ?>☐<? } ?></td>
</tr>
<? } ?> <? } ?>
</tbody> </tbody>
</table> </table>