postit and them also
This commit is contained in:
parent
de616809ae
commit
fa7d6242f1
18 changed files with 5322 additions and 121 deletions
12
.env.sample
12
.env.sample
|
|
@ -1,18 +1,6 @@
|
||||||
APP_NAME=Yavsc
|
|
||||||
POSTGRES_HOST=localhost
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
POSTGRES_DB=yavsc
|
|
||||||
POSTGRES_USER=yavsc
|
|
||||||
POSTGRES_PASSWORD=lame-YAVSC_CONNECTION_PASSWORD
|
|
||||||
ASPNETCORE_ConnectionStrings__YavscConnection=Server=$POSTGRES_HOST;Port=$POSTGRES_PORT;Database=$POSTGRES_DB;Username=$POSTGRES_USER;Password=$POSTGRES_PASSWORD;
|
|
||||||
|
|
||||||
ANTHROPIC_API_KEY=<votre-clé-api-anthropic>
|
ANTHROPIC_API_KEY=<votre-clé-api-anthropic>
|
||||||
MODERATION_AUTO_REJECT_THRESHOLD=0.9
|
MODERATION_AUTO_REJECT_THRESHOLD=0.9
|
||||||
MODERATION_AUTO_APPROVE_THRESHOLD=0.7
|
MODERATION_AUTO_APPROVE_THRESHOLD=0.7
|
||||||
# Limite par appel
|
# Limite par appel
|
||||||
ANTHROPIC_MAX_TOKENS=256 # la modération n'a pas besoin de plus
|
ANTHROPIC_MAX_TOKENS=256 # la modération n'a pas besoin de plus
|
||||||
|
|
||||||
YAVSC_API_HOST=127.0.0.1
|
|
||||||
YAVSC_API_PORT=6001
|
|
||||||
|
|
||||||
ASPNETCORE_Kestrel__Endpoints__Https__Url="https://$YAVSC_API_HOST:$YAVSC_API_PORT"
|
|
||||||
|
|
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -64,7 +64,7 @@
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"envFile": "${workspaceFolder}/src/Yavsc.Org/.env",
|
"envFile": "${workspaceFolder}/.env",
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/src/Yavsc.Org/Views"
|
"/Views": "${workspaceFolder}/src/Yavsc.Org/Views"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/PostIt/PostIt/Models/BlogPost.cs
Normal file
16
src/PostIt/PostIt/Models/BlogPost.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PostIt.Models;
|
||||||
|
|
||||||
|
public class BlogPost
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string? Article { get; set; }
|
||||||
|
public string? Photo { get; set; }
|
||||||
|
public string? AuthorId { get; set; }
|
||||||
|
public DateTime DateCreated { get; set; }
|
||||||
|
public string? UserCreated { get; set; }
|
||||||
|
public DateTime DateModified { get; set; }
|
||||||
|
public string? UserModified { get; set; }
|
||||||
|
}
|
||||||
78
src/PostIt/PostIt/Services/BlogApiClient.cs
Normal file
78
src/PostIt/PostIt/Services/BlogApiClient.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PostIt.Models;
|
||||||
|
|
||||||
|
namespace PostIt.Services;
|
||||||
|
|
||||||
|
public sealed class BlogApiClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions;
|
||||||
|
|
||||||
|
public BlogApiClient(string baseUrl, string? bearerToken = null)
|
||||||
|
: this(CreateHttpClient(baseUrl, bearerToken))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlogApiClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
_serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient(string baseUrl, string? bearerToken)
|
||||||
|
{
|
||||||
|
var client = new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(bearerToken))
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<BlogPost>> GetPostsAsync(int start = 0, int take = 25)
|
||||||
|
{
|
||||||
|
var result = await _httpClient.GetFromJsonAsync<List<BlogPost>>($"api/blog?start={start}&take={take}", _serializerOptions).ConfigureAwait(false);
|
||||||
|
return result ?? new List<BlogPost>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<BlogPost?> GetPostAsync(long id)
|
||||||
|
=> _httpClient.GetFromJsonAsync<BlogPost>($"api/blog/{id}", _serializerOptions);
|
||||||
|
|
||||||
|
public async Task<BlogPost?> CreatePostAsync(BlogPost post)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("api/blog", post, _serializerOptions).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadFromJsonAsync<BlogPost>(_serializerOptions).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePostAsync(long id, BlogPost post)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PutAsJsonAsync($"api/blog/{id}", post, _serializerOptions).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeletePostAsync(long id)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.DeleteAsync($"api/blog/{id}").ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_httpClient.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,216 @@
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using PostIt.Models;
|
||||||
|
using PostIt.Services;
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
namespace PostIt.ViewModels;
|
||||||
|
|
||||||
public partial class MainViewModel : ViewModelBase
|
public partial class MainViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _greeting = "Welcome to Avalonia!";
|
private string _apiUrl = "http://localhost:5000";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _searchText = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? _bearerToken;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<BlogPost> _posts = new();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<BlogPost> _filteredPosts = new();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private BlogPost? _selectedPost;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _statusMessage = "Ready";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isBusy;
|
||||||
|
|
||||||
|
public MainViewModel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSearchTextChanged(string value)
|
||||||
|
{
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedPostChanged(BlogPost? value)
|
||||||
|
{
|
||||||
|
UpdateCommandStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsBusyChanged(bool value)
|
||||||
|
{
|
||||||
|
UpdateCommandStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task LoadPostsAsync()
|
||||||
|
{
|
||||||
|
await ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
using var client = CreateClient();
|
||||||
|
var posts = await client.GetPostsAsync();
|
||||||
|
Posts.Clear();
|
||||||
|
foreach (var post in posts.OrderByDescending(p => p.DateModified))
|
||||||
|
{
|
||||||
|
Posts.Add(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyFilter();
|
||||||
|
StatusMessage = $"Loaded {Posts.Count} posts.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void Search()
|
||||||
|
{
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
if (SelectedPost is null)
|
||||||
|
{
|
||||||
|
StatusMessage = "A post must be selected before saving.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
using var client = CreateClient();
|
||||||
|
|
||||||
|
if (SelectedPost.Id == 0)
|
||||||
|
{
|
||||||
|
SelectedPost.DateCreated = DateTime.UtcNow;
|
||||||
|
SelectedPost.DateModified = DateTime.UtcNow;
|
||||||
|
var created = await client.CreatePostAsync(SelectedPost);
|
||||||
|
if (created is not null)
|
||||||
|
{
|
||||||
|
SelectedPost = created;
|
||||||
|
StatusMessage = $"Created post {created.Id}.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedPost.DateModified = DateTime.UtcNow;
|
||||||
|
await client.UpdatePostAsync(SelectedPost.Id, SelectedPost);
|
||||||
|
StatusMessage = $"Saved post {SelectedPost.Id}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
await RefreshPostsAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task DeleteAsync()
|
||||||
|
{
|
||||||
|
if (SelectedPost is null || SelectedPost.Id == 0)
|
||||||
|
{
|
||||||
|
StatusMessage = "Select an existing post before deleting.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
using var client = CreateClient();
|
||||||
|
await client.DeletePostAsync(SelectedPost.Id);
|
||||||
|
StatusMessage = $"Deleted post {SelectedPost.Id}.";
|
||||||
|
SelectedPost = null;
|
||||||
|
await RefreshPostsAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void New()
|
||||||
|
{
|
||||||
|
SelectedPost = new BlogPost
|
||||||
|
{
|
||||||
|
Title = string.Empty,
|
||||||
|
Article = string.Empty,
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
StatusMessage = "New blog post ready.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshPostsAsync()
|
||||||
|
{
|
||||||
|
using var client = CreateClient();
|
||||||
|
var posts = await client.GetPostsAsync();
|
||||||
|
Posts.Clear();
|
||||||
|
foreach (var post in posts.OrderByDescending(p => p.DateModified))
|
||||||
|
{
|
||||||
|
Posts.Add(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyFilter();
|
||||||
|
|
||||||
|
if (SelectedPost is not null)
|
||||||
|
{
|
||||||
|
SelectedPost = Posts.FirstOrDefault(post => post.Id == SelectedPost.Id) ?? SelectedPost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
var query = SearchText?.Trim();
|
||||||
|
var filtered = string.IsNullOrWhiteSpace(query)
|
||||||
|
? Posts.OrderByDescending(p => p.DateModified)
|
||||||
|
: Posts.Where(p => p.Title?.Contains(query, StringComparison.OrdinalIgnoreCase) == true
|
||||||
|
|| p.Article?.Contains(query, StringComparison.OrdinalIgnoreCase) == true
|
||||||
|
|| p.AuthorId?.Contains(query, StringComparison.OrdinalIgnoreCase) == true)
|
||||||
|
.OrderByDescending(p => p.DateModified);
|
||||||
|
|
||||||
|
FilteredPosts.Clear();
|
||||||
|
foreach (var post in filtered)
|
||||||
|
{
|
||||||
|
FilteredPosts.Add(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteAsync(Func<Task> action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
StatusMessage = "Working...";
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Error: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlogApiClient CreateClient()
|
||||||
|
=> new BlogApiClient(ApiUrl, BearerToken);
|
||||||
|
|
||||||
|
private void UpdateCommandStates()
|
||||||
|
{
|
||||||
|
LoadPostsCommand.NotifyCanExecuteChanged();
|
||||||
|
SaveCommand.NotifyCanExecuteChanged();
|
||||||
|
DeleteCommand.NotifyCanExecuteChanged();
|
||||||
|
NewCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanSave() => SelectedPost is not null && !IsBusy;
|
||||||
|
private bool CanDelete() => SelectedPost is not null && SelectedPost.Id != 0 && !IsBusy;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,68 @@
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="using:PostIt.ViewModels"
|
xmlns:vm="using:PostIt.ViewModels"
|
||||||
|
xmlns:models="using:PostIt.Models"
|
||||||
xmlns:views="using:PostIt.Views"
|
xmlns:views="using:PostIt.Views"
|
||||||
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
|
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
|
||||||
x:Class="PostIt.Views.MainView"
|
x:Class="PostIt.Views.MainView"
|
||||||
x:DataType="vm:MainViewModel">
|
x:DataType="vm:MainViewModel">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
|
||||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
|
||||||
<vm:MainViewModel />
|
<vm:MainViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<StackPanel>
|
|
||||||
|
|
||||||
<Label>Hello</Label>
|
<StackPanel Margin="12" Spacing="12">
|
||||||
<AvaloniaEdit:TextEditor
|
<TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" />
|
||||||
ShowLineNumbers="True"
|
|
||||||
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
|
<Grid ColumnDefinitions="Auto,1*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||||
Background="AliceBlue"
|
<TextBlock Text="API URL" VerticalAlignment="Center" />
|
||||||
Foreground="Black"
|
<TextBox Grid.Column="1" Text="{Binding ApiUrl, Mode=TwoWay}" />
|
||||||
Watermark="Hit me strong!"
|
|
||||||
/>
|
<TextBlock Grid.Row="1" Text="Bearer token" VerticalAlignment="Center" />
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding BearerToken, Mode=TwoWay}" PlaceholderText="Optional token for blog scope" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="Search" VerticalAlignment="Center" />
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SearchText, Mode=TwoWay}" PlaceholderText="Search title, article, author" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="3" Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Command="{Binding LoadPostsCommand}" Content="Load posts" />
|
||||||
|
<Button Command="{Binding SearchCommand}" Content="Filter" />
|
||||||
|
<Button Command="{Binding NewCommand}" Content="New post" />
|
||||||
|
<Button Command="{Binding SaveCommand}" Content="Save" />
|
||||||
|
<Button Command="{Binding DeleteCommand}" Content="Delete" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="2*,3*" RowDefinitions="*" ColumnSpacing="12">
|
||||||
|
<Border BorderBrush="Gray" BorderThickness="1" Padding="8">
|
||||||
|
<ListBox ItemsSource="{Binding FilteredPosts}" SelectedItem="{Binding SelectedPost, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:BlogPost">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="{Binding DateModified, StringFormat='Updated: {0:yyyy-MM-dd HH:mm}'}" FontSize="10" Foreground="Gray" />
|
||||||
|
<TextBlock Text="{Binding AuthorId}" FontSize="10" Foreground="DarkSlateGray" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border BorderBrush="Gray" BorderThickness="1" Padding="8">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Post detail" FontWeight="SemiBold" />
|
||||||
|
<TextBox Text="{Binding SelectedPost.Title, Mode=TwoWay}" PlaceholderText="Title" />
|
||||||
|
<TextBox Text="{Binding SelectedPost.AuthorId, Mode=TwoWay}" PlaceholderText="Author id" />
|
||||||
|
<AvaloniaEdit:TextEditor
|
||||||
|
views:TextEditorBinding.Text="{Binding SelectedPost.Article, Mode=TwoWay}"
|
||||||
|
ShowLineNumbers="True"
|
||||||
|
FontFamily="Cascadia Code, Consolas, Menlo, Monospace"
|
||||||
|
Height="320"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Auto" />
|
||||||
|
<TextBlock Text="{Binding StatusMessage}" Foreground="Gray" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Blog;
|
using Yavsc.Models.Blog;
|
||||||
|
using Yavsc.Server.Exceptions;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
|
|
||||||
namespace Yavsc.Controllers
|
namespace Yavsc.Controllers
|
||||||
|
|
@ -13,43 +15,48 @@ namespace Yavsc.Controllers
|
||||||
|
|
||||||
public class BlogApiController : Controller
|
public class BlogApiController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly BlogSpotService blogSpotService;
|
||||||
|
|
||||||
public BlogApiController(ApplicationDbContext context)
|
public BlogApiController(BlogSpotService blogSpotService)
|
||||||
{
|
{
|
||||||
_context = context;
|
this.blogSpotService = blogSpotService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/BlogApi
|
// GET: api/BlogApi
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IEnumerable<BlogPost> GetBlogspot(int start=0, int take=25)
|
public async Task<IEnumerable<IBlogPost>> GetBlogspot(int start = 0, int take = 25)
|
||||||
{
|
{
|
||||||
return _context.BlogSpot.OrderByDescending(b => b.UserModified)
|
return await blogSpotService.Index(User, null, start, take);
|
||||||
.Skip(start).Take(take);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/BlogApi/5
|
// GET: api/BlogApi/5
|
||||||
[HttpGet("{id}", Name = "GetBlog")]
|
[HttpGet("{id}", Name = "GetBlog")]
|
||||||
public IActionResult GetBlog([FromRoute] long id)
|
public async Task<IActionResult> GetBlog([FromRoute] long id)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return BadRequest(ModelState);
|
return BadRequest(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlogPost blog = _context.BlogSpot.Single(m => m.Id == id);
|
try
|
||||||
|
|
||||||
if (blog == null)
|
|
||||||
{
|
{
|
||||||
return NotFound();
|
var blog = await blogSpotService.Details(User, id);
|
||||||
}
|
if (blog == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(blog);
|
return Ok(blog);
|
||||||
|
}
|
||||||
|
catch (AuthorizationFailureException)
|
||||||
|
{
|
||||||
|
return Challenge();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT: api/BlogApi/5
|
// PUT: api/BlogApi/5
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
public IActionResult PutBlog(long id, [FromBody] BlogPost blog)
|
public async Task<IActionResult> PutBlog(long id, [FromBody] BlogPost blog)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
|
|
@ -61,22 +68,19 @@ namespace Yavsc.Controllers
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.Entry(blog).State = EntityState.Modified;
|
var existing = await blogSpotService.GetBlogPostAsync(id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_context.SaveChanges(User.GetUserId());
|
await blogSpotService.Modify(User, blog);
|
||||||
}
|
}
|
||||||
catch (DbUpdateConcurrencyException)
|
catch (AuthorizationFailureException)
|
||||||
{
|
{
|
||||||
if (!BlogExists(id))
|
return Challenge();
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StatusCodeResult(StatusCodes.Status204NoContent);
|
return new StatusCodeResult(StatusCodes.Status204NoContent);
|
||||||
|
|
@ -91,59 +95,32 @@ namespace Yavsc.Controllers
|
||||||
return BadRequest(ModelState);
|
return BadRequest(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.BlogSpot.Add(blog);
|
var post = blogSpotService.Create(User.GetUserId(), blog, Request.Form.Files);
|
||||||
try
|
return CreatedAtRoute("GetBlog", new { id = post.Id }, post);
|
||||||
{
|
|
||||||
_context.SaveChanges(User.GetUserId());
|
|
||||||
}
|
|
||||||
catch (DbUpdateException)
|
|
||||||
{
|
|
||||||
if (BlogExists(blog.Id))
|
|
||||||
{
|
|
||||||
return new StatusCodeResult(StatusCodes.Status409Conflict);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreatedAtRoute("GetBlog", new { id = blog.Id }, blog);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: api/BlogApi/5
|
// DELETE: api/BlogApi/5
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
public IActionResult DeleteBlog(long id)
|
public async Task<IActionResult> DeleteBlog(long id)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return BadRequest(ModelState);
|
return BadRequest(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlogPost blog = _context.BlogSpot.Single(m => m.Id == id);
|
var blog = await blogSpotService.GetBlogPostAsync(id);
|
||||||
if (blog == null)
|
if (blog == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.BlogSpot.Remove(blog);
|
await blogSpotService.Delete(User, id);
|
||||||
_context.SaveChanges(User.GetUserId());
|
|
||||||
|
|
||||||
return Ok(blog);
|
return Ok(blog);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_context.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BlogExists(long id)
|
|
||||||
{
|
|
||||||
return _context.BlogSpot.Count(e => e.Id == id) > 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Yavsc;
|
using Yavsc;
|
||||||
|
|
@ -19,6 +20,7 @@ using Yavsc.Interface;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Services;
|
using Yavsc.Services;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
|
using Yavsc.Extensions;
|
||||||
|
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
|
|
@ -78,6 +80,8 @@ internal class Program
|
||||||
.AddTransient<IBillingService, BillingService>()
|
.AddTransient<IBillingService, BillingService>()
|
||||||
.AddTransient<ICalendarManager, CalendarManager>();
|
.AddTransient<ICalendarManager, CalendarManager>();
|
||||||
services.AddTransient<IFileSystemAuthManager, FileSystemAuthManager>();
|
services.AddTransient<IFileSystemAuthManager, FileSystemAuthManager>();
|
||||||
|
services.AddTransient<BlogSpotService>();
|
||||||
|
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
|
||||||
|
|
||||||
services.AddLocalization(options =>
|
services.AddLocalization(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<ProjectReference Include="../Yavsc.Server/Yavsc.Server.csproj" />
|
<ProjectReference Include="../Yavsc.Server/Yavsc.Server.csproj" />
|
||||||
|
<ProjectReference Include="../Yavsc.Org/Yavsc.Org.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
4627
src/Yavsc.Org/Migrations/20260609224137_AddBlogFileAttachments.Designer.cs
generated
Normal file
4627
src/Yavsc.Org/Migrations/20260609224137_AddBlogFileAttachments.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,69 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Yavsc.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBlogFileAttachments : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UploadedFiles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Path = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Length = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ContentType = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UploadedFiles", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BlogAttachedFiles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
FileId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
PostId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BlogAttachedFiles", x => new { x.FileId, x.PostId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_BlogAttachedFiles_BlogSpot_PostId",
|
||||||
|
column: x => x.PostId,
|
||||||
|
principalTable: "BlogSpot",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_BlogAttachedFiles_UploadedFiles_FileId",
|
||||||
|
column: x => x.FileId,
|
||||||
|
principalTable: "UploadedFiles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BlogAttachedFiles_PostId",
|
||||||
|
table: "BlogAttachedFiles",
|
||||||
|
column: "PostId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BlogAttachedFiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UploadedFiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1413,6 +1413,21 @@ namespace Yavsc.Migrations
|
||||||
b.ToTable("ExceptionsSIREN");
|
b.ToTable("ExceptionsSIREN");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("FileId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("PostId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("FileId", "PostId");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("BlogAttachedFiles");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b =>
|
modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
|
|
@ -1518,6 +1533,28 @@ namespace Yavsc.Migrations
|
||||||
b.ToTable("Comment");
|
b.ToTable("Comment");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<long>("Length")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("UploadedFiles");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b =>
|
modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("BlogpostId")
|
b.Property<long>("BlogpostId")
|
||||||
|
|
@ -3700,6 +3737,25 @@ namespace Yavsc.Migrations
|
||||||
b.Navigation("Query");
|
b.Navigation("Query");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Yavsc.Models.Blog.UploadedFile", "File")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FileId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Yavsc.Models.Blog.BlogPost", "Post")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PostId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("File");
|
||||||
|
|
||||||
|
b.Navigation("Post");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b =>
|
modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Yavsc.Models.ApplicationUser", "Author")
|
b.HasOne("Yavsc.Models.ApplicationUser", "Author")
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "https://localhost:5001",
|
|
||||||
"sslPort": 5001
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"applicationUrl": "https://localhost:5001;",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,8 @@ using Yavsc.Server.Exceptions;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
using Yavsc.Services;
|
using Yavsc.Services;
|
||||||
using Yavsc.ViewModels.Auth;
|
using Yavsc.ViewModels.Auth;
|
||||||
|
using Yavsc.Abstract.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
public class BlogSpotService
|
public class BlogSpotService
|
||||||
{
|
{
|
||||||
|
|
@ -29,11 +31,63 @@ public class BlogSpotService
|
||||||
|
|
||||||
public BlogPost Create(string userId, BlogPost post, IFormFileCollection files)
|
public BlogPost Create(string userId, BlogPost post, IFormFileCollection files)
|
||||||
{
|
{
|
||||||
foreach (var file in files)
|
// Sauvegarder le post d'abord pour obtenir son ID
|
||||||
{
|
|
||||||
}
|
|
||||||
_context.BlogSpot.Add(post);
|
_context.BlogSpot.Add(post);
|
||||||
_context.SaveChanges(userId);
|
_context.SaveChanges(userId);
|
||||||
|
|
||||||
|
// Traiter les fichiers attachés s'il y en a
|
||||||
|
if (files != null && files.Count > 0)
|
||||||
|
{
|
||||||
|
var user = _context.Users.FirstOrDefault(u => u.Id == userId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Créer un répertoire pour les fichiers du blog
|
||||||
|
string blogFilesSubdir = $"blogs/{post.Id}";
|
||||||
|
string destDir = Path.Combine(
|
||||||
|
AbstractFileSystemHelpers.UserFilesDirName,
|
||||||
|
user.UserName,
|
||||||
|
blogFilesSubdir
|
||||||
|
);
|
||||||
|
var di = new DirectoryInfo(destDir);
|
||||||
|
if (!di.Exists) di.Create();
|
||||||
|
|
||||||
|
// Traiter chaque fichier
|
||||||
|
foreach (var formFile in files)
|
||||||
|
{
|
||||||
|
var fileInfo = user.ReceiveUserFile(destDir, formFile);
|
||||||
|
if (fileInfo != null && !fileInfo.QuotaOffense)
|
||||||
|
{
|
||||||
|
// Créer une entrée UploadedFile si nécessaire
|
||||||
|
var uploadedFile = new UploadedFile
|
||||||
|
{
|
||||||
|
Path = fileInfo.FileName,
|
||||||
|
ContentType = formFile.ContentType,
|
||||||
|
Length = formFile.Length
|
||||||
|
};
|
||||||
|
_context.UploadedFiles.Add(uploadedFile);
|
||||||
|
_context.SaveChanges(userId);
|
||||||
|
|
||||||
|
// Lier le fichier au post
|
||||||
|
var attachment = new BlogAttachedFile
|
||||||
|
{
|
||||||
|
PostId = post.Id,
|
||||||
|
FileId = uploadedFile.Id
|
||||||
|
};
|
||||||
|
_context.BlogAttachedFiles.Add(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.SaveChanges(userId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Logger l'erreur mais ne pas échouer la création du post
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Erreur lors du traitement des fichiers : {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
public async Task<BlogPostEditViewModel> GetPostForEdition(ClaimsPrincipal user, long blogPostId)
|
public async Task<BlogPostEditViewModel> GetPostForEdition(ClaimsPrincipal user, long blogPostId)
|
||||||
|
|
@ -112,6 +166,29 @@ public class BlogSpotService
|
||||||
_context.SaveChanges(user.GetUserId());
|
_context.SaveChanges(user.GetUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Modify(ClaimsPrincipal user, BlogPost blog)
|
||||||
|
{
|
||||||
|
var existing = await _context.BlogSpot.Include(b => b.ACL).SingleOrDefaultAsync(b => b.Id == blog.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Blog post {blog.Id} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var auth = await _authorizationService.AuthorizeAsync(user, existing, new EditPermission());
|
||||||
|
if (!auth.Succeeded)
|
||||||
|
{
|
||||||
|
throw new AuthorizationFailureException(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.Title = blog.Title;
|
||||||
|
existing.Article = blog.Article;
|
||||||
|
existing.Photo = blog.Photo;
|
||||||
|
existing.ACL = blog.ACL;
|
||||||
|
|
||||||
|
_context.Update(existing);
|
||||||
|
_context.SaveChanges(user.GetUserId());
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<IBlogPost>> Index(ClaimsPrincipal user, string id, int skip = 0, int take = 25)
|
public async Task<IEnumerable<IBlogPost>> Index(ClaimsPrincipal user, string id, int skip = 0, int take = 25)
|
||||||
{
|
{
|
||||||
IEnumerable<IBlogPost> posts;
|
IEnumerable<IBlogPost> posts;
|
||||||
|
|
@ -148,7 +225,9 @@ public class BlogSpotService
|
||||||
.Select(p => p.BlogPost).ToArray();
|
.Select(p => p.BlogPost).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = posts.OrderByDescending(p => p.DateModified);
|
var data = posts.OrderByDescending(p => p.DateModified)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -385,6 +385,10 @@ namespace Yavsc.Models
|
||||||
|
|
||||||
public DbSet<BlogSpotPublication> blogSpotPublications { get; set; }
|
public DbSet<BlogSpotPublication> blogSpotPublications { get; set; }
|
||||||
|
|
||||||
|
public DbSet<UploadedFile> UploadedFiles { get; set; }
|
||||||
|
|
||||||
|
public DbSet<BlogAttachedFile> BlogAttachedFiles { get; set; }
|
||||||
|
|
||||||
public DbSet<Client> Clients { get; set; }
|
public DbSet<Client> Clients { get; set; }
|
||||||
public DbSet<ClientIdPRestriction> ClientIdPRestrictions { get; set; }
|
public DbSet<ClientIdPRestriction> ClientIdPRestrictions { get; set; }
|
||||||
public DbSet<ClientProperty> ClientProperties { get; set; }
|
public DbSet<ClientProperty> ClientProperties { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Org.BouncyCastle.Crypto.Modes;
|
using Org.BouncyCastle.Crypto.Modes;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Yavsc.Models.Blog
|
namespace Yavsc.Models.Blog
|
||||||
{
|
{
|
||||||
|
|
@ -34,11 +35,12 @@ namespace Yavsc.Models.Blog
|
||||||
/// <value></value>
|
/// <value></value>
|
||||||
public string ContentType { get; set; }
|
public string ContentType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[PrimaryKey(nameof(FileId), nameof(PostId))]
|
||||||
public class BlogAttachedFile
|
public class BlogAttachedFile
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Post Id
|
/// File Id (part of composite key)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value></value>
|
/// <value></value>
|
||||||
public long FileId { get; set; }
|
public long FileId { get; set; }
|
||||||
|
|
@ -47,7 +49,7 @@ namespace Yavsc.Models.Blog
|
||||||
public virtual UploadedFile File { get; set; }
|
public virtual UploadedFile File { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Post Id
|
/// Post Id (part of composite key)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value></value>
|
/// <value></value>
|
||||||
public long PostId { get; set; }
|
public long PostId { get; set; }
|
||||||
|
|
|
||||||
79
test/yavscTests/NonRegression/PostItViewModelTests.cs
Normal file
79
test/yavscTests/NonRegression/PostItViewModelTests.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PostIt.Models;
|
||||||
|
using PostIt.Services;
|
||||||
|
using PostIt.ViewModels;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Yavsc.Tests.NonRegression;
|
||||||
|
|
||||||
|
public class PostItViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SearchCommand_filters_posts_by_title_article_or_author()
|
||||||
|
{
|
||||||
|
var viewModel = new MainViewModel();
|
||||||
|
|
||||||
|
viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" });
|
||||||
|
viewModel.Posts.Add(new BlogPost { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" });
|
||||||
|
viewModel.Posts.Add(new BlogPost { Id = 3, Title = "Third post", Article = "Search me", AuthorId = "carol" });
|
||||||
|
|
||||||
|
viewModel.SearchText = "search";
|
||||||
|
viewModel.SearchCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Single(viewModel.FilteredPosts);
|
||||||
|
Assert.Equal(3, viewModel.FilteredPosts[0].Id);
|
||||||
|
|
||||||
|
viewModel.SearchText = "bob";
|
||||||
|
viewModel.SearchCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Single(viewModel.FilteredPosts);
|
||||||
|
Assert.Equal(2, viewModel.FilteredPosts[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BlogApiClient_GetPostsAsync_returns_posts_from_api()
|
||||||
|
{
|
||||||
|
var expected = new List<BlogPost>
|
||||||
|
{
|
||||||
|
new() { Id = 1, Title = "Hello" },
|
||||||
|
new() { Id = 2, Title = "World" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, JsonSerializer.Serialize(expected));
|
||||||
|
using var client = new HttpClient(handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new System.Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
|
||||||
|
using var apiClient = new BlogApiClient(client);
|
||||||
|
var posts = await apiClient.GetPostsAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, posts.Count);
|
||||||
|
Assert.Equal("Hello", posts[0].Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeHttpMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly HttpResponseMessage _response;
|
||||||
|
|
||||||
|
public FakeHttpMessageHandler(HttpStatusCode statusCode, string content)
|
||||||
|
{
|
||||||
|
_response = new HttpResponseMessage(statusCode)
|
||||||
|
{
|
||||||
|
Content = new StringContent(content, Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
<ProjectReference Include="..\..\src\Yavsc.Org\Yavsc.Org.csproj" />
|
<ProjectReference Include="..\..\src\Yavsc.Org\Yavsc.Org.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Yavsc.Abstract\Yavsc.Abstract.csproj" />
|
<ProjectReference Include="..\..\src\Yavsc.Abstract\Yavsc.Abstract.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Yavsc.Server\Yavsc.Server.csproj" />
|
<ProjectReference Include="..\..\src\Yavsc.Server\Yavsc.Server.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\PostIt\PostIt\PostIt.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue