Add license editing in admin view.

This commit is contained in:
2020-05-27 15:01:55 +01:00
parent 86cfb40ed8
commit b51119648f
9 changed files with 377 additions and 51 deletions

View File

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

View File

@@ -31,8 +31,10 @@
}
@page "/admin/licenses/details/{Id:int}"
@page "/admin/licenses/edit/{Id:int}"
@inherits OwningComponentBase<LicensesService>
@inject IStringLocalizer<LicensesService> L
@inject NavigationManager NavigationManager
@attribute [Authorize(Roles = "UberAdmin, Admin")]
<h3>@L["License details"]</h3>
<hr />
@@ -47,33 +49,84 @@
<div>
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit ReadOnly="!_editable" @bind-Text="@_model.Name"/>
<Validation Validator="@ValidateName">
<TextEdit Disabled="!_editing" @bind-Text="@_model.Name">
<Feedback>
<ValidationError>@L["Please enter a valid name."]</ValidationError>
</Feedback>
</TextEdit>
</Validation>
</Field>
<Field>
<FieldLabel>@L["SPDX identifier"]</FieldLabel>
<TextEdit ReadOnly="!_editable" @bind-Text="@_model.SPDX"/>
<Validation Validator="@ValidateSpdx">
<TextEdit ReadOnly="!_editing" @bind-Text="@_model.SPDX">
<Feedback>
<ValidationError>@L["Please enter a valid SPDX identifier."]</ValidationError>
</Feedback>
</TextEdit>
</Validation>
</Field>
<Field>
<FieldLabel>@L["FSF approved"]</FieldLabel>
<Check TValue="bool" Disabled="!_editable" @bind-Checked="@_model.FsfApproved"/>
<Check TValue="bool" Disabled="!_editing" @bind-Checked="@_model.FsfApproved"/>
</Field>
<Field>
<FieldLabel>@L["OSI approved"]</FieldLabel>
<Check TValue="bool" Disabled="!_editable" @bind-Checked="@_model.OsiApproved"/>
<Check TValue="bool" Disabled="!_editing" @bind-Checked="@_model.OsiApproved"/>
</Field>
@if (_editing || _model.Link != null)
{
<Field>
<FieldLabel>@L["License text link"]</FieldLabel>
<TextEdit ReadOnly="!_editable" @bind-Text="@_model.Link"/>
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownLink">@L["Unknown or none (text link)"]</Check>
}
@if (!_editing ||
!_unknownLink)
{
<Validation Validator="@ValidateLink">
<TextEdit ReadOnly="!_editing" @bind-Text="@_model.Link">
<Feedback>
<ValidationError>@L["Please enter a license text link."]</ValidationError>
</Feedback>
</TextEdit>
</Validation>
}
</Field>
@if (_editable || _model.Text != null)
}
@if (_editing || _model.Text != null)
{
<Field>
<FieldLabel>@L["License text"]</FieldLabel>
<MemoEdit Rows="200" Plaintext="true" ReadOnly="!_editable" @bind-Text="@_model.Text" />
@if (_editing)
{
<Check TValue="bool" @bind-Checked="@_unknownText">@L["Unknown (license text)"]</Check>
}
@if (!_editing ||
!_unknownText)
{
<Validation Validator="@ValidateText">
<MemoEdit Rows="200" Plaintext="true" ReadOnly="!_editing" @bind-Text="@_model.Text">
<Feedback>
<ValidationError>@L["Please enter a valid license text."]</ValidationError>
</Feedback>
</MemoEdit>
</Validation>
}
</Field>
}
</div>
<div>
<span class="btn btn-primary">@L["Edit"]</span>
@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/licenses" class="btn btn-secondary">@L["Back to list"]</a>
</div>

View File

@@ -1,13 +1,18 @@
using System;
using System.Threading.Tasks;
using Blazorise;
using Marechai.Shared;
using Microsoft.AspNetCore.Components;
namespace Marechai.Pages.Admin.Details
{
public partial class License
{
bool _editable;
bool _editing;
bool _loaded;
Database.Models.License _model;
bool _unknownLink;
bool _unknownText;
[Parameter]
public int Id { get; set; }
@@ -23,7 +28,74 @@ namespace Marechai.Pages.Admin.Details
_model = await Service.GetAsync(Id);
_editing = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).ToLowerInvariant().
StartsWith("admin/licenses/edit/", StringComparison.InvariantCulture);
if(_editing)
SetCheckboxes();
StateHasChanged();
}
void SetCheckboxes()
{
_unknownText = string.IsNullOrWhiteSpace(_model.Text);
_unknownLink = string.IsNullOrWhiteSpace(_model.Link);
}
void OnEditClicked()
{
_editing = true;
SetCheckboxes();
StateHasChanged();
}
async void OnCancelClicked()
{
_editing = false;
_model = await Service.GetAsync(Id);
SetCheckboxes();
StateHasChanged();
}
async void OnSaveClicked()
{
if(_unknownText)
_model.Text = null;
else if(string.IsNullOrWhiteSpace(_model.Text))
return;
if(string.IsNullOrWhiteSpace(_model.Text) ||
_model.Name?.Length > 255)
return;
if(string.IsNullOrWhiteSpace(_model.SPDX) ||
_model.SPDX?.Length > 255)
return;
if(_unknownLink)
_model.Link = null;
else if(string.IsNullOrWhiteSpace(_model.Link) ||
_model.Link?.Length > 512)
return;
_editing = false;
await Service.UpdateAsync(_model);
_model = await Service.GetAsync(Id);
SetCheckboxes();
StateHasChanged();
}
void ValidateName(ValidatorEventArgs e) =>
Validators.ValidateString(e, L["License name cannot contain more than 255 characters."], 255);
void ValidateSpdx(ValidatorEventArgs e) =>
Validators.ValidateString(e, L["SPDX identifier cannot contain more than 255 characters."], 255);
void ValidateLink(ValidatorEventArgs e) =>
Validators.ValidateUrl(e, L["License text link must be smaller than 512 characters."], 512);
void ValidateText(ValidatorEventArgs e) =>
Validators.ValidateString(e, L["License text cannot contain more than 131072 characters."], 131072);
}
}

View File

@@ -1,20 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Blazorise;
using Marechai.Database.Models;
using Marechai.Shared;
using Marechai.ViewModels;
using Microsoft.AspNetCore.Components;
using Match = System.Text.RegularExpressions.Match;
namespace Marechai.Pages.Admin.Details
{
public partial class Person
{
const string _webpageRegex =
@"^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$";
List<Iso31661Numeric> _countries;
bool _editing;
bool _loaded;
@@ -260,32 +256,8 @@ namespace Marechai.Pages.Admin.Details
e.Status = ValidationStatus.Error;
}
void ValidateWebpage(ValidatorEventArgs e)
{
if(!(e.Value is string webpage))
{
e.Status = ValidationStatus.Error;
return;
}
if(webpage.Length < 1 ||
webpage.Length > 255)
{
e.ErrorText = L["Webpage must be smaller than 255 characters."];
e.Status = ValidationStatus.Error;
return;
}
var rx = new Regex(_webpageRegex);
Match m = rx.Match(webpage);
if(m.Success)
return;
e.Status = ValidationStatus.Error;
}
void ValidateWebpage(ValidatorEventArgs e) =>
Validators.ValidateUrl(e, L["Webpage must be smaller than 255 characters."], 255);
void ValidateTwitter(ValidatorEventArgs e)
{

View File

@@ -93,9 +93,7 @@
</td>
<td>
<a class="btn btn-primary" href="/admin/licenses/details/@item.Id">@L["Details"]</a>
<span class="btn btn-secondary">
@L["Edit"]
</span>
<a class="btn btn-primary" href="/admin/licenses/edit/@item.Id">@L["Edit"]</a>
<Button Color="Color.Danger" Clicked="() => {ShowModal(item.Id);}">@L["Delete"]</Button>
</td>
</tr>

View File

@@ -0,0 +1,129 @@
<?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="Unknown or none (text link)" xml:space="preserve">
<value>Unknown or none</value>
<comment>Unknown or none, referring to a license text links</comment>
</data>
<data name="Unknown (license text)" xml:space="preserve">
<value>Unknown</value>
<comment>Unknown, referring to the license text</comment>
</data>
</root>

View File

@@ -202,4 +202,48 @@
<value>Texto de la licencia</value>
<comment>License text</comment>
</data>
<data name="Save" xml:space="preserve">
<value>Guardar</value>
<comment>Save</comment>
</data>
<data name="Unknown or none (text link)" xml:space="preserve">
<value>Desconocido o ninguno</value>
<comment>Unknown or none, referring to a license text links</comment>
</data>
<data name="Unknown (license text)" xml:space="preserve">
<value>Desconocido</value>
<comment>Unknown, referring to the license text</comment>
</data>
<data name="Please enter a valid name." xml:space="preserve">
<value>Por favor introduce un nombre válido.</value>
<comment>Please enter a valid name.</comment>
</data>
<data name="Please enter a valid SPDX identifier." xml:space="preserve">
<value>Por favor introduce un identificador SPDX válido.</value>
<comment>Please enter a valid SPDX identifier.</comment>
</data>
<data name="Please enter a license text link." xml:space="preserve">
<value>Por favor introduce un enlace al texto de la licencia válido.</value>
<comment>Please enter a license text link.</comment>
</data>
<data name="Please enter a valid license text." xml:space="preserve">
<value>Por favor introduce un texto de licencia válido.</value>
<comment>Please enter a valid license text.</comment>
</data>
<data name="License name cannot contain more than 255 characters." xml:space="preserve">
<value>El nombre de la licencia no puede contener más de 255 caracteres.</value>
<comment>License name cannot contain more than 255 characters.</comment>
</data>
<data name="SPDX identifier cannot contain more than 255 characters." xml:space="preserve">
<value>El identificador SPDX no puede contener más de 255 caracteres.</value>
<comment>SPDX identifier cannot contain more than 255 characters.</comment>
</data>
<data name="License text link must be smaller than 512 characters." xml:space="preserve">
<value>El enlace al texto de la licencia debe contener menos de 512 caracteres.</value>
<comment>License text link must be smaller than 512 characters.</comment>
</data>
<data name="License text cannot contain more than 131072 characters." xml:space="preserve">
<value>El texto de la licencia no puede contener mas de 131072 caracteres.</value>
<comment>License text cannot contain more than 131072 characters.</comment>
</data>
</root>

View File

@@ -19,7 +19,29 @@ namespace Marechai.Services
OsiApproved = l.OsiApproved, SPDX = l.SPDX
}).ToListAsync();
public async Task<License> GetAsync(int id) => await _context.Licenses.FindAsync(id);
public async Task<License> GetAsync(int id) =>
await _context.Licenses.Where(l => l.Id == id).Select(l => new License
{
FsfApproved = l.FsfApproved, Id = l.Id, Link = l.Link, Name = l.Name,
OsiApproved = l.OsiApproved, SPDX = l.SPDX, Text = l.Text
}).FirstOrDefaultAsync();
public async Task UpdateAsync(License viewModel)
{
License model = await _context.Licenses.FindAsync(viewModel.Id);
if(model is null)
return;
model.FsfApproved = viewModel.FsfApproved;
model.Link = viewModel.Link;
model.Name = viewModel.Name;
model.OsiApproved = viewModel.OsiApproved;
model.SPDX = viewModel.SPDX;
model.Text = viewModel.Text;
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{

View File

@@ -1,10 +1,15 @@
using System;
using System.Text.RegularExpressions;
using Blazorise;
using Match = System.Text.RegularExpressions.Match;
namespace Marechai.Shared
{
public static class Validators
{
const string _urlRegex =
@"^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$";
public static void ValidateString(ValidatorEventArgs e, string message, int maxLength)
{
string item = e.Value as string;
@@ -31,7 +36,8 @@ namespace Marechai.Shared
public static void ValidateDouble(ValidatorEventArgs e, double minValue = 0, double maxValue = double.MaxValue)
{
if(!(e.Value is double item) ||
item < minValue || item > maxValue)
item < minValue ||
item > maxValue)
e.Status = ValidationStatus.Error;
else
e.Status = ValidationStatus.Success;
@@ -40,7 +46,8 @@ namespace Marechai.Shared
public static void ValidateInteger(ValidatorEventArgs e, int minValue = 0, int maxValue = int.MaxValue)
{
if(!(e.Value is int item) ||
item < minValue || item > maxValue)
item < minValue ||
item > maxValue)
e.Status = ValidationStatus.Error;
else
e.Status = ValidationStatus.Success;
@@ -49,7 +56,8 @@ namespace Marechai.Shared
public static void ValidateLong(ValidatorEventArgs e, long minValue = 0, long maxValue = long.MaxValue)
{
if(!(e.Value is long item) ||
item < minValue || item > maxValue)
item < minValue ||
item > maxValue)
e.Status = ValidationStatus.Error;
else
e.Status = ValidationStatus.Success;
@@ -58,10 +66,38 @@ namespace Marechai.Shared
public static void ValidateFloat(ValidatorEventArgs e, float minValue = 0, float maxValue = float.MaxValue)
{
if(!(e.Value is float item) ||
item < minValue || item > maxValue)
item < minValue ||
item > maxValue)
e.Status = ValidationStatus.Error;
else
e.Status = ValidationStatus.Success;
}
public static void ValidateUrl(ValidatorEventArgs e, string message, int maxLength)
{
if(!(e.Value is string url))
{
e.Status = ValidationStatus.Error;
return;
}
if(url.Length < 1 ||
url.Length > maxLength)
{
e.ErrorText = message;
e.Status = ValidationStatus.Error;
return;
}
var rx = new Regex(_urlRegex);
Match m = rx.Match(url);
if(m.Success)
return;
e.Status = ValidationStatus.Error;
}
}
}