Compare commits
No commits in common. "66b14baee8a1b2bea0e9357bd85e6a8e4a285877" and "c50fc079fd4541622d1cf42e6022f8267b510a81" have entirely different histories.
66b14baee8
...
c50fc079fd
40
.github/workflows/testing.yml
vendored
40
.github/workflows/testing.yml
vendored
|
@ -1,40 +0,0 @@
|
||||||
name: NUnit Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
TEST_PROJECT: Wave.Tests
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
dotnet-version: [ '8.0.x' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup dotnet ${{ matrix.dotnet-version }}
|
|
||||||
uses: actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: ${{ matrix.dotnet-version }}
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.nuget/packages
|
|
||||||
# Look to see if there is a cache hit for the corresponding requirements file
|
|
||||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-nuget
|
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore ${{ env.TEST_PROJECT }}
|
|
||||||
|
|
||||||
- name: Test with the dotnet CLI
|
|
||||||
run: dotnet test ${{ env.TEST_PROJECT }}
|
|
|
@ -3,8 +3,6 @@
|
||||||
# Wave - The Collaborative Open-Source Blogging Engine
|
# Wave - The Collaborative Open-Source Blogging Engine
|
||||||
## Stay afloat in a current of Information
|
## Stay afloat in a current of Information
|
||||||
|
|
||||||
[![NUnit Tests](https://github.com/miawinter98/Wave/actions/workflows/testing.yml/badge.svg?branch=main)](https://github.com/miawinter98/Wave/actions/workflows/testing.yml)
|
|
||||||
|
|
||||||
![Wave License](https://img.shields.io/github/license/miawinter98/Wave?color=green&style=for-the-badge)
|
![Wave License](https://img.shields.io/github/license/miawinter98/Wave?color=green&style=for-the-badge)
|
||||||
![GitHub Forks](https://img.shields.io/github/forks/miawinter98/Wave?label=github%20forks&logo=github&style=for-the-badge)
|
![GitHub Forks](https://img.shields.io/github/forks/miawinter98/Wave?label=github%20forks&logo=github&style=for-the-badge)
|
||||||
![GitHub Stars](https://img.shields.io/github/stars/miawinter98/Wave?label=github%20stars&color=yellow&logo=github&style=for-the-badge)
|
![GitHub Stars](https://img.shields.io/github/stars/miawinter98/Wave?label=github%20stars&color=yellow&logo=github&style=for-the-badge)
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Testcontainers.PostgreSql;
|
|
||||||
using Wave.Data;
|
|
||||||
|
|
||||||
namespace Wave.Tests.Data;
|
|
||||||
|
|
||||||
[TestFixture, FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
|
|
||||||
[TestOf(typeof(ApplicationDbContext))]
|
|
||||||
public class ApplicationDbContextTest {
|
|
||||||
private PostgreSqlContainer PostgresContainer { get; } = new PostgreSqlBuilder().WithImage("postgres:16.1-alpine").Build();
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public async Task SetUp() {
|
|
||||||
await PostgresContainer.StartAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TearDown]
|
|
||||||
public async Task TearDown() {
|
|
||||||
await PostgresContainer.DisposeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ApplicationDbContext GetContext() {
|
|
||||||
return new ApplicationDbContext(
|
|
||||||
new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
||||||
.UseNpgsql(PostgresContainer.GetConnectionString())
|
|
||||||
.EnableSensitiveDataLogging()
|
|
||||||
.EnableDetailedErrors()
|
|
||||||
.EnableThreadSafetyChecks()
|
|
||||||
.Options);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Migration() {
|
|
||||||
await using var context = GetContext();
|
|
||||||
Assert.DoesNotThrowAsync(() => context.Database.MigrateAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task CreateArticle() {
|
|
||||||
await using var context = GetContext();
|
|
||||||
await context.Database.EnsureCreatedAsync();
|
|
||||||
|
|
||||||
var author = new ApplicationUser {
|
|
||||||
FullName = "Test User"
|
|
||||||
};
|
|
||||||
Article article = new() {
|
|
||||||
Title = "Testing Article",
|
|
||||||
Body = "This is a *test* Article",
|
|
||||||
Author = author
|
|
||||||
};
|
|
||||||
article.UpdateSlug(null);
|
|
||||||
article.UpdateBody();
|
|
||||||
|
|
||||||
await context.AddAsync(article);
|
|
||||||
Assert.DoesNotThrowAsync(() => context.SaveChangesAsync());
|
|
||||||
|
|
||||||
var dbArticle = await context.Set<Article>()
|
|
||||||
.IgnoreQueryFilters().FirstOrDefaultAsync();
|
|
||||||
Assert.That(dbArticle, Is.Not.Null);
|
|
||||||
|
|
||||||
Assert.That(dbArticle.Title, Is.EqualTo("Testing Article"));
|
|
||||||
Assert.That(dbArticle.Slug, Is.EqualTo("testing-article"));
|
|
||||||
|
|
||||||
Assert.That(dbArticle.Body, Is.EqualTo("This is a *test* Article"));
|
|
||||||
Assert.That(dbArticle.BodyPlain, Is.EqualTo("This is a test Article"));
|
|
||||||
Assert.That(dbArticle.BodyHtml, Is.EqualTo("<p>This is a <em>test</em> Article</p>"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
using Wave.Data;
|
|
||||||
|
|
||||||
namespace Wave.Tests.Data;
|
|
||||||
|
|
||||||
[TestFixture, FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
|
|
||||||
[TestOf(typeof(Article))]
|
|
||||||
public class ArticleTest {
|
|
||||||
private Article Article { get; } = new() {
|
|
||||||
Author = null!,
|
|
||||||
Title = null!,
|
|
||||||
Body = null!
|
|
||||||
};
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugWithAscii() {
|
|
||||||
Article.Title = "Testing Article";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("testing-article"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugWithSpecialCharacters() {
|
|
||||||
Article.Title = "Title with, special characters?";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("title-with%2C-special-characters%3F"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64Characters() {
|
|
||||||
Article.Title = "Article Title that is longer than the sixty four character limit and should be truncated";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("article-title-that-is-longer-than-the-sixty-four-character-limit"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition55() {
|
|
||||||
Article.Title = "Auto generating slugs was a mistake I hate this ______ €";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-______-%E2%82%AC"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize2AtPosition56() {
|
|
||||||
Article.Title = "Auto generating slugs was a mistake I hate this _______ üa";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-_______-%C3%BCa"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition56() {
|
|
||||||
Article.Title = "Auto generating slugs was a mistake I hate this _______ €";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-_______-"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize2AtPosition57() {
|
|
||||||
Article.Title = "Auto generating slugs was a mistake I hate this ________ üa";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-________-%C3%BCa"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition57() {
|
|
||||||
Article.Title = "Auto generating slugs was a mistake I hate this ________ €";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-________-"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition61() {
|
|
||||||
Article.Title = "Article that ends with a special character and need special cäre";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-need-special-c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition62() {
|
|
||||||
Article.Title = "Article that ends with a special character and needs special cäre";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-needs-special-c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition63() {
|
|
||||||
Article.Title = "Article that ends with a special character and needs special caäre";
|
|
||||||
Article.UpdateSlug();
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-needs-special-ca"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugProvidedValidUri() {
|
|
||||||
Article.Title = "Testing providing a slug";
|
|
||||||
Article.UpdateSlug("test-slug");
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("test-slug"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SlugProvidedNeedsEscaping() {
|
|
||||||
Article.Title = "Testing providing a slug";
|
|
||||||
Article.UpdateSlug("test slug");
|
|
||||||
Assert.That(Article.Slug, Is.EqualTo("test-slug"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
using NUnit.Framework.Constraints;
|
|
||||||
using Wave.Data;
|
|
||||||
using Wave.Utilities;
|
|
||||||
|
|
||||||
namespace Wave.Tests.Utilities;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[TestOf(typeof(ArticleUtilities))]
|
|
||||||
public class ArticleUtilitiesTest {
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void GenerateArticleLink() {
|
|
||||||
var testArticle = new Article {
|
|
||||||
Id = Guid.Parse("e7a94905-d83a-4146-8061-de2ef7869a82"),
|
|
||||||
Title = "Test Article",
|
|
||||||
Body = "This is the body of the test Article",
|
|
||||||
Author = new ApplicationUser {
|
|
||||||
UserName = "test@example.com",
|
|
||||||
FullName = "Test User"
|
|
||||||
},
|
|
||||||
PublishDate = DateTimeOffset.MaxValue,
|
|
||||||
Slug = "test-article"
|
|
||||||
};
|
|
||||||
|
|
||||||
string linkWithoutPublishDate = ArticleUtilities.GenerateArticleLink(testArticle, null);
|
|
||||||
Assert.That(linkWithoutPublishDate, Is.EqualTo("/article/e7a94905-d83a-4146-8061-de2ef7869a82"));
|
|
||||||
|
|
||||||
testArticle.PublishDate = new DateTimeOffset(new DateOnly(2024, 4, 24), TimeOnly.MinValue, TimeSpan.Zero);
|
|
||||||
string linkWithPublishDate = ArticleUtilities.GenerateArticleLink(testArticle, null);
|
|
||||||
Assert.That(linkWithPublishDate, Is.EqualTo("/2024/04/24/test-article"));
|
|
||||||
|
|
||||||
string testHttps = ArticleUtilities.GenerateArticleLink(testArticle, new Uri("http://example.com", UriKind.Absolute));
|
|
||||||
Assert.That(testHttps, new StartsWithConstraint("https://"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
|
||||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
|
||||||
<PackageReference Include="Testcontainers" Version="3.8.0" />
|
|
||||||
<PackageReference Include="Testcontainers.PostgreSql" Version="3.8.0" />
|
|
||||||
<PackageReference Include="Testcontainers.Redis" Version="3.8.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Wave\Wave.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="NUnit.Framework" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Utilities\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
6
Wave.sln
6
Wave.sln
|
@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wave", "Wave\Wave.csproj",
|
||||||
EndProject
|
EndProject
|
||||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}"
|
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wave.Tests", "Wave.Tests\Wave.Tests.csproj", "{54BFBF0E-5918-4830-BCDD-135BAD702529}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -23,10 +21,6 @@ Global
|
||||||
{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{FE5DA24A-8490-4DCE-BDFB-49C9CF656F8A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{54BFBF0E-5918-4830-BCDD-135BAD702529}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{54BFBF0E-5918-4830-BCDD-135BAD702529}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{54BFBF0E-5918-4830-BCDD-135BAD702529}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{54BFBF0E-5918-4830-BCDD-135BAD702529}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<HelpDropdownComponent Body="@Localizer["AboutTheAuthor_Explanation"]"/>
|
<HelpDropdownComponent Body="@Localizer["AboutTheAuthor_Explanation"]"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<InputTextArea class="textarea textarea-bordered w-full h-24" maxlength="500" oninput="charactersLeft_onInput(this)"
|
<InputTextArea class="textarea textarea-bordered w-full h-24" maxlength="512"
|
||||||
@bind-Value="@Model.AboutTheAuthor" placeholder="@Localizer["AboutTheAuthor_Placeholder"]"/>
|
@bind-Value="@Model.AboutTheAuthor" placeholder="@Localizer["AboutTheAuthor_Placeholder"]"/>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt text-error"><ValidationMessage For="() => Model.AboutTheAuthor"/></span>
|
<span class="label-text-alt text-error"><ValidationMessage For="() => Model.AboutTheAuthor"/></span>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<HelpDropdownComponent Body="@Localizer["Biography_Explanation"]"/>
|
<HelpDropdownComponent Body="@Localizer["Biography_Explanation"]"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<InputTextArea class="textarea textarea-bordered w-full h-48" maxlength="4000" oninput="charactersLeft_onInput(this)"
|
<InputTextArea class="textarea textarea-bordered w-full h-48" maxlength="4096"
|
||||||
@bind-Value="@Model.Biography" placeholder="@Localizer["Biography_Placeholder"]"/>
|
@bind-Value="@Model.Biography" placeholder="@Localizer["Biography_Placeholder"]"/>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt text-error"><ValidationMessage For="() => Model.Biography"/></span>
|
<span class="label-text-alt text-error"><ValidationMessage For="() => Model.Biography"/></span>
|
||||||
|
|
|
@ -14,22 +14,18 @@
|
||||||
|
|
||||||
<InputLabelComponent For="() => Model.ContactEmail" LabelText="@Localizer["Contact_Email_Label"]">
|
<InputLabelComponent For="() => Model.ContactEmail" LabelText="@Localizer["Contact_Email_Label"]">
|
||||||
<InputText class="input input-bordered w-full" maxlength="128" type="email" autocomplete="email"
|
<InputText class="input input-bordered w-full" maxlength="128" type="email" autocomplete="email"
|
||||||
oninput="charactersLeft_onInput(this)"
|
|
||||||
@bind-Value="@Model.ContactEmail" placeholder="@Localizer["Contact_Email_Placeholder"]" />
|
@bind-Value="@Model.ContactEmail" placeholder="@Localizer["Contact_Email_Placeholder"]" />
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
<InputLabelComponent For="() => Model.ContactPhone" LabelText="@Localizer["Contact_Phone_Label"]">
|
<InputLabelComponent For="() => Model.ContactPhone" LabelText="@Localizer["Contact_Phone_Label"]">
|
||||||
<InputText class="input input-bordered w-full" maxlength="64" type="tel" autocomplete="tel"
|
<InputText class="input input-bordered w-full" maxlength="64" type="tel" autocomplete="tel"
|
||||||
oninput="charactersLeft_onInput(this)"
|
|
||||||
@bind-Value="@Model.ContactPhone" placeholder="@Localizer["Contact_Phone_Placeholder"]" />
|
@bind-Value="@Model.ContactPhone" placeholder="@Localizer["Contact_Phone_Placeholder"]" />
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
<InputLabelComponent For="() => Model.ContactPhoneBusiness" LabelText="@Localizer["Contact_PhoneBusiness_Label"]">
|
<InputLabelComponent For="() => Model.ContactPhoneBusiness" LabelText="@Localizer["Contact_PhoneBusiness_Label"]">
|
||||||
<InputText class="input input-bordered w-full" maxlength="64" type="tel" autocomplete="tel"
|
<InputText class="input input-bordered w-full" maxlength="64" type="tel" autocomplete="tel"
|
||||||
oninput="charactersLeft_onInput(this)"
|
|
||||||
@bind-Value="@Model.ContactPhoneBusiness" placeholder="@Localizer["Contact_PhoneBusiness_Placeholder"]" />
|
@bind-Value="@Model.ContactPhoneBusiness" placeholder="@Localizer["Contact_PhoneBusiness_Placeholder"]" />
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
<InputLabelComponent For="() => Model.ContactWebsite" LabelText="@Localizer["Contact_Website_Label"]">
|
<InputLabelComponent For="() => Model.ContactWebsite" LabelText="@Localizer["Contact_Website_Label"]">
|
||||||
<InputText class="input input-bordered w-full" maxlength="128" type="url" autocomplete="url"
|
<InputText class="input input-bordered w-full" maxlength="128" type="url" autocomplete="url"
|
||||||
oninput="charactersLeft_onInput(this)"
|
|
||||||
@bind-Value="@Model.ContactWebsite" placeholder="@Localizer["Contact_Website_Placeholder"]" />
|
@bind-Value="@Model.ContactWebsite" placeholder="@Localizer["Contact_Website_Placeholder"]" />
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,7 @@
|
||||||
<form id="add-user-link" @formname="add-user-link" @onsubmit="Add" method="post">
|
<form id="add-user-link" @formname="add-user-link" @onsubmit="Add" method="post">
|
||||||
<AntiforgeryToken />
|
<AntiforgeryToken />
|
||||||
<InputLabelComponent LabelText="@Localizer["Links_Url_Label"]">
|
<InputLabelComponent LabelText="@Localizer["Links_Url_Label"]">
|
||||||
<input class="input input-bordered" maxlength="512" autocomplete="off" type="url"
|
<input class="input input-bordered" maxlength="1024" autocomplete="off" type="url"
|
||||||
oninput="charactersLeft_onInput(this)"
|
|
||||||
name="link-url" value="@Url" placeholder="@Localizer["Links_Url_Placeholder"]" />
|
name="link-url" value="@Url" placeholder="@Localizer["Links_Url_Placeholder"]" />
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
</form>
|
</form>
|
||||||
|
@ -51,7 +50,7 @@
|
||||||
public required ApplicationUser User { get; set; }
|
public required ApplicationUser User { get; set; }
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
[SupplyParameterFromForm(FormName = "add-user-link", Name = "link-url"), MaxLength(512)]
|
[SupplyParameterFromForm(FormName = "add-user-link", Name = "link-url")]
|
||||||
private string Url { get; set; } = string.Empty;
|
private string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
|
@ -63,7 +62,7 @@
|
||||||
Message.ShowError("Url is required.");
|
Message.ShowError("Url is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Url.Length > 512) {
|
if (Url.Length > 1024) {
|
||||||
Message.ShowError("Url is too long.");
|
Message.ShowError("Url is too long.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">@Localizer["FullName_Label"]</span>
|
<span class="label-text">@Localizer["FullName_Label"]</span>
|
||||||
</div>
|
</div>
|
||||||
<InputText class="input input-bordered w-full" maxlength="64" autocomplete="name" oninput="charactersLeft_onInput(this)"
|
<InputText class="input input-bordered w-full" maxlength="64" autocomplete="name"
|
||||||
@bind-Value="@Model.FullName" placeholder="@Localizer["FullName_Placeholder"]" />
|
@bind-Value="@Model.FullName" placeholder="@Localizer["FullName_Placeholder"]" />
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt text-error">
|
<span class="label-text-alt text-error">
|
||||||
|
|
|
@ -109,6 +109,52 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<SectionContent SectionName="scripts">
|
<SectionContent SectionName="scripts">
|
||||||
|
<script>
|
||||||
|
window.insertBeforeSelection = function(markdown, startOfLine = false) {
|
||||||
|
const target = document.getElementById("tool-target");
|
||||||
|
|
||||||
|
const start = target.selectionStart;
|
||||||
|
const end = target.selectionEnd;
|
||||||
|
const value = target.value;
|
||||||
|
let doStart = start;
|
||||||
|
if (startOfLine) {
|
||||||
|
doStart = value.lastIndexOf("\n", start) +1;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.focus();
|
||||||
|
target.value = value.substring(0, doStart) + markdown + value.substring(doStart);
|
||||||
|
|
||||||
|
target.selectionStart = start + markdown.length;
|
||||||
|
target.selectionEnd = end + markdown.length;
|
||||||
|
target.focus();
|
||||||
|
target.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.insertBeforeAndAfterSelection = function (markdown) {
|
||||||
|
const target = document.getElementById("tool-target");
|
||||||
|
|
||||||
|
while (/\s/.test(target.value[target.selectionStart]) && target.selectionStart < target.value.length) {
|
||||||
|
target.selectionStart++;
|
||||||
|
}
|
||||||
|
while (/\s/.test(target.value[target.selectionEnd-1]) && target.selectionEnd > 0) {
|
||||||
|
target.selectionEnd--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = target.selectionStart;
|
||||||
|
const end = target.selectionEnd;
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
target.focus();
|
||||||
|
target.value = value.substring(0, start) +
|
||||||
|
markdown + value.substring(start, end) + markdown +
|
||||||
|
value.substring(end);
|
||||||
|
|
||||||
|
target.selectionStart = start + markdown.length;
|
||||||
|
target.selectionEnd = end + markdown.length;
|
||||||
|
target.focus();
|
||||||
|
target.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
|
@ -44,72 +44,8 @@
|
||||||
<CascadingValue Value="UserTheme" Name="UserTheme">
|
<CascadingValue Value="UserTheme" Name="UserTheme">
|
||||||
<Routes />
|
<Routes />
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
<script src="_framework/blazor.web.js" defer></script>
|
<SectionOutlet SectionName="scripts" />
|
||||||
<SectionOutlet SectionName="scripts" />
|
<script src="_framework/blazor.web.js" defer></script>
|
||||||
<script>
|
|
||||||
charactersLeft_onInput = function(input) {
|
|
||||||
const maxLength = input.maxLength;
|
|
||||||
const currentLength = input.value.length;
|
|
||||||
|
|
||||||
const newLeft = maxLength - currentLength;
|
|
||||||
|
|
||||||
let elem = input.parentNode.querySelector(".characters-left");
|
|
||||||
if (elem) {
|
|
||||||
elem.innerText = newLeft;
|
|
||||||
} else {
|
|
||||||
input.parentNode.classList.add("relative");
|
|
||||||
elem = document.createElement("span");
|
|
||||||
elem.classList.add("characters-left");
|
|
||||||
elem.innerText = newLeft;
|
|
||||||
input.parentNode.appendChild(elem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.insertBeforeSelection = function (markdown, startOfLine = false) {
|
|
||||||
const target = document.getElementById("tool-target");
|
|
||||||
|
|
||||||
const start = target.selectionStart;
|
|
||||||
const end = target.selectionEnd;
|
|
||||||
const value = target.value;
|
|
||||||
let doStart = start;
|
|
||||||
if (startOfLine) {
|
|
||||||
doStart = value.lastIndexOf("\n", start) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
target.focus();
|
|
||||||
target.value = value.substring(0, doStart) + markdown + value.substring(doStart);
|
|
||||||
|
|
||||||
target.selectionStart = start + markdown.length;
|
|
||||||
target.selectionEnd = end + markdown.length;
|
|
||||||
target.focus();
|
|
||||||
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.insertBeforeAndAfterSelection = function (markdown) {
|
|
||||||
const target = document.getElementById("tool-target");
|
|
||||||
|
|
||||||
while (/\s/.test(target.value[target.selectionStart]) && target.selectionStart < target.value.length) {
|
|
||||||
target.selectionStart++;
|
|
||||||
}
|
|
||||||
while (/\s/.test(target.value[target.selectionEnd - 1]) && target.selectionEnd > 0) {
|
|
||||||
target.selectionEnd--;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = target.selectionStart;
|
|
||||||
const end = target.selectionEnd;
|
|
||||||
const value = target.value;
|
|
||||||
|
|
||||||
target.focus();
|
|
||||||
target.value = value.substring(0, start) +
|
|
||||||
markdown + value.substring(start, end) + markdown +
|
|
||||||
value.substring(end);
|
|
||||||
|
|
||||||
target.selectionStart = start + markdown.length;
|
|
||||||
target.selectionEnd = end + markdown.length;
|
|
||||||
target.focus();
|
|
||||||
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
@using Humanizer
|
@using Humanizer
|
||||||
@using Wave.Utilities
|
@using Wave.Utilities
|
||||||
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject IStringLocalizer<Pages.ArticleView> Localizer
|
@inject IStringLocalizer<Pages.ArticleView> Localizer
|
||||||
|
|
||||||
<SectionContent SectionName="GlobalHeader">
|
<SectionContent SectionName="GlobalHeader">
|
||||||
<header class="bg-secondary text-secondary-content border-b-2 border-current py-6 px-4 md:px-12" data-nosnippet>
|
<header class="bg-secondary text-secondary-content border-b-2 border-current py-6 px-4 md:px-12">
|
||||||
<h1 class="text-3xl lg:text-5xl font-light">
|
<h1 class="text-3xl lg:text-5xl font-light">
|
||||||
@Article.Title
|
@Article.Title
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -47,39 +46,6 @@
|
||||||
</header>
|
</header>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
|
|
||||||
@if (Article.Headings.Count > 0) {
|
|
||||||
<section class="mb-3 p-2 bg-base-200 rounded-box w-80 float-start mr-2 mb-2" data-nosnippet>
|
|
||||||
<h2 class="text-xl font-bold mb-3">@Localizer["TableOfContents"]</h2>
|
|
||||||
<ul class="menu p-0 [&_li>*]:rounded-none">
|
|
||||||
@{
|
|
||||||
int level = 1;
|
|
||||||
foreach (var heading in Article.Headings.OrderBy(h => h.Order)) {
|
|
||||||
int headingLevel = heading.Order % 10;
|
|
||||||
|
|
||||||
while (headingLevel < level) {
|
|
||||||
level--;
|
|
||||||
@(new MarkupString("</ul></li>"))
|
|
||||||
}
|
|
||||||
|
|
||||||
while (headingLevel > level) {
|
|
||||||
level++;
|
|
||||||
@(new MarkupString("<li><ul>"))
|
|
||||||
}
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="/@Navigation.ToBaseRelativePath(Navigation.Uri)#@heading.Anchor">@((MarkupString)heading.Label)</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
while (level > 1) {
|
|
||||||
level--;
|
|
||||||
@(new MarkupString("<li><ul>"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
<article class="mb-6">
|
<article class="mb-6">
|
||||||
<div class="prose prose-neutral max-w-none hyphens-auto text-justify">
|
<div class="prose prose-neutral max-w-none hyphens-auto text-justify">
|
||||||
@Content
|
@Content
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<li><NavLink href="manage/api">@Localizer["ManageApi_Label"]</NavLink></li>
|
<li><NavLink href="manage/api">@Localizer["ManageApi_Label"]</NavLink></li>
|
||||||
<li><NavLink href="newsletter">@Localizer["Newsletter_Label"]</NavLink></li>
|
<li><NavLink href="newsletter">@Localizer["Newsletter_Label"]</NavLink></li>
|
||||||
<li><NavLink href="subscribers">@Localizer["Subscribers_Label"]</NavLink></li>
|
|
||||||
</Authorized>
|
</Authorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -52,7 +51,7 @@
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<li class="flex gap-2">
|
<li class="flex gap-2">
|
||||||
<NavLink href="/account/manage">
|
<NavLink href="/Account/Manage">
|
||||||
<span class="line-clamp-2">@context.User.FindFirst("FullName")!.Value</span>
|
<span class="line-clamp-2">@context.User.FindFirst("FullName")!.Value</span>
|
||||||
<div class="w-8">
|
<div class="w-8">
|
||||||
<ProfilePictureComponent Size="100" ProfileId="@context.User.FindFirst("Id")!.Value" />
|
<ProfilePictureComponent Size="100" ProfileId="@context.User.FindFirst("Id")!.Value" />
|
||||||
|
@ -60,7 +59,7 @@
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
<li class="">
|
<li class="">
|
||||||
<form action="/account/logout" method="post">
|
<form action="/Account/Logout" method="post">
|
||||||
<AntiforgeryToken />
|
<AntiforgeryToken />
|
||||||
<input type="hidden" name="ReturnUrl" value="@_currentUrl" />
|
<input type="hidden" name="ReturnUrl" value="@_currentUrl" />
|
||||||
<button type="submit" class="flex gap-2">
|
<button type="submit" class="flex gap-2">
|
||||||
|
@ -75,7 +74,7 @@
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<li>
|
<li>
|
||||||
<NavLink href="/account/login">
|
<NavLink href="/Account/Login">
|
||||||
@Localizer["Login_Label"]
|
@Localizer["Login_Label"]
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
<path fill-rule="evenodd" d="M17 4.25A2.25 2.25 0 0 0 14.75 2h-5.5A2.25 2.25 0 0 0 7 4.25v2a.75.75 0 0 0 1.5 0v-2a.75.75 0 0 1 .75-.75h5.5a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-.75.75h-5.5a.75.75 0 0 1-.75-.75v-2a.75.75 0 0 0-1.5 0v2A2.25 2.25 0 0 0 9.25 18h5.5A2.25 2.25 0 0 0 17 15.75V4.25Z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M17 4.25A2.25 2.25 0 0 0 14.75 2h-5.5A2.25 2.25 0 0 0 7 4.25v2a.75.75 0 0 0 1.5 0v-2a.75.75 0 0 1 .75-.75h5.5a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-.75.75h-5.5a.75.75 0 0 1-.75-.75v-2a.75.75 0 0 0-1.5 0v2A2.25 2.25 0 0 0 9.25 18h5.5A2.25 2.25 0 0 0 17 15.75V4.25Z" clip-rule="evenodd" />
|
||||||
|
@ -85,7 +84,7 @@
|
||||||
</li>
|
</li>
|
||||||
@if (Features.Value.NativeSignup) {
|
@if (Features.Value.NativeSignup) {
|
||||||
<li>
|
<li>
|
||||||
<NavLink href="/account/register">
|
<NavLink href="/Account/Register">
|
||||||
@Localizer["SignUp_Label"]
|
@Localizer["SignUp_Label"]
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -47,7 +47,6 @@
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var article = await context.Set<Article>().IgnoreQueryFilters()
|
var article = await context.Set<Article>().IgnoreQueryFilters()
|
||||||
.Include(a => a.Author).Include(a => a.Reviewer)
|
|
||||||
.Where(a => !a.IsDeleted).FirstOrDefaultAsync(a => a.Id == Id);
|
.Where(a => !a.IsDeleted).FirstOrDefaultAsync(a => a.Id == Id);
|
||||||
if (article.AllowedToDelete(HttpContext.User)) Article = article;
|
if (article.AllowedToDelete(HttpContext.User)) Article = article;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,14 +74,6 @@
|
||||||
@Localizer["Delete_Submit"]
|
@Localizer["Delete_Submit"]
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (article.AllowedToRejectReview(HttpContext.User)) {
|
|
||||||
<form @formname="reject-review" method="post" @onsubmit="RejectReview" class="max-sm:w-full">
|
|
||||||
<AntiforgeryToken />
|
|
||||||
<button type="submit" class="btn btn-error w-full sm:btn-wide">
|
|
||||||
@Localizer["Review_Reject"]
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
@if (article.AllowedToSubmitForReview(HttpContext.User)) {
|
@if (article.AllowedToSubmitForReview(HttpContext.User)) {
|
||||||
<form @formname="submit-for-review" method="post" @onsubmit="SubmitForReview" class="max-sm:w-full">
|
<form @formname="submit-for-review" method="post" @onsubmit="SubmitForReview" class="max-sm:w-full">
|
||||||
<AntiforgeryToken/>
|
<AntiforgeryToken/>
|
||||||
|
@ -194,7 +186,7 @@
|
||||||
if (Id is not null) {
|
if (Id is not null) {
|
||||||
Article = query.AsSingleQuery().FirstOrDefault(a => a.Id == Id);
|
Article = query.AsSingleQuery().FirstOrDefault(a => a.Id == Id);
|
||||||
} else if (Date is { } date && Title is { } title) {
|
} else if (Date is { } date && Title is { } title) {
|
||||||
string? slug = TitleEncoded == null ? null : Uri.EscapeDataString(TitleEncoded.Replace("-", " ")).Replace("%20", "-");
|
string? slug = TitleEncoded == null ? null : Uri.EscapeDataString(TitleEncoded);
|
||||||
Article = query.AsSingleQuery().FirstOrDefault(a =>
|
Article = query.AsSingleQuery().FirstOrDefault(a =>
|
||||||
a.PublishDate.Date == date.Date
|
a.PublishDate.Date == date.Date
|
||||||
&& (slug != null && a.Slug == slug || a.Title.ToLower() == title));
|
&& (slug != null && a.Slug == slug || a.Title.ToLower() == title));
|
||||||
|
@ -303,47 +295,6 @@
|
||||||
Navigation.NavigateTo("/");
|
Navigation.NavigateTo("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RejectReview() {
|
|
||||||
if (Article.AllowedToRejectReview(HttpContext.User) is false) return;
|
|
||||||
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
||||||
Article!.Status = ArticleStatus.Draft;
|
|
||||||
string userId = HttpContext.User.FindFirst("Id")!.Value;
|
|
||||||
if (Article.Author.Id != userId) {
|
|
||||||
Article.Reviewer = await context.Users.FindAsync(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Update(Article);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
try {
|
|
||||||
var author = Article.Author;
|
|
||||||
|
|
||||||
string message =
|
|
||||||
$"The Article '{Article.Title}' has been rejected by a Reviewer, you will find it in your drafts.\n" +
|
|
||||||
$"Please make appropriate changes before submitting it again.";
|
|
||||||
if (author.Id != HttpContext.User.FindFirst("Id")!.Value) {
|
|
||||||
await EmailService.ConnectAsync(CancellationToken.None);
|
|
||||||
|
|
||||||
var email = await Email.CreateDefaultEmail(
|
|
||||||
author.Email!,
|
|
||||||
author.Name,
|
|
||||||
"Review Rejected",
|
|
||||||
"Your Article has been reject",
|
|
||||||
$"<p>{message}</p>",
|
|
||||||
message);
|
|
||||||
// TODO check if they enabled email notifications (property currently not implemented)
|
|
||||||
await EmailService.SendEmailAsync(email);
|
|
||||||
|
|
||||||
await EmailService.DisconnectAsync(CancellationToken.None);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.LogError(ex, "Failed to send mail to author about article '{title}' being rejected.", Article.Title);
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigation.NavigateTo("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SubmitForPublish() {
|
private async Task SubmitForPublish() {
|
||||||
if (Article.AllowedToPublish(HttpContext.User) is false) return;
|
if (Article.AllowedToPublish(HttpContext.User) is false) return;
|
||||||
|
|
||||||
|
@ -402,5 +353,4 @@
|
||||||
Navigation.NavigateTo("/");
|
Navigation.NavigateTo("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
@page "/Subscribers/edit/{id:guid}"
|
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using Wave.Data
|
|
||||||
@using Wave.Utilities
|
|
||||||
|
|
||||||
@attribute [Authorize(Roles = "Admin")]
|
|
||||||
|
|
||||||
@inject ILogger<EditSubscriber> Logger
|
|
||||||
@inject IStringLocalizer<EditSubscriber> Localizer
|
|
||||||
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
|
||||||
@inject IMessageDisplay Message
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
<PageTitle>@(Localizer["Title"] + TitlePostfix)</PageTitle>
|
|
||||||
|
|
||||||
<BoardComponent CenterContent="true">
|
|
||||||
<BoardCardComponent>
|
|
||||||
@if (Model is null) {
|
|
||||||
<p>not found</p>
|
|
||||||
} else {
|
|
||||||
<EditForm FormName="EditSubscriber" Model="Model" method="post" OnValidSubmit="Submit">
|
|
||||||
<h1 class="text-3xl">@Localizer["Title"]</h1>
|
|
||||||
<p class="text-xl my-3">@Email</p>
|
|
||||||
<InputLabelComponent LabelText="@Localizer["Name_Label"]" For="() => Model.Name">
|
|
||||||
<InputText @bind-Value="Model.Name" class="input input-bordered w-full" autocomplete="off"
|
|
||||||
placeholder="@Localizer["Name_Placeholder"]"/>
|
|
||||||
</InputLabelComponent>
|
|
||||||
<div class="form-control w-full mb-3">
|
|
||||||
<label class="label cursor-pointer">
|
|
||||||
<span class="label-text">@Localizer["Subscribed_Label"]</span>
|
|
||||||
<InputCheckbox @bind-Value="Model.Subscribed" class="checkbox"/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary w-full">@Localizer["Submit"]</button>
|
|
||||||
</EditForm>
|
|
||||||
}
|
|
||||||
</BoardCardComponent>
|
|
||||||
</BoardComponent>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[CascadingParameter(Name = "TitlePostfix")]
|
|
||||||
private string TitlePostfix { get; set; } = default!;
|
|
||||||
[Parameter]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
[SupplyParameterFromForm(FormName = "EditSubscriber")]
|
|
||||||
private EditModel? Model { get; set; }
|
|
||||||
|
|
||||||
private string Email { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
||||||
var subscriber = await context.Set<EmailSubscriber>()
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.IgnoreAutoIncludes()
|
|
||||||
.FirstOrDefaultAsync(s => s.Id == Id);
|
|
||||||
|
|
||||||
if (subscriber is null) {
|
|
||||||
Message.ShowError(Localizer["Error_NotFound"]);
|
|
||||||
} else if (Model is null) {
|
|
||||||
Email = subscriber.Email;
|
|
||||||
Model = new EditModel {
|
|
||||||
Name = subscriber.Name ?? string.Empty,
|
|
||||||
Subscribed = !subscriber.Unsubscribed
|
|
||||||
};
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private async Task Submit() {
|
|
||||||
if (Model is null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
||||||
var subscriber = await context.Set<EmailSubscriber>()
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.IgnoreAutoIncludes()
|
|
||||||
.FirstOrDefaultAsync(s => s.Id == Id);
|
|
||||||
|
|
||||||
if (subscriber is null) {
|
|
||||||
Message.ShowError(Localizer["Error_NotFound"]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriber.Name = string.IsNullOrWhiteSpace(Model.Name) ? null : Model.Name;
|
|
||||||
subscriber.Unsubscribed = !Model.Subscribed;
|
|
||||||
|
|
||||||
context.Update(subscriber);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.LogError(ex, "Failed to save changes to Email Subscriber {Email}.", Email);
|
|
||||||
Message.ShowError(Localizer["Submit_Error"]);
|
|
||||||
}
|
|
||||||
Message.ShowSuccess(Localizer["Submit_Success"]);
|
|
||||||
|
|
||||||
Navigation.NavigateTo("/subscribers");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class EditModel {
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public bool Subscribed { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -62,7 +62,7 @@
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (Features.Value.EmailSubscriptions) {
|
@if (Features.Value.EmailSubscriptions) {
|
||||||
<a class="btn btn-sm btn-primary" title="E-Mail Newsletter" href="/email/subscribe">
|
<a class="btn btn-sm btn-primary" title="E-Mail Newsletter" href="/Email/Subscribe">
|
||||||
E-Mail
|
E-Mail
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
||||||
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
|
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
|
||||||
<InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
|
<InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
|
||||||
<InputText class="input input-bordered w-full" maxlength="256" required aria-required oninput="charactersLeft_onInput(this)"
|
<InputText class="input input-bordered w-full" maxlength="256" required aria-required
|
||||||
@bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off"/>
|
@bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off"/>
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
<InputLabelComponent LabelText="@Localizer["Slug_Label"]" For="() => Model.Slug">
|
<InputLabelComponent LabelText="@Localizer["Slug_Label"]" For="() => Model.Slug">
|
||||||
@if (Article.Status is not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow) {
|
@if (Article.Status is not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow) {
|
||||||
<InputText class="input input-bordered w-full" maxlength="64" oninput="charactersLeft_onInput(this)"
|
<InputText class="input input-bordered w-full" maxlength="64"
|
||||||
@bind-Value="@Model.Slug" placeholder="@Localizer["Slug_Placeholder"]" autocomplete="off"/>
|
@bind-Value="@Model.Slug" placeholder="@Localizer["Slug_Placeholder"]" autocomplete="off"/>
|
||||||
} else {
|
} else {
|
||||||
<input class="input input-bordered w-full" readonly value="@Model.Slug"
|
<input class="input input-bordered w-full" readonly value="@Model.Slug"
|
||||||
|
@ -188,12 +188,19 @@
|
||||||
Article.PublishDate = Model.PublishDate.Value;
|
Article.PublishDate = Model.PublishDate.Value;
|
||||||
if (Article.Status is ArticleStatus.Published && Article.PublishDate < DateTimeOffset.Now) {
|
if (Article.Status is ArticleStatus.Published && Article.PublishDate < DateTimeOffset.Now) {
|
||||||
// can't change slugs when the article is public
|
// can't change slugs when the article is public
|
||||||
} else {
|
} else if (!string.IsNullOrWhiteSpace(Model.Slug)) {
|
||||||
Article.UpdateSlug(Model.Slug);
|
Article.Slug = WebUtility.UrlEncode(Model.Slug);
|
||||||
|
} else if (string.IsNullOrWhiteSpace(Article.Slug)) {
|
||||||
|
Article.Slug = Uri.EscapeDataString(Article.Title.ToLowerInvariant())
|
||||||
|
.Replace("-", "+")
|
||||||
|
.Replace("%20", "-");
|
||||||
|
Article.Slug = Article.Slug[..Math.Min(64, Article.Slug.Length)];
|
||||||
Model.Slug = Article.Slug;
|
Model.Slug = Article.Slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
Article.LastModified = DateTimeOffset.UtcNow;
|
Article.LastModified = DateTimeOffset.UtcNow;
|
||||||
|
Article.BodyHtml = MarkdownUtilities.Parse(Article.Body);
|
||||||
|
Article.BodyPlain = HtmlUtilities.GetPlainText(Article.BodyHtml);
|
||||||
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
@ -212,8 +219,6 @@
|
||||||
.Where(ac => ac.Article.Id == Article.Id).LoadAsync();
|
.Where(ac => ac.Article.Id == Article.Id).LoadAsync();
|
||||||
|
|
||||||
context.Update(Article);
|
context.Update(Article);
|
||||||
context.RemoveRange(Article.Headings);
|
|
||||||
Article.UpdateBody();
|
|
||||||
|
|
||||||
var existingImages = await context.Set<Article>()
|
var existingImages = await context.Set<Article>()
|
||||||
.IgnoreQueryFilters().Where(a => a.Id == Article.Id)
|
.IgnoreQueryFilters().Where(a => a.Id == Article.Id)
|
||||||
|
@ -252,10 +257,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message.ShowSuccess(Localizer["Save_Success"]);
|
Message.ShowSuccess(Localizer["Save_Success"]);
|
||||||
|
|
||||||
if (Navigation.Uri.EndsWith("/article/new")) {
|
|
||||||
Navigation.NavigateTo($"/article/{Article.Id}/edit", false, true);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Message.ShowError(Localizer["Save_Error"]);
|
Message.ShowError(Localizer["Save_Error"]);
|
||||||
Logger.LogError(ex, "Failed to save article.");
|
Logger.LogError(ex, "Failed to save article.");
|
||||||
|
@ -279,7 +280,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// published articles may only be edited my admins or moderators
|
// published articles may only be edited my admins or moderators
|
||||||
if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Moderator")) {
|
if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Reviewer")) {
|
||||||
article.Reviewer = me; // TODO replace with editor or something?
|
article.Reviewer = me; // TODO replace with editor or something?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,224 +0,0 @@
|
||||||
@page "/Subscribers"
|
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using Microsoft.Extensions.Options
|
|
||||||
@using Wave.Data
|
|
||||||
@using Wave.Utilities
|
|
||||||
@using CsvHelper.Configuration
|
|
||||||
@using System.Globalization
|
|
||||||
@using CsvHelper
|
|
||||||
|
|
||||||
@attribute [Authorize(Roles = "Admin")]
|
|
||||||
|
|
||||||
@inject IStringLocalizer<Subscribers> Localizer
|
|
||||||
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
|
||||||
@inject ILogger<Subscribers> Logger
|
|
||||||
@inject IMessageDisplay Message
|
|
||||||
|
|
||||||
<ModalComponent Id="@ModalId">
|
|
||||||
<ChildContent>
|
|
||||||
<form id="AddSubscribers" method="post" @formname="AddSubscribers" @onsubmit="AddSubscribers">
|
|
||||||
<AntiforgeryToken/>
|
|
||||||
|
|
||||||
<h2 class="text-xl">@Localizer["AddSubscribers_Label"]</h2>
|
|
||||||
<span class="my-3"><small>Format: Email;[Name];[Exclusion Reason];</small></span>
|
|
||||||
<InputLabelComponent LabelText="@Localizer["AddSubscribers_Input_Label"]">
|
|
||||||
<InputTextArea class="textarea textarea-bordered" rows="12"
|
|
||||||
@bind-Value="@SubscribersInput"
|
|
||||||
placeholder="@Localizer["AddSubscribers_Input_Placeholder"]"
|
|
||||||
required aria-required max="8096"
|
|
||||||
autocomplete="off"/>
|
|
||||||
</InputLabelComponent>
|
|
||||||
</form>
|
|
||||||
</ChildContent>
|
|
||||||
<Actions>
|
|
||||||
<button type="submit" form="AddSubscribers" class="btn btn-primary">
|
|
||||||
@Localizer["Submit"]
|
|
||||||
</button>
|
|
||||||
</Actions>
|
|
||||||
</ModalComponent>
|
|
||||||
|
|
||||||
<PageTitle>@(Localizer["Title"] + TitlePostfix)</PageTitle>
|
|
||||||
|
|
||||||
<h1 class="text-3xl lg:text-5xl font-light mb-6 text-primary">@Localizer["Title"]</h1>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 mb-3">
|
|
||||||
<details class="dropdown">
|
|
||||||
<summary class="btn btn-sm btn-accent">
|
|
||||||
@Items /
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4" title="page">
|
|
||||||
<path d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625Z" />
|
|
||||||
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
|
|
||||||
</svg>
|
|
||||||
</summary>
|
|
||||||
<ul class="p-2 shadow-xl menu dropdown-content z-[1] bg-accent text-accent-content rounded-box w-52">
|
|
||||||
<li><a href="@($"/subscribers?page={Math.Floor(Items * Page / 5.0)}&items=5")">5</a></li>
|
|
||||||
<li><a href="@($"/subscribers?page={Math.Floor(Items * Page / 10.0)}")">10</a></li>
|
|
||||||
<li><a href="@($"/subscribers?page={Math.Floor(Items * Page / 25.0)}&items=25")">25</a></li>
|
|
||||||
<li><a href="@($"/subscribers?page={Math.Floor(Items * Page / 50.0)}&items=50")">50</a></li>
|
|
||||||
<li><a href="@($"/subscribers?page={Math.Floor(Items * Page / 100.0)}&items=100")">100</a></li>
|
|
||||||
<li><a href="@($"/subscribers?page={Math.Floor(Items * Page / 250.0)}&items=250")">250</a></li>
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-primary" onclick="@(ModalId).showModal()">
|
|
||||||
@Localizer["AddSubscribers_Label"]
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="flex flex-col h-128">
|
|
||||||
<div class="flex-grow overflow-auto">
|
|
||||||
<table class="table relative">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="bg-base-200 sticky top-0 text-center md:text-left md:w-48">@Localizer["Header_Email"]</th>
|
|
||||||
<th class="bg-base-200 sticky top-0 text-center md:text-left w-48 max-md:hidden">@Localizer["Header_Name"]</th>
|
|
||||||
<th class="bg-base-200 sticky top-0 text-center md:text-left w-24 max-md:hidden">@Localizer["Header_LastReceived"]</th>
|
|
||||||
<th class="bg-base-200 sticky top-0 text-center md:text-left w-24 max-md:hidden">@Localizer["Header_LastOpen"]</th>
|
|
||||||
<th class="bg-base-200 sticky top-0 text-center md:text-left w-24 max-md:hidden">@Localizer["Header_UnsubscribeReason"]</th>
|
|
||||||
<th class="bg-base-200 sticky top-0 text-center md:text-left md:w-8 z-10">@Localizer["Header_Subscribed"]</th>
|
|
||||||
<td class="bg-base-200 sticky top-0 text-center md:text-left w-24"></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y">
|
|
||||||
<PageComponent Page="@Page" LoadCallback="LoadSubscribers" ItemsPerPage="@Items">
|
|
||||||
<tr>
|
|
||||||
<td>@context.Email</td>
|
|
||||||
<td class="max-md:hidden">@context.Name</td>
|
|
||||||
<td class="max-md:hidden">@context.LastMailReceived?.ToString("g")</td>
|
|
||||||
<td class="max-md:hidden">@context.LastMailOpened?.ToString("g")</td>
|
|
||||||
<td class="max-md:hidden">@context.UnsubscribeReason</td>
|
|
||||||
<td class="text-center md:text-left">
|
|
||||||
<input type="checkbox" class="checkbox no-animation" checked="@(!context.Unsubscribed)" disabled/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a class="btn btn-sm btn-info w-24" href="@($"/subscribers/edit/{context.Id}")">
|
|
||||||
@Localizer["Subscriber_Edit"]
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</PageComponent>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="max-md:hidden">
|
|
||||||
@Localizer["Newsletter_Footer_Timezone"] @TimeZoneInfo.Local
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid place-content-center my-3">
|
|
||||||
<div class="join">
|
|
||||||
@if (Page < 1) {
|
|
||||||
<button class="join-item btn" disabled title="@Localizer["Paging_Previous"]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 1 1 1.06 1.06L9.31 12l6.97 6.97a.75.75 0 1 1-1.06 1.06l-7.5-7.5Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
} else {
|
|
||||||
<a class="join-item btn" target="_top" href="@(Page < 2 ? $"/subscribers?items={Items}" : $"/subscribers?page={Page - 1}&items={Items}")" title="@Localizer["Paging_Previous"]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 1 1 1.06 1.06L9.31 12l6.97 6.97a.75.75 0 1 1-1.06 1.06l-7.5-7.5Z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
<button class="join-item btn md:btn-wide no-animation">@Localizer["Paging_Page"] @(Page + 1)</button>
|
|
||||||
@if (Page >= TotalPages - 1) {
|
|
||||||
<button class="join-item btn" disabled title="@Localizer["Paging_Next"]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M16.28 11.47a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 0 1-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 0 1 1.06-1.06l7.5 7.5Z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
} else {
|
|
||||||
<a class="join-item btn" target="_top" href="@($"/subscribers?page={Page + 1}&items={Items}")" title="@Localizer["Paging_Next"]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M16.28 11.47a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 0 1-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 0 1 1.06-1.06l7.5 7.5Z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[CascadingParameter(Name = "TitlePostfix")]
|
|
||||||
private string TitlePostfix { get; set; } = default!;
|
|
||||||
[SupplyParameterFromQuery]
|
|
||||||
public int Page { get; set; } = 0;
|
|
||||||
[SupplyParameterFromQuery]
|
|
||||||
public int Items { get; set; }
|
|
||||||
|
|
||||||
private int TotalPages { get; set; }
|
|
||||||
|
|
||||||
[SupplyParameterFromForm(FormName = "AddSubscribers")]
|
|
||||||
private string SubscribersInput { get; set; } = string.Empty;
|
|
||||||
private static string ModalId => "AddSubscribersDialog";
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
if (Items < 1) Items = 10;
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
||||||
var query = context.Set<EmailSubscriber>().IgnoreQueryFilters();
|
|
||||||
TotalPages = (int)Math.Max(Math.Ceiling(await query.CountAsync() / (double)Items), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask<IEnumerable<EmailSubscriber>> LoadSubscribers(int page, int count) {
|
|
||||||
try {
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
||||||
return await context.Set<EmailSubscriber>()
|
|
||||||
.IgnoreAutoIncludes().IgnoreQueryFilters()
|
|
||||||
.OrderBy(s => s.Email).ThenBy(s => s.Id)
|
|
||||||
.Skip(page).Take(count).ToListAsync();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.LogError(ex, "Failed to load subscribers on page {Page} with count {Count}.", page, count);
|
|
||||||
Message.ShowError(Localizer["Subscriber_Load_Error"]);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddSubscribers() {
|
|
||||||
var config = new CsvConfiguration(CultureInfo.CurrentCulture) {
|
|
||||||
NewLine = Environment.NewLine,
|
|
||||||
HasHeaderRecord = false
|
|
||||||
};
|
|
||||||
|
|
||||||
List<SubscriberModel> list;
|
|
||||||
try {
|
|
||||||
using var reader = new CsvReader(new StringReader(SubscribersInput), config);
|
|
||||||
list = reader.GetRecords<SubscriberModel>().ToList();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Message.ShowError(string.Format(Localizer["AddSubscribers_Parse_Error"], ex.Message));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (list.Count < 1) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var emailSubscribers = new List<EmailSubscriber>();
|
|
||||||
foreach (var input in list) {
|
|
||||||
emailSubscribers.Add(new EmailSubscriber {
|
|
||||||
Email = input.Email,
|
|
||||||
Name = input.Name,
|
|
||||||
Unsubscribed = !string.IsNullOrWhiteSpace(input.UnsubscribeReason),
|
|
||||||
UnsubscribeReason = input.UnsubscribeReason,
|
|
||||||
Language = "en-US"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
||||||
context.AddRange(emailSubscribers);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
SubscribersInput = string.Empty;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Message.ShowError(string.Format(Localizer["AddSubscribers_Save_Error"], ex.InnerException?.Message ?? ex.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class SubscriberModel {
|
|
||||||
[CsvHelper.Configuration.Attributes.Index(0)]
|
|
||||||
public string Email { get; set; } = string.Empty;
|
|
||||||
[CsvHelper.Configuration.Attributes.Index(1)]
|
|
||||||
public string? Name { get; set; }
|
|
||||||
[CsvHelper.Configuration.Attributes.Index(2)]
|
|
||||||
public string? UnsubscribeReason { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -43,7 +43,6 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
||||||
.IsRequired().OnDelete(DeleteBehavior.Cascade);
|
.IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||||
article.HasOne(a => a.Reviewer).WithMany()
|
article.HasOne(a => a.Reviewer).WithMany()
|
||||||
.IsRequired(false).OnDelete(DeleteBehavior.SetNull);
|
.IsRequired(false).OnDelete(DeleteBehavior.SetNull);
|
||||||
article.OwnsMany(a => a.Headings);
|
|
||||||
|
|
||||||
article.Property(a => a.CreationDate)
|
article.Property(a => a.CreationDate)
|
||||||
.IsRequired().HasDefaultValueSql("now()")
|
.IsRequired().HasDefaultValueSql("now()")
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Wave.Utilities;
|
|
||||||
|
|
||||||
namespace Wave.Data;
|
namespace Wave.Data;
|
||||||
|
|
||||||
|
@ -10,20 +8,10 @@ public enum ArticleStatus {
|
||||||
Published = 2
|
Published = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ArticleHeading {
|
|
||||||
[Key]
|
|
||||||
public int Id { get; set; }
|
|
||||||
public required int Order { get; set; }
|
|
||||||
[MaxLength(128)]
|
|
||||||
public required string Label { get; set; }
|
|
||||||
[MaxLength(256)]
|
|
||||||
public required string Anchor { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:: Add tags for MVP ?
|
// TODO:: Add tags for MVP ?
|
||||||
// TODO:: Archive System (Notice / Redirect to new content?) (Deprecation date?)
|
// TODO:: Archive System (Notice / Redirect to new content?) (Deprecation date?)
|
||||||
|
|
||||||
public partial class Article : ISoftDelete {
|
public class Article : ISoftDelete {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
@ -33,7 +21,6 @@ public partial class Article : ISoftDelete {
|
||||||
|
|
||||||
[MaxLength(256)]
|
[MaxLength(256)]
|
||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
// ReSharper disable thrice EntityFramework.ModelValidation.UnlimitedStringLength
|
|
||||||
public required string Body { get; set; }
|
public required string Body { get; set; }
|
||||||
public string BodyHtml { get; set; } = string.Empty;
|
public string BodyHtml { get; set; } = string.Empty;
|
||||||
public string BodyPlain { get; set; } = string.Empty;
|
public string BodyPlain { get; set; } = string.Empty;
|
||||||
|
@ -56,56 +43,4 @@ public partial class Article : ISoftDelete {
|
||||||
|
|
||||||
public IList<Category> Categories { get; } = [];
|
public IList<Category> Categories { get; } = [];
|
||||||
public IList<ArticleImage> Images { get; } = [];
|
public IList<ArticleImage> Images { get; } = [];
|
||||||
public IList<ArticleHeading> Headings { get; } = [];
|
|
||||||
|
|
||||||
public void UpdateSlug(string? potentialNewSlug = null) {
|
|
||||||
if (!string.IsNullOrWhiteSpace(potentialNewSlug) && Uri.IsWellFormedUriString(potentialNewSlug, UriKind.Relative)) {
|
|
||||||
Slug = potentialNewSlug;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(potentialNewSlug) && !string.IsNullOrWhiteSpace(Slug)) return;
|
|
||||||
|
|
||||||
string baseSlug = potentialNewSlug ?? Title;
|
|
||||||
baseSlug = baseSlug.ToLowerInvariant()[..Math.Min(64, baseSlug.Length)];
|
|
||||||
string slug = Uri.EscapeDataString(baseSlug).Replace("-", "+").Replace("%20", "-");
|
|
||||||
|
|
||||||
// I hate my life
|
|
||||||
int escapeTrimOvershoot = 0;
|
|
||||||
if (slug.Length > 64) {
|
|
||||||
// Escape sequences come with a % and two hex digits, there may be up to 3 of such sequences
|
|
||||||
// per character escaping ('?' has %3F, but € has %E2%82%AC), so we need to find the last group
|
|
||||||
// of such an escape parade and see if it's going over by less than 9, because then we need to
|
|
||||||
// remove more characters in the truncation, or we end up with a partial escape sequence.. parade
|
|
||||||
escapeTrimOvershoot = 64 - Regex.Match(slug,
|
|
||||||
@"(?<escape>(%[a-fA-F\d][a-fA-F\d])+)",
|
|
||||||
RegexOptions.None | RegexOptions.ExplicitCapture)
|
|
||||||
.Groups.Values.Last(g => g.Index < 64).Index;
|
|
||||||
if (escapeTrimOvershoot > 9) escapeTrimOvershoot = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Slug = slug[..Math.Min(slug.Length, 64 - escapeTrimOvershoot)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateBody() {
|
|
||||||
BodyHtml = MarkdownUtilities.Parse(Body).Trim();
|
|
||||||
BodyPlain = HtmlUtilities.GetPlainText(BodyHtml).Trim();
|
|
||||||
|
|
||||||
Headings.Clear();
|
|
||||||
var headings = HeadingsRegex().Matches(BodyHtml);
|
|
||||||
foreach(Match match in headings) {
|
|
||||||
string label = match.Groups["Label"].Value;
|
|
||||||
string anchor = match.Groups["Anchor"].Value;
|
|
||||||
|
|
||||||
var h = new ArticleHeading {
|
|
||||||
Order = match.Index * 10 + int.Parse(match.Groups["Level"].Value),
|
|
||||||
Label = label[..Math.Min(128, label.Length)],
|
|
||||||
Anchor = anchor[..Math.Min(256, anchor.Length)]
|
|
||||||
};
|
|
||||||
Headings.Add(h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex("<h(?<Level>[1-6]).*id=\"(?<Anchor>.+)\".*>(?<Label>.+)</h[1-6]>")]
|
|
||||||
private static partial Regex HeadingsRegex();
|
|
||||||
}
|
}
|
|
@ -1,750 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using Wave.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Wave.Data.Migrations.postgres
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20240502122029_TableOfContents")]
|
|
||||||
partial class TableOfContents
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("Npgsql:CollationDefinition:default-case-insensitive", "und-u-kf-upper-ks-level1,und-u-kf-upper-ks-level1,icu,False")
|
|
||||||
.HasAnnotation("ProductVersion", "8.0.0")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("AspNetRoles", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetRoleClaims", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserClaims", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserLogins", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserRoles", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserTokens", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ApiClaim", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ApiKeyKey")
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApiKeyKey");
|
|
||||||
|
|
||||||
b.ToTable("ApiClaim");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ApiKey", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("OwnerName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("ApiKey");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ApplicationUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AboutTheAuthor")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<int>("AccessFailedCount")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Biography")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)");
|
|
||||||
|
|
||||||
b.Property<string>("BiographyHtml")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactEmail")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("ContactPhone")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ContactPhoneBusiness")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ContactWebsite")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("EmailConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("FullName")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedEmail")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedUserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
|
||||||
.HasDatabaseName("EmailIndex");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("UserNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUsers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.Article", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BodyHtml")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BodyPlain")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasDefaultValue("");
|
|
||||||
|
|
||||||
b.Property<bool>("CanBePublic")
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasComputedColumnSql("\"IsDeleted\" = false AND \"Status\" = 2", true);
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreationDate")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasDefaultValueSql("now()");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("LastModified")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasDefaultValueSql("now()");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("PublishDate")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReviewerId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)")
|
|
||||||
.HasDefaultValue("");
|
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AuthorId");
|
|
||||||
|
|
||||||
b.HasIndex("ReviewerId");
|
|
||||||
|
|
||||||
b.ToTable("Articles", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<Guid>("ArticleId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("CategoryId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ArticleId");
|
|
||||||
|
|
||||||
b.HasIndex("CategoryId");
|
|
||||||
|
|
||||||
b.ToTable("ArticleCategories", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ArticleImage", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid?>("ArticleId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("ImageDescription")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ArticleId");
|
|
||||||
|
|
||||||
b.ToTable("Images", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.Category", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("Color")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(25);
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)")
|
|
||||||
.UseCollation("default-case-insensitive");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Name")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Categories", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.EmailNewsletter", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<Guid>("ArticleId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("DistributionDateTime")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsSend")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ArticleId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Newsletter", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.EmailSubscriber", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)")
|
|
||||||
.UseCollation("default-case-insensitive");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)")
|
|
||||||
.HasDefaultValue("en-US");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastMailOpened")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastMailReceived")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("UnsubscribeReason")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("Unsubscribed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Email")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("Unsubscribed");
|
|
||||||
|
|
||||||
b.ToTable("NewsletterSubscribers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<Guid>("ImageId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("ProfilePictures", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.UserLink", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UrlString")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId");
|
|
||||||
|
|
||||||
b.ToTable("UserLink");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ApiClaim", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApiKey", null)
|
|
||||||
.WithMany("ApiClaims")
|
|
||||||
.HasForeignKey("ApiKeyKey")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.Article", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", "Author")
|
|
||||||
.WithMany("Articles")
|
|
||||||
.HasForeignKey("AuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", "Reviewer")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ReviewerId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.OwnsMany("Wave.Data.ArticleHeading", "Headings", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<Guid>("ArticleId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b1.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
|
||||||
|
|
||||||
b1.Property<string>("Anchor")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b1.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b1.Property<int>("Order")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b1.HasKey("ArticleId", "Id");
|
|
||||||
|
|
||||||
b1.ToTable("ArticleHeading");
|
|
||||||
|
|
||||||
b1.WithOwner()
|
|
||||||
.HasForeignKey("ArticleId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Author");
|
|
||||||
|
|
||||||
b.Navigation("Headings");
|
|
||||||
|
|
||||||
b.Navigation("Reviewer");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.Article", "Article")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ArticleId")
|
|
||||||
.OnDelete(DeleteBehavior.NoAction)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Wave.Data.Category", "Category")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CategoryId")
|
|
||||||
.OnDelete(DeleteBehavior.NoAction)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Article");
|
|
||||||
|
|
||||||
b.Navigation("Category");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ArticleImage", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.Article", null)
|
|
||||||
.WithMany("Images")
|
|
||||||
.HasForeignKey("ArticleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.EmailNewsletter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.Article", "Article")
|
|
||||||
.WithOne()
|
|
||||||
.HasForeignKey("Wave.Data.EmailNewsletter", "ArticleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Article");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", null)
|
|
||||||
.WithOne("ProfilePicture")
|
|
||||||
.HasForeignKey("Wave.Data.ProfilePicture", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.UserLink", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Wave.Data.ApplicationUser", null)
|
|
||||||
.WithMany("Links")
|
|
||||||
.HasForeignKey("ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ApiKey", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("ApiClaims");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.ApplicationUser", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Articles");
|
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
|
|
||||||
b.Navigation("ProfilePicture");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Wave.Data.Article", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Images");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Wave.Data.Migrations.postgres;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TableOfContents : Migration {
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder) {
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ArticleHeading",
|
|
||||||
columns: table => new {
|
|
||||||
ArticleId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy",
|
|
||||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Order = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Label = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
|
||||||
Anchor = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table => {
|
|
||||||
table.PrimaryKey("PK_ArticleHeading", x => new {x.ArticleId, x.Id});
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ArticleHeading_Articles_ArticleId",
|
|
||||||
column: x => x.ArticleId,
|
|
||||||
principalTable: "Articles",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder) {
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ArticleHeading");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -630,42 +630,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
.HasForeignKey("ReviewerId")
|
.HasForeignKey("ReviewerId")
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.OwnsMany("Wave.Data.ArticleHeading", "Headings", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<Guid>("ArticleId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b1.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
|
||||||
|
|
||||||
b1.Property<string>("Anchor")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b1.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b1.Property<int>("Order")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b1.HasKey("ArticleId", "Id");
|
|
||||||
|
|
||||||
b1.ToTable("ArticleHeading");
|
|
||||||
|
|
||||||
b1.WithOwner()
|
|
||||||
.HasForeignKey("ArticleId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Author");
|
b.Navigation("Author");
|
||||||
|
|
||||||
b.Navigation("Headings");
|
|
||||||
|
|
||||||
b.Navigation("Reviewer");
|
b.Navigation("Reviewer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,4 @@
|
||||||
<data name="Deleted_Label" xml:space="preserve">
|
<data name="Deleted_Label" xml:space="preserve">
|
||||||
<value>Gelöscht</value>
|
<value>Gelöscht</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Subscribers_Label" xml:space="preserve">
|
|
||||||
<value>Abonnenten</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
|
@ -137,7 +137,4 @@
|
||||||
<data name="Deleted_Label" xml:space="preserve">
|
<data name="Deleted_Label" xml:space="preserve">
|
||||||
<value>Deleted</value>
|
<value>Deleted</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Subscribers_Label" xml:space="preserve">
|
|
||||||
<value>Subscribers</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
|
@ -158,7 +158,4 @@
|
||||||
<data name="Recommendations_Title" xml:space="preserve">
|
<data name="Recommendations_Title" xml:space="preserve">
|
||||||
<value>Das könnte Sie auch interessieren</value>
|
<value>Das könnte Sie auch interessieren</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TableOfContents" xml:space="preserve">
|
|
||||||
<value>Inhaltsübersicht</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
|
@ -158,7 +158,4 @@
|
||||||
<data name="Recommendations_Title" xml:space="preserve">
|
<data name="Recommendations_Title" xml:space="preserve">
|
||||||
<value>This might also interest you</value>
|
<value>This might also interest you</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TableOfContents" xml:space="preserve">
|
|
||||||
<value>Table of Content</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
|
@ -1,125 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 1.3
|
|
||||||
|
|
||||||
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">1.3</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1">this is my long string</data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
[base64 mime encoded serialized .NET Framework object]
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
|
||||||
</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.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:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<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" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</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>1.3</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Abonnent Bearbeiten</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name_Placeholder" xml:space="preserve">
|
|
||||||
<value>Anna Muster</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name_Label" xml:space="preserve">
|
|
||||||
<value>Name (Optional)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Subscribed_Label" xml:space="preserve">
|
|
||||||
<value>Aktiv (erhält E-Mails)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Submit" xml:space="preserve">
|
|
||||||
<value>Speichern</value>
|
|
||||||
</data>
|
|
||||||
<data name="Error_NotFound" xml:space="preserve">
|
|
||||||
<value>Abonnent nicht gefunden (Vielleicht wurde dieser gelöscht?)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Submit_Error" xml:space="preserve">
|
|
||||||
<value>Unbekannter Fehler beim Speichern ihrer Änderungen.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Submit_Success" xml:space="preserve">
|
|
||||||
<value>Ihre Änderungen wurden erfolgreich gespeichert</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
|
@ -1,101 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 1.3
|
|
||||||
|
|
||||||
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">1.3</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1">this is my long string</data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
[base64 mime encoded serialized .NET Framework object]
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
|
||||||
</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.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:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<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" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</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>1.3</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
|
@ -1,125 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 1.3
|
|
||||||
|
|
||||||
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">1.3</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1">this is my long string</data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
[base64 mime encoded serialized .NET Framework object]
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
|
||||||
</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.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:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<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" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</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>1.3</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Edit Subscriber</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name_Label" xml:space="preserve">
|
|
||||||
<value>Name (optional)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name_Placeholder" xml:space="preserve">
|
|
||||||
<value>John Smith</value>
|
|
||||||
</data>
|
|
||||||
<data name="Subscribed_Label" xml:space="preserve">
|
|
||||||
<value>Active (receives Emails)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Submit" xml:space="preserve">
|
|
||||||
<value>Save</value>
|
|
||||||
</data>
|
|
||||||
<data name="Error_NotFound" xml:space="preserve">
|
|
||||||
<value>Subscriber not Found (maybe it has been deleted?)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Submit_Error" xml:space="preserve">
|
|
||||||
<value>Unexpected Error trying to save your changes.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Submit_Success" xml:space="preserve">
|
|
||||||
<value>Changes saved Successfully</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
|
@ -137,7 +137,4 @@
|
||||||
<data name="WelcomeEmailBody" xml:space="preserve">
|
<data name="WelcomeEmailBody" xml:space="preserve">
|
||||||
<value>Sie werden von nun an über neue Artikel informiert. Schauen Sie doch mal in der folgenden Sektion was Sie vielleicht verpasst haben. Sie können sich jeder Zeit in diesem oder zukünftigen E-Mails abmelden über den Link den Sie am ende finden.</value>
|
<value>Sie werden von nun an über neue Artikel informiert. Schauen Sie doch mal in der folgenden Sektion was Sie vielleicht verpasst haben. Sie können sich jeder Zeit in diesem oder zukünftigen E-Mails abmelden über den Link den Sie am ende finden.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Name_Label" xml:space="preserve">
|
|
||||||
<value>Name (Optional)</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
|
@ -102,13 +102,13 @@
|
||||||
<value>Subscribe</value>
|
<value>Subscribe</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Name_Label" xml:space="preserve">
|
<data name="Name_Label" xml:space="preserve">
|
||||||
<value>Name (optional)</value>
|
<value>Name</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Name_Placeholder" xml:space="preserve">
|
<data name="Name_Placeholder" xml:space="preserve">
|
||||||
<value>John Doe</value>
|
<value>John Doe</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Email_Label" xml:space="preserve">
|
<data name="Email_Label" xml:space="preserve">
|
||||||
<value>Email*</value>
|
<value>Email</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Email_Placeholder" xml:space="preserve">
|
<data name="Email_Placeholder" xml:space="preserve">
|
||||||
<value>john.doe@example.com</value>
|
<value>john.doe@example.com</value>
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 1.3
|
|
||||||
|
|
||||||
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">1.3</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1">this is my long string</data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
[base64 mime encoded serialized .NET Framework object]
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
|
||||||
</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.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:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<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" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</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>1.3</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Abonnenten</value>
|
|
||||||
</data>
|
|
||||||
<data name="Paging_Next" xml:space="preserve">
|
|
||||||
<value>Nächste Seite</value>
|
|
||||||
</data>
|
|
||||||
<data name="Paging_Page" xml:space="preserve">
|
|
||||||
<value>Seite</value>
|
|
||||||
</data>
|
|
||||||
<data name="Paging_Previous" xml:space="preserve">
|
|
||||||
<value>Vorherige Seite</value>
|
|
||||||
</data>
|
|
||||||
<data name="Subscriber_Load_Error" xml:space="preserve">
|
|
||||||
<value>Unerwarteter Fehler beim laden der Abonnenten</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_Email" xml:space="preserve">
|
|
||||||
<value>E-Mail</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_UnsubscribeReason" xml:space="preserve">
|
|
||||||
<value>Notiz</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_Subscribed" xml:space="preserve">
|
|
||||||
<value>Angemeldet</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_LastReceived" xml:space="preserve">
|
|
||||||
<value>Zuletzt Zugestellt</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_LastOpen" xml:space="preserve">
|
|
||||||
<value>Zuletzt Geöffnet</value>
|
|
||||||
</data>
|
|
||||||
<data name="Newsletter_Footer_Timezone" xml:space="preserve">
|
|
||||||
<value>Alle Uhrzeiten sind in der folgenden Zeitzone:</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Parse_Error" xml:space="preserve">
|
|
||||||
<value>Fehler beim einlesen von Abonennent(en): {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Save_Error" xml:space="preserve">
|
|
||||||
<value>Fehler beim speichern von Abonennent(en): {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Label" xml:space="preserve">
|
|
||||||
<value>Abonnent(en) Hinzufügen</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Input_Label" xml:space="preserve">
|
|
||||||
<value>Abonnent(en) [CSV]</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Input_Placeholder" xml:space="preserve">
|
|
||||||
<value>anna.muster@example.de; Anna Muster; Spam;
|
|
||||||
peter.muster@example.de;;;</value>
|
|
||||||
</data>
|
|
||||||
<data name="Subscriber_Edit" xml:space="preserve">
|
|
||||||
<value>Bearbeiten</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
|
@ -1,101 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 1.3
|
|
||||||
|
|
||||||
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">1.3</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1">this is my long string</data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
[base64 mime encoded serialized .NET Framework object]
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
|
||||||
</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.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:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<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" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</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>1.3</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
|
@ -1,156 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 1.3
|
|
||||||
|
|
||||||
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">1.3</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1">this is my long string</data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
[base64 mime encoded serialized .NET Framework object]
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
|
||||||
</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.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:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<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" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</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>1.3</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Subscribers</value>
|
|
||||||
</data>
|
|
||||||
<data name="Paging_Next" xml:space="preserve">
|
|
||||||
<value>Next page</value>
|
|
||||||
</data>
|
|
||||||
<data name="Paging_Page" xml:space="preserve">
|
|
||||||
<value>Page</value>
|
|
||||||
</data>
|
|
||||||
<data name="Paging_Previous" xml:space="preserve">
|
|
||||||
<value>Previous page</value>
|
|
||||||
</data>
|
|
||||||
<data name="Subscriber_Load_Error" xml:space="preserve">
|
|
||||||
<value>Unknown error loading subscribers</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_Email" xml:space="preserve">
|
|
||||||
<value>Email</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_Name" xml:space="preserve">
|
|
||||||
<value>Name</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_UnsubscribeReason" xml:space="preserve">
|
|
||||||
<value>Note</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_Subscribed" xml:space="preserve">
|
|
||||||
<value>Subscribed</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_LastReceived" xml:space="preserve">
|
|
||||||
<value>Last Received</value>
|
|
||||||
</data>
|
|
||||||
<data name="Header_LastOpen" xml:space="preserve">
|
|
||||||
<value>Last Open</value>
|
|
||||||
</data>
|
|
||||||
<data name="Newsletter_Footer_Timezone" xml:space="preserve">
|
|
||||||
<value>All times are using this timezone:</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Parse_Error" xml:space="preserve">
|
|
||||||
<value>Failed to parse subscriber(s): {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Save_Error" xml:space="preserve">
|
|
||||||
<value>Failed to save subscriber(s): {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Label" xml:space="preserve">
|
|
||||||
<value>Add Subscriber(s)</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Input_Label" xml:space="preserve">
|
|
||||||
<value>Subscriber(s) [CSV]</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSubscribers_Input_Placeholder" xml:space="preserve">
|
|
||||||
<value>john.smith@example.com; John Smith; Spam;
|
|
||||||
jay.smith@example.com;;;</value>
|
|
||||||
</data>
|
|
||||||
<data name="Subscriber_Edit" xml:space="preserve">
|
|
||||||
<value>Edit</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
|
@ -1,6 +1,5 @@
|
||||||
using ColorCode.Styling;
|
using ColorCode.Styling;
|
||||||
using Markdig;
|
using Markdig;
|
||||||
using Markdig.Extensions.AutoIdentifiers;
|
|
||||||
using Markdig.Extensions.MediaLinks;
|
using Markdig.Extensions.MediaLinks;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Markdown.ColorCode;
|
using Markdown.ColorCode;
|
||||||
|
@ -11,7 +10,6 @@ namespace Wave.Utilities;
|
||||||
public static class MarkdownUtilities {
|
public static class MarkdownUtilities {
|
||||||
public static string Parse(string markdown) {
|
public static string Parse(string markdown) {
|
||||||
var pipeline = new MarkdownPipelineBuilder()
|
var pipeline = new MarkdownPipelineBuilder()
|
||||||
.UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
|
|
||||||
.UsePipeTables()
|
.UsePipeTables()
|
||||||
.UseEmphasisExtras()
|
.UseEmphasisExtras()
|
||||||
.UseListExtras()
|
.UseListExtras()
|
||||||
|
|
|
@ -9,7 +9,6 @@ namespace Wave.Utilities;
|
||||||
public static class Permissions {
|
public static class Permissions {
|
||||||
public static bool AllowedToRead(this Article? article, ClaimsPrincipal principal) {
|
public static bool AllowedToRead(this Article? article, ClaimsPrincipal principal) {
|
||||||
if (article is null || article.IsDeleted) return false;
|
if (article is null || article.IsDeleted) return false;
|
||||||
if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author.");
|
|
||||||
|
|
||||||
// The Article is publicly available
|
// The Article is publicly available
|
||||||
if (article.Status >= ArticleStatus.Published && article.PublishDate <= DateTimeOffset.UtcNow) {
|
if (article.Status >= ArticleStatus.Published && article.PublishDate <= DateTimeOffset.UtcNow) {
|
||||||
|
@ -36,7 +35,6 @@ public static class Permissions {
|
||||||
|
|
||||||
public static bool AllowedToEdit(this Article? article, ClaimsPrincipal principal) {
|
public static bool AllowedToEdit(this Article? article, ClaimsPrincipal principal) {
|
||||||
if (article is null || article.IsDeleted) return false;
|
if (article is null || article.IsDeleted) return false;
|
||||||
if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author.");
|
|
||||||
|
|
||||||
// Admins always can edit articles
|
// Admins always can edit articles
|
||||||
if (principal.IsInRole("Admin")) {
|
if (principal.IsInRole("Admin")) {
|
||||||
|
@ -69,14 +67,8 @@ public static class Permissions {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool AllowedToRejectReview(this Article? article, ClaimsPrincipal principal) {
|
|
||||||
// if you can publish it, you can reject it
|
|
||||||
return article?.Status is ArticleStatus.InReview && article.AllowedToPublish(principal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool AllowedToSubmitForReview(this Article? article, ClaimsPrincipal principal) {
|
public static bool AllowedToSubmitForReview(this Article? article, ClaimsPrincipal principal) {
|
||||||
if (article is null || article.IsDeleted) return false;
|
if (article is null || article.IsDeleted) return false;
|
||||||
if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author.");
|
|
||||||
|
|
||||||
// Draft articles can be submitted by their authors (admins can publish them anyway, no need to submit)
|
// Draft articles can be submitted by their authors (admins can publish them anyway, no need to submit)
|
||||||
if (article.Status is ArticleStatus.Draft && article.Author.Id == principal.FindFirst("Id")!.Value) {
|
if (article.Status is ArticleStatus.Draft && article.Author.Id == principal.FindFirst("Id")!.Value) {
|
||||||
|
@ -88,7 +80,6 @@ public static class Permissions {
|
||||||
|
|
||||||
public static bool AllowedToPublish(this Article? article, ClaimsPrincipal principal) {
|
public static bool AllowedToPublish(this Article? article, ClaimsPrincipal principal) {
|
||||||
if (article is null || article.IsDeleted) return false;
|
if (article is null || article.IsDeleted) return false;
|
||||||
if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author.");
|
|
||||||
|
|
||||||
// Admins can skip review and directly publish draft articles
|
// Admins can skip review and directly publish draft articles
|
||||||
if (article.Status is ArticleStatus.Draft && principal.IsInRole("Admin")) {
|
if (article.Status is ArticleStatus.Draft && principal.IsInRole("Admin")) {
|
||||||
|
@ -111,7 +102,6 @@ public static class Permissions {
|
||||||
|
|
||||||
public static bool AllowedToDelete(this Article? article, ClaimsPrincipal principal) {
|
public static bool AllowedToDelete(this Article? article, ClaimsPrincipal principal) {
|
||||||
if (article is null || article.IsDeleted) return false;
|
if (article is null || article.IsDeleted) return false;
|
||||||
if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author.");
|
|
||||||
|
|
||||||
// Admins can delete articles whenever
|
// Admins can delete articles whenever
|
||||||
if (principal.IsInRole("Admin")) {
|
if (principal.IsInRole("Admin")) {
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="8.0.1" />
|
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="8.0.1" />
|
||||||
<PackageReference Include="CsvHelper" Version="31.0.4" />
|
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||||
<PackageReference Include="Humanizer.Core.de" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core.de" Version="2.14.1" />
|
||||||
<PackageReference Include="Humanizer.Core.uk" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core.uk" Version="2.14.1" />
|
||||||
|
|
|
@ -14,9 +14,6 @@ module.exports = {
|
||||||
'6xl': "2560px",
|
'6xl': "2560px",
|
||||||
'8xl': "3072px"
|
'8xl': "3072px"
|
||||||
},
|
},
|
||||||
height: {
|
|
||||||
'128': "32rem"
|
|
||||||
},
|
|
||||||
container: {
|
container: {
|
||||||
'max-width': {
|
'max-width': {
|
||||||
'3xl': "1792px",
|
'3xl': "1792px",
|
||||||
|
|
|
@ -51,8 +51,4 @@ @layer components {
|
||||||
.prose pre:has(code) {
|
.prose pre:has(code) {
|
||||||
@apply border-2 border-current;
|
@apply border-2 border-current;
|
||||||
}
|
}
|
||||||
|
|
||||||
.characters-left {
|
|
||||||
@apply absolute right-6 bottom-6 select-none pointer-events-none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
2
Wave/wwwroot/css/main.min.css
vendored
2
Wave/wwwroot/css/main.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue