Add books admin page.

This commit is contained in:
2020-08-07 01:13:30 +01:00
parent e3f2557796
commit 8c2b355646
15 changed files with 1225 additions and 1 deletions

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Version>4.0.0.1790</Version> <Version>4.0.0.1792</Version>
<Company>Canary Islands Computer Museum</Company> <Company>Canary Islands Computer Museum</Company>
<Copyright>Copyright © 2003-2020 Natalia Portillo</Copyright> <Copyright>Copyright © 2003-2020 Natalia Portillo</Copyright>
<Product>Canary Islands Computer Museum Website</Product> <Product>Canary Islands Computer Museum Website</Product>

View File

@@ -0,0 +1,110 @@
@{
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2020 Natalia Portillo
*******************************************************************************/
}
@page "/admin/books"
@using Marechai.Database.Models
@inherits OwningComponentBase<BooksService>
@inject IStringLocalizer<BooksService> L
@inject Microsoft.AspNetCore.Identity.UserManager<ApplicationUser> UserManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@attribute [Authorize(Roles = "UberAdmin, Admin")]
<h3>@L["Books"]</h3>
@if (_books is null)
{
<p>@L["Loading..."]</p>
return;
}
<p>
<a class="btn btn-primary" href="/admin/books/create">@L["Create new"]</a>
</p>
<table class="table table-striped">
<thead>
<tr>
<th>
@L["Title"]
</th>
<th>
@L["Native title"]
</th>
<th>
@L["Published"]
</th>
<th>
@L["Country"]
</th>
<th>
@L["ISBN"]
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in _books)
{
<tr>
<td>
@item.Title
</td>
<td>
@item.NativeTitle
</td>
<td>
@($"{item.Published:d}")
</td>
<td>
@item.Country
</td>
<td>
@item.Isbn
</td>
<td>
<a class="btn btn-primary" href="/admin/books/details/@item.Id">@L["Details"]</a>
<a class="btn btn-secondary" href="/admin/books/edit/@item.Id">@L["Edit"]</a>
<Button Color="Color.Danger" Clicked="() => {ShowModal(item.Id);}">@L["Delete"]</Button>
</td>
</tr>
}
</tbody>
</table>
<Modal @ref="_frmDelete" IsCentered="true" Closing="@ModalClosing">
<ModalBackdrop/>
<ModalContent Centered="true">
<ModalHeader>
<ModalTitle>@L["Delete book"]</ModalTitle>
<CloseButton Clicked="@HideModal"/>
</ModalHeader>
<ModalBody>
<Text>@string.Format(@L["Are you sure you want to delete the book {0}?"], _currentBook?.Title)</Text>
</ModalBody>
<ModalFooter>
<Button Color="Color.Primary" Clicked="@HideModal" Disabled="@_deleteInProgress">@L["Cancel"]</Button>
<Button Color="Color.Danger" Clicked="@ConfirmDelete" Disabled="@_deleteInProgress">@L["Delete"]</Button>
</ModalFooter>
</ModalContent>
</Modal>

View File

@@ -0,0 +1,88 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2020 Natalia Portillo
*******************************************************************************/
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Blazorise;
using Marechai.ViewModels;
using Microsoft.AspNetCore.Components.Authorization;
namespace Marechai.Pages.Admin
{
public partial class Books
{
List<BookViewModel> _books;
BookViewModel _currentBook;
bool _deleteInProgress;
Modal _frmDelete;
bool _loaded;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(_loaded)
return;
_books = await Service.GetAsync();
_loaded = true;
StateHasChanged();
}
void ShowModal(long itemId)
{
_currentBook = _books.FirstOrDefault(n => n.Id == itemId);
_frmDelete.Show();
}
void HideModal() => _frmDelete.Hide();
async void ConfirmDelete()
{
if(_currentBook is null)
return;
_deleteInProgress = true;
_books = null;
AuthenticationState authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
// Yield thread to let UI to update
await Task.Yield();
await Service.DeleteAsync(_currentBook.Id, (await UserManager.GetUserAsync(authState.User)).Id);
_books = await Service.GetAsync();
_deleteInProgress = false;
_frmDelete.Hide();
// Yield thread to let UI to update
await Task.Yield();
// Tell we finished loading
StateHasChanged();
}
void ModalClosing(ModalClosingEventArgs obj) => _currentBook = null;
}
}

View File

@@ -0,0 +1,211 @@
@{
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2020 Natalia Portillo
*******************************************************************************/
}
@page "/admin/books/details/{Id:long}"
@page "/admin/books/edit/{Id:long}"
@page "/admin/books/create"
@using Marechai.Database
@using Marechai.Database.Models
@inherits OwningComponentBase<BooksService>
@inject IStringLocalizer<BooksService> L
@inject Iso31661NumericService CountriesService
@inject NavigationManager NavigationManager
@inject IWebHostEnvironment Host
@inject IJSRuntime JSRuntime
@inject Microsoft.AspNetCore.Identity.UserManager<ApplicationUser> UserManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@attribute [Authorize(Roles = "UberAdmin, Admin")]
<h3>@L["Book details"]</h3>
<hr />
@if (!_loaded)
{
<p align="center">@L["Loading..."]</p>
return;
}
<div>
<Field>
<FieldLabel>@L["Title using latin script"]</FieldLabel>
<Validation Validator="@ValidateTitle">
<TextEdit ReadOnly="!_editing" @bind-Text="@_model.Title">
<Feedback>
<ValidationError>@L["Please enter a valid title."]</ValidationError>
</Feedback>
</TextEdit>
</Validation>
</Field>
@if (_editing || _model.NativeTitle != null)
{
<Field>
<FieldLabel>@L["Native title, that is, title using native script (cyrillic, chinese, etc)"]</FieldLabel>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownNativeTitle">@L["Unknown (native title)"]</Check>
}
@if (!_editing ||
!_unknownNativeTitle)
{
<Validation Validator="@ValidateNativeTitle">
<TextEdit Disabled="!_editing" @bind-Text="@_model.NativeTitle">
<Feedback>
<ValidationError>@L["Please enter a valid native title."]</ValidationError>
</Feedback>
</TextEdit>
</Validation>
}
</Field>
}
@if (_editing || _model.Published != null)
{
<Field>
<FieldLabel>@L["Published"]</FieldLabel>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownPublished">@L["Unknown (publication date)"]</Check>
}
@if (!_editing || !_unknownPublished)
{
<Validation Validator="@ValidatePublished">
<DateEdit TValue="DateTime?" ReadOnly="!_editing" @bind-Date="@_model.Published" >
<Feedback>
<ValidationError>@L["Please enter a valid publication date."]</ValidationError>
</Feedback>
</DateEdit>
</Validation>
}
</Field>
}
@if (_editing || _model.CountryId != null)
{
<Field>
<FieldLabel>@L["Country of publication"]</FieldLabel>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownCountry">@L["Unknown (country of publication)"]</Check>
}
@if (!_editing ||
!_unknownCountry)
{
<Select Disabled="!_editing" TValue="short?" @bind-SelectedValue="@_model.CountryId">
@foreach (var country in _countries)
{
<SelectItem TValue="short?" Value="@country.Id">@country.Name</SelectItem>
}
</Select>
}
</Field>
}
@if (_editing || _model.Isbn != null)
{
<Field>
<FieldLabel>@L["ISBN"]</FieldLabel>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownIsbn">@L["Unknown (ISBN)"]</Check>
}
@if (!_editing ||
!_unknownIsbn)
{
<Validation Validator="@ValidateIsbn">
<TextEdit ReadOnly="!_editing" @bind-Text="@_model.Isbn">
<Feedback>
<ValidationError>@L["Please enter a valid ISBN."]</ValidationError>
</Feedback>
</TextEdit>
</Validation>
}
</Field>
}
@if (_editing || _model.Pages.HasValue)
{
<Field>
<FieldLabel>@L["Pages"]</FieldLabel>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownPages">@L["Unknown (pages)"]</Check>
}
@if (!_editing ||
!_unknownPages)
{
<Validation Validator="@ValidatePages">
<NumericEdit Disabled="!_editing" TValue="short?" Decimals="0" @bind-Value="@_model.Pages">
<Feedback>
<ValidationError>@L["Please enter a valid number of pages."]</ValidationError>
</Feedback>
</NumericEdit>
</Validation>
}
</Field>
}
@if (_editing || _model.Edition.HasValue)
{
<Field>
<FieldLabel>@L["Edition"]</FieldLabel>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownEdition">@L["Unknown (edition)"]</Check>
}
@if (!_editing ||
!_unknownEdition)
{
<Validation Validator="@ValidateEdition">
<NumericEdit Disabled="!_editing" TValue="int?" Decimals="0" @bind-Value="@_model.Edition">
<Feedback>
<ValidationError>@L["Please enter a valid edition number."]</ValidationError>
</Feedback>
</NumericEdit>
</Validation>
}
</Field>
}
@{
// TODO: Source book
}
@{
// TODO: Link to previous edition
}
</div>
<div>
@if (!_editing)
{
<Button Color="Color.Primary" Clicked="@OnEditClicked">@L["Edit"]</Button>
}
else
{
<Button Color="Color.Success" Clicked="@OnSaveClicked">@L["Save"]</Button>
<Button Color="Color.Danger" Clicked="@OnCancelClicked">@L["Cancel"]</Button>
}
<a href="/admin/books" class="btn btn-secondary">@L["Back to list"]</a>
</div>
@{
// TODO: Book synopsis
}

View File

@@ -0,0 +1,202 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2020 Natalia Portillo
*******************************************************************************/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Blazorise;
using Marechai.Database.Models;
using Marechai.Shared;
using Marechai.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
namespace Marechai.Pages.Admin.Details
{
public partial class Book
{
AuthenticationState _authState;
List<Iso31661Numeric> _countries;
bool _creating;
bool _editing;
bool _loaded;
BookViewModel _model;
bool _unknownCountry;
bool _unknownEdition;
bool _unknownIsbn;
bool _unknownNativeTitle;
bool _unknownPages;
bool _unknownPublished;
[Parameter]
public long Id { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(_loaded)
return;
_loaded = true;
_creating = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).ToLowerInvariant().
StartsWith("admin/books/create", StringComparison.InvariantCulture);
if(Id <= 0 &&
!_creating)
return;
_countries = await CountriesService.GetAsync();
_model = _creating ? new BookViewModel() : await Service.GetAsync(Id);
_authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_editing = _creating || NavigationManager.ToBaseRelativePath(NavigationManager.Uri).ToLowerInvariant().
StartsWith("admin/books/edit/",
StringComparison.InvariantCulture);
if(_editing)
SetCheckboxes();
StateHasChanged();
}
void SetCheckboxes()
{
_unknownCountry = !_model.CountryId.HasValue;
_unknownNativeTitle = string.IsNullOrWhiteSpace(_model.NativeTitle);
_unknownPublished = !_model.Published.HasValue;
_unknownIsbn = string.IsNullOrWhiteSpace(_model.Isbn);
_unknownPages = !_model.Pages.HasValue;
_unknownEdition = !_model.Edition.HasValue;
}
void OnEditClicked()
{
_editing = true;
SetCheckboxes();
StateHasChanged();
}
async void OnCancelClicked()
{
_editing = false;
if(_creating)
{
NavigationManager.ToBaseRelativePath("admin/books");
return;
}
_model = await Service.GetAsync(Id);
SetCheckboxes();
StateHasChanged();
}
async void OnSaveClicked()
{
if(_unknownNativeTitle)
_model.NativeTitle = null;
else if(string.IsNullOrWhiteSpace(_model.NativeTitle))
return;
if(_unknownCountry)
_model.CountryId = null;
else if(_model.CountryId < 0)
return;
if(_unknownPages)
_model.Pages = null;
else if(_model.Pages < 1)
return;
if(_unknownEdition)
_model.Edition = null;
else if(_model.Edition < 1)
return;
if(_unknownPublished)
_model.Published = null;
else if(_model.Published?.Date >= DateTime.UtcNow.Date)
return;
if(_unknownIsbn)
_model.Isbn = null;
else if(string.IsNullOrWhiteSpace(_model.Isbn))
return;
// Convert ISBN-10 to ISBN-13
if(_model.Isbn?.Length == 10)
{
char[] newIsbn = new char[13];
Array.Copy(_model.Isbn.ToCharArray(), 0, newIsbn, 3, 9);
newIsbn[0] = '9';
newIsbn[1] = '7';
newIsbn[2] = '8';
int sum = (newIsbn[0] - 0x30) + ((newIsbn[1] - 0x30) * 3) + (newIsbn[2] - 0x30) +
((newIsbn[3] - 0x30) * 3) + (newIsbn[4] - 0x30) + ((newIsbn[5] - 0x30) * 3) +
(newIsbn[6] - 0x30) + ((newIsbn[7] - 0x30) * 3) + (newIsbn[8] - 0x30) +
((newIsbn[9] - 0x30) * 3) + (newIsbn[10] - 0x30) + ((newIsbn[11] - 0x30) * 3);
int modulo = sum % 10;
if(modulo != 0)
modulo = 10 - modulo;
newIsbn[12] = (char)(modulo + 0x30);
_model.Isbn = new string(newIsbn);
}
if(string.IsNullOrWhiteSpace(_model.Title))
return;
if(_creating)
Id = await Service.CreateAsync(_model, (await UserManager.GetUserAsync(_authState.User)).Id);
else
await Service.UpdateAsync(_model, (await UserManager.GetUserAsync(_authState.User)).Id);
_editing = false;
_creating = false;
_model = await Service.GetAsync(Id);
SetCheckboxes();
StateHasChanged();
}
void ValidateTitle(ValidatorEventArgs e) =>
Validators.ValidateString(e, L["Title must be smaller than 256 characters."], 256);
void ValidatePublished(ValidatorEventArgs e) => Validators.ValidateDate(e);
void ValidateNativeTitle(ValidatorEventArgs e) =>
Validators.ValidateString(e, L["Native title must be smaller than 256 characters."], 256);
void ValidatePages(ValidatorEventArgs e) => Validators.ValidateShort(e, 1);
void ValidateEdition(ValidatorEventArgs e) => Validators.ValidateInteger(e, 1);
void ValidateIsbn(ValidatorEventArgs e) => Validators.ValidateIsbn(e);
}
}

View File

@@ -89,6 +89,9 @@
<li> <li>
<a href="/admin/document_people">@L["Document people"]</a> <a href="/admin/document_people">@L["Document people"]</a>
</li> </li>
<li>
<a href="/admin/books">@L["Books"]</a>
</li>
</ul> </ul>
</div> </div>
<div class="content"> <div class="content">

View File

@@ -0,0 +1,17 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Books" xml:space="preserve">
<value>Books</value>
</data>
</root>

View File

@@ -210,4 +210,7 @@
<value>Resoluciones</value> <value>Resoluciones</value>
<comment>Resolutions.</comment> <comment>Resolutions.</comment>
</data> </data>
<data name="Books" xml:space="preserve">
<value>Libros</value>
</data>
</root> </root>

View File

@@ -0,0 +1,119 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Are you sure you want to delete the book {0}?" xml:space="preserve">
<value>Are you sure you want to delete the book {0}?</value>
</data>
<data name="Books" xml:space="preserve">
<value>Books</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Country" xml:space="preserve">
<value>Country</value>
</data>
<data name="Create new" xml:space="preserve">
<value>Create new</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Delete book" xml:space="preserve">
<value>Delete book</value>
</data>
<data name="Details" xml:space="preserve">
<value>Details</value>
</data>
<data name="Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="ISBN" xml:space="preserve">
<value>ISBN</value>
</data>
<data name="Loading..." xml:space="preserve">
<value>Loading...</value>
</data>
<data name="Native title" xml:space="preserve">
<value>Native title</value>
</data>
<data name="Published" xml:space="preserve">
<value>Published</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="Book details" xml:space="preserve">
<value>Book details</value>
</data>
<data name="Title using latin script" xml:space="preserve">
<value>Title using latin script</value>
</data>
<data name="Please enter a valid title." xml:space="preserve">
<value>Please enter a valid title.</value>
</data>
<data name="Native title, that is, title using native script (cyrillic, chinese, etc)" xml:space="preserve">
<value>Native title, that is, title using native script (cyrillic, chinese, etc)</value>
</data>
<data name="Unknown (native title)" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="Please enter a valid native title." xml:space="preserve">
<value>Please enter a valid native title.</value>
</data>
<data name="Unknown (publication date)" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="Please enter a valid publication date." xml:space="preserve">
<value>Please enter a valid publication date.</value>
</data>
<data name="Country of publication" xml:space="preserve">
<value>Country of publication</value>
</data>
<data name="Please enter a valid ISBN." xml:space="preserve">
<value>Please enter a valid ISBN.</value>
</data>
<data name="Pages" xml:space="preserve">
<value>Pages</value>
</data>
<data name="Please enter a valid number of pages." xml:space="preserve">
<value>Please enter a valid number of pages.</value>
</data>
<data name="Edition" xml:space="preserve">
<value>Edition</value>
</data>
<data name="Please enter a valid edition number." xml:space="preserve">
<value>Please enter a valid edition number.</value>
</data>
<data name="Back to list" xml:space="preserve">
<value>Back to list</value>
</data>
<data name="Title must be smaller than 256 characters." xml:space="preserve">
<value>Title must be smaller than 256 characters.</value>
</data>
<data name="Native title must be smaller than 256 characters." xml:space="preserve">
<value>Native title must be smaller than 256 characters.</value>
</data>
<data name="Unknown (pages)" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="Unknown (ISBN)" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="Unknown (edition)" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="Unknown (country of publication)" xml:space="preserve">
<value>Unknown</value>
</data>
</root>

View File

@@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- ReSharper disable MarkupTextTypo -->
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Books" xml:space="preserve">
<value>Libros</value>
<comment>Books</comment>
</data>
<data name="Loading..." xml:space="preserve">
<value>Cargando...</value>
<comment>Message that appears while data is being loaded from database</comment>
</data>
<data name="Country" xml:space="preserve">
<value>País</value>
<comment>Country where the book was published</comment>
</data>
<data name="Details" xml:space="preserve">
<value>Detalles</value>
<comment>Details</comment>
</data>
<data name="Edit" xml:space="preserve">
<value>Editar</value>
<comment>Edit</comment>
</data>
<data name="Delete" xml:space="preserve">
<value>Eliminar</value>
<comment>Delete</comment>
</data>
<data name="Delete book" xml:space="preserve">
<value>Eliminar libro</value>
<comment>Delete book</comment>
</data>
<data name="Are you sure you want to delete the book {0}?" xml:space="preserve">
<value>¿Está seguro de que desea borrar el libro {0}?</value>
<comment>{0} book name</comment>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancelar</value>
<comment>Cancel</comment>
</data>
<data name="Create new" xml:space="preserve">
<value>Crear nuevo</value>
</data>
<data name="Title" xml:space="preserve">
<value>Título</value>
</data>
<data name="Native title" xml:space="preserve">
<value>Título nativo</value>
</data>
<data name="Published" xml:space="preserve">
<value>Publicado el</value>
</data>
<data name="ISBN" xml:space="preserve">
<value>ISBN</value>
</data>
<data name="Book details" xml:space="preserve">
<value>Detalles de libro</value>
</data>
<data name="Title using latin script" xml:space="preserve">
<value>Título utilizando escritura latina</value>
</data>
<data name="Please enter a valid title." xml:space="preserve">
<value>Por favor introduce un título válido.</value>
</data>
<data name="Native title, that is, title using native script (cyrillic, chinese, etc)" xml:space="preserve">
<value>Título nativo, es decir, el título usando el sistema de escritura nativo (cirílico, chino, etc)</value>
</data>
<data name="Unknown (native title)" xml:space="preserve">
<value>Desconocido</value>
</data>
<data name="Please enter a valid native title." xml:space="preserve">
<value>Por favor introduce un título nativo válido.</value>
</data>
<data name="Unknown (publication date)" xml:space="preserve">
<value>Desconocida</value>
</data>
<data name="Please enter a valid publication date." xml:space="preserve">
<value>Por favor introduce una fecha de publicación válida.</value>
</data>
<data name="Country of publication" xml:space="preserve">
<value>País de publicación</value>
</data>
<data name="Unknown (country of publication)" xml:space="preserve">
<value>Desconocido</value>
</data>
<data name="Unknown (ISBN)" xml:space="preserve">
<value>Desconocido</value>
</data>
<data name="Please enter a valid ISBN." xml:space="preserve">
<value>Por favor introduce un ISBN válido.</value>
</data>
<data name="Pages" xml:space="preserve">
<value>Páginas</value>
</data>
<data name="Unknown (pages)" xml:space="preserve">
<value>Desconocidas</value>
</data>
<data name="Unknown (edition)" xml:space="preserve">
<value>Desconocida</value>
</data>
<data name="Please enter a valid number of pages." xml:space="preserve">
<value>Por favor introduce un número de páginas válido.</value>
</data>
<data name="Edition" xml:space="preserve">
<value>Edición</value>
</data>
<data name="Please enter a valid edition number." xml:space="preserve">
<value>Por favor introduce un número de edición válido.</value>
</data>
<data name="Back to list" xml:space="preserve">
<value>Volver a la lista</value>
</data>
<data name="Title must be smaller than 256 characters." xml:space="preserve">
<value>El título debe contener menos de 256 caracteres.</value>
</data>
<data name="Native title must be smaller than 256 characters." xml:space="preserve">
<value>El título nativo debe contener menos de 256 caracteres.</value>
</data>
</root>

View File

@@ -0,0 +1,134 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2020 Natalia Portillo
*******************************************************************************/
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Marechai.Database.Models;
using Marechai.ViewModels;
using Microsoft.EntityFrameworkCore;
namespace Marechai.Services
{
public class BooksService
{
readonly MarechaiContext _context;
public BooksService(MarechaiContext context) => _context = context;
public async Task<List<BookViewModel>> GetAsync() => await _context.
Books.OrderBy(b => b.NativeTitle).
ThenBy(b => b.Published).ThenBy(b => b.Title).
Select(b => new BookViewModel
{
Id = b.Id,
Title = b.Title,
NativeTitle = b.NativeTitle,
Published = b.Published,
Synopsis = b.Synopsis,
Isbn = b.Isbn,
CountryId = b.CountryId,
Pages = b.Pages,
Edition = b.Edition,
PreviousId = b.PreviousId,
SourceId = b.SourceId,
Country = b.Country.Name
}).ToListAsync();
public async Task<BookViewModel> GetAsync(long id) => await _context.Books.Where(b => b.Id == id).
Select(b => new BookViewModel
{
Id = b.Id,
Title = b.Title,
NativeTitle = b.NativeTitle,
Published = b.Published,
Synopsis = b.Synopsis,
Isbn = b.Isbn,
CountryId = b.CountryId,
Pages = b.Pages,
Edition = b.Edition,
PreviousId = b.PreviousId,
SourceId = b.SourceId,
Country = b.Country.Name
}).FirstOrDefaultAsync();
public async Task UpdateAsync(BookViewModel viewModel, string userId)
{
Book model = await _context.Books.FindAsync(viewModel.Id);
if(model is null)
return;
model.Title = viewModel.Title;
model.NativeTitle = viewModel.NativeTitle;
model.Published = viewModel.Published;
model.Synopsis = viewModel.Synopsis;
model.CountryId = viewModel.CountryId;
model.Isbn = viewModel.Isbn;
model.Pages = viewModel.Pages;
model.Edition = viewModel.Edition;
model.PreviousId = viewModel.PreviousId;
model.SourceId = viewModel.SourceId;
await _context.SaveChangesWithUserAsync(userId);
}
public async Task<long> CreateAsync(BookViewModel viewModel, string userId)
{
var model = new Book
{
Title = viewModel.Title,
NativeTitle = viewModel.NativeTitle,
Published = viewModel.Published,
Synopsis = viewModel.Synopsis,
CountryId = viewModel.CountryId,
Isbn = viewModel.Isbn,
Pages = viewModel.Pages,
Edition = viewModel.Edition,
PreviousId = viewModel.PreviousId,
SourceId = viewModel.SourceId
};
await _context.Books.AddAsync(model);
await _context.SaveChangesWithUserAsync(userId);
return model.Id;
}
public async Task<string> GetSynopsisTextAsync(int id) =>
(await _context.Books.FirstOrDefaultAsync(d => d.Id == id))?.Synopsis;
public async Task DeleteAsync(long id, string userId)
{
Book item = await _context.Books.FindAsync(id);
if(item is null)
return;
_context.Books.Remove(item);
await _context.SaveChangesWithUserAsync(userId);
}
}
}

View File

@@ -67,6 +67,7 @@ namespace Marechai.Services
services.AddScoped<ResolutionsByScreenService>(); services.AddScoped<ResolutionsByScreenService>();
services.AddScoped<ResolutionsByGpuService>(); services.AddScoped<ResolutionsByGpuService>();
services.AddScoped<MachinePhotosService>(); services.AddScoped<MachinePhotosService>();
services.AddScoped<BooksService>();
} }
} }
} }

View File

@@ -133,5 +133,81 @@ namespace Marechai.Shared
else else
e.Status = ValidationStatus.Success; e.Status = ValidationStatus.Success;
} }
public static void ValidateShort(ValidatorEventArgs e, short minValue = 0, short maxValue = short.MaxValue)
{
if(!(e.Value is short item) ||
item < minValue ||
item > maxValue)
e.Status = ValidationStatus.Error;
else
e.Status = ValidationStatus.Success;
}
public static void ValidateIsbn(ValidatorEventArgs e)
{
e.Status = ValidationStatus.Error;
if(!(e.Value is string isbn))
return;
if(isbn.Length != 10 &&
isbn.Length != 13)
return;
for(int c = 0; c < (isbn.Length == 13 ? 13 : 9); c++)
{
if(isbn[c] < 0x30 ||
isbn[c] > 0x39)
return;
}
int sum;
int modulo;
if(isbn.Length == 10)
{
if((isbn[9] < 0x30 || isbn[9] > 0x39) &&
isbn[9] != 'x' &&
isbn[9] != 'X')
return;
sum = 0;
modulo = 0;
for(int i = 0; i < 10; i++)
{
modulo += isbn[i] - 0x30;
sum += modulo;
}
modulo = sum % 11;
if((isbn[9] == 'x' || isbn[9] == 'X') &&
modulo == 10)
e.Status = ValidationStatus.Success;
else if(modulo == isbn[9] - 0x30)
e.Status = ValidationStatus.Success;
return;
}
if(isbn[0] != '9' ||
isbn[1] != '7' ||
(isbn[1] != '8' && isbn[1] != '9'))
return;
sum = (isbn[0] - 0x30) + ((isbn[1] - 0x30) * 3) + (isbn[2] - 0x30) + ((isbn[3] - 0x30) * 3) +
(isbn[4] - 0x30) + ((isbn[5] - 0x30) * 3) + (isbn[6] - 0x30) + ((isbn[7] - 0x30) * 3) +
(isbn[8] - 0x30) + ((isbn[9] - 0x30) * 3) + (isbn[10] - 0x30) + ((isbn[11] - 0x30) * 3);
modulo = sum % 10;
if(modulo != 0)
modulo = 10 - modulo;
if(modulo == isbn[12] - 0x30)
e.Status = ValidationStatus.Success;
}
} }
} }

View File

@@ -0,0 +1,11 @@
namespace Marechai.ViewModels
{
public class BookViewModel : DocumentBaseViewModel
{
public string Isbn { get; set; }
public short? Pages { get; set; }
public int? Edition { get; set; }
public long? PreviousId { get; set; }
public long? SourceId { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Marechai.ViewModels
{
public abstract class DocumentBaseViewModel : BaseViewModel<long>
{
public string Title { get; set; }
public string NativeTitle { get; set; }
public DateTime? Published { get; set; }
public short? CountryId { get; set; }
public string Country { get; set; }
public string Synopsis { get; set; }
}
}