2016-05-17 18:22:18 +02:00
|
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
|
using System.ComponentModel.DataAnnotations.Schema;
|
2016-08-03 23:25:57 +02:00
|
|
|
using Newtonsoft.Json;
|
2018-07-16 02:36:44 +02:00
|
|
|
using Yavsc.Abstract.Identity.Security;
|
2017-01-20 14:20:44 +01:00
|
|
|
using Yavsc.Models.Access;
|
2017-10-02 13:15:01 +02:00
|
|
|
using Yavsc.Models.Relationship;
|
2026-08-03 01:32:59 +01:00
|
|
|
using Yavsc.Blogspot;
|
2016-05-17 18:22:18 +02:00
|
|
|
|
2017-10-04 23:53:47 +02:00
|
|
|
namespace Yavsc.Models.Blog
|
2016-05-17 18:22:18 +02:00
|
|
|
{
|
2026-08-03 01:32:59 +01:00
|
|
|
|
|
|
|
|
public class BlogPost : IBlogPost
|
2016-05-17 18:22:18 +02:00
|
|
|
{
|
|
|
|
|
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Identifiant du post")]
|
|
|
|
|
public long Id { get; set; }
|
|
|
|
|
|
|
|
|
|
[StringLength(1024)]
|
|
|
|
|
public string? Photo { get; set; }
|
|
|
|
|
|
|
|
|
|
[StringLength(1024)]
|
|
|
|
|
[Required]
|
|
|
|
|
public string Title { get; set; }
|
|
|
|
|
|
|
|
|
|
[StringLength(56224)]
|
2026-02-22 19:59:33 +00:00
|
|
|
public string? Article { get; set; }
|
2026-02-22 19:54:10 +00:00
|
|
|
|
|
|
|
|
[InverseProperty("Target")]
|
|
|
|
|
[Display(Name = "Liste de contrôle d'accès")]
|
|
|
|
|
public virtual List<CircleAuthorizationToBlogPost>? ACL { get; set; }
|
2016-07-27 10:55:44 +02:00
|
|
|
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Identifiant de l'auteur")]
|
2023-03-26 21:48:25 +01:00
|
|
|
[ForeignKey("Author")]
|
2026-02-22 19:54:10 +00:00
|
|
|
public string? AuthorId { get; set; }
|
2017-03-10 16:42:57 +01:00
|
|
|
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Auteur")]
|
2026-08-05 21:05:14 +01:00
|
|
|
public virtual ApplicationUser Author { set; get; }
|
2017-10-04 23:53:47 +02:00
|
|
|
|
2017-01-17 14:56:53 +01:00
|
|
|
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Date de création")]
|
2017-01-17 14:56:53 +01:00
|
|
|
public DateTime DateCreated
|
|
|
|
|
{
|
|
|
|
|
get; set;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Créateur")]
|
2025-06-13 15:22:02 +01:00
|
|
|
public string? UserCreated
|
2017-01-17 14:56:53 +01:00
|
|
|
{
|
|
|
|
|
get; set;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Dernière modification")]
|
2017-01-17 14:56:53 +01:00
|
|
|
public DateTime DateModified
|
|
|
|
|
{
|
|
|
|
|
get; set;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 19:54:10 +00:00
|
|
|
[Display(Name = "Utilisateur ayant modifé le dernier")]
|
|
|
|
|
public string? UserModified
|
2017-01-17 14:56:53 +01:00
|
|
|
{
|
|
|
|
|
get; set;
|
|
|
|
|
}
|
2017-05-27 16:22:58 +02:00
|
|
|
|
2017-01-21 22:36:43 +01:00
|
|
|
public bool AuthorizeCircle(long circleId)
|
|
|
|
|
{
|
2026-02-22 19:54:10 +00:00
|
|
|
return ACL?.Any(i => i.CircleId == circleId) ?? true;
|
2017-01-21 22:36:43 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-30 19:37:55 +01:00
|
|
|
public CircleAuthorization[] GetACL()
|
2017-01-21 22:36:43 +01:00
|
|
|
{
|
2026-08-30 19:37:55 +01:00
|
|
|
return ACL?.ToArray() ?? Array.Empty<CircleAuthorization>();
|
2017-01-21 22:36:43 +01:00
|
|
|
}
|
2017-10-02 13:15:01 +02:00
|
|
|
|
|
|
|
|
public void Tag(Tag tag)
|
|
|
|
|
{
|
2026-02-22 19:54:10 +00:00
|
|
|
var existent = Tags.SingleOrDefault(t => t.PostId == Id && t.TagId == tag.Id);
|
|
|
|
|
if (existent == null) Tags.Add(new BlogTag { PostId = Id, Tag = tag });
|
2017-10-02 13:15:01 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 20:23:23 +00:00
|
|
|
public void DeTag(Tag tag)
|
2017-10-02 13:15:01 +02:00
|
|
|
{
|
2026-02-22 19:54:10 +00:00
|
|
|
var existent = Tags.SingleOrDefault(t => ((t.TagId == tag.Id) && t.PostId == Id));
|
|
|
|
|
if (existent != null) Tags.Remove(existent);
|
2017-10-02 13:15:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string[] GetTags()
|
|
|
|
|
{
|
2026-08-28 20:10:32 +01:00
|
|
|
return Tags?.Select(t => t.Tag.Name).ToArray() ?? Array.Empty<string>();
|
2017-10-02 13:15:01 +02:00
|
|
|
}
|
|
|
|
|
|
2017-10-04 23:53:47 +02:00
|
|
|
[InverseProperty("Post")]
|
2026-02-22 19:54:10 +00:00
|
|
|
public virtual List<BlogTag> Tags { get; set; }
|
2017-10-04 23:53:47 +02:00
|
|
|
|
|
|
|
|
[InverseProperty("Post")]
|
2026-02-22 19:54:10 +00:00
|
|
|
public virtual List<Comment> Comments { get; set; }
|
2025-02-08 20:06:24 +00:00
|
|
|
|
feat(post): add Publish toggle for blog posts (no schema change)
Replaces the previous 'Visibility enum' approach (commit 33ecfa7e,
reverted in 42625f5d) with the existing BlogSpotPublication
mechanism. Paul pointed out that the system already had a
publication table and a Publish field on BlogPostEditViewModel;
we just didn't expose it through the API.
The toggle is its own action on the API surface — a dedicated
endpoint rather than a field on the existing BlogPost wire
DTO. This keeps the BlogPostDto contract unchanged and avoids
shoe-horning 'Publish' into the entity model alongside
Title/Article (where the existing BlogSpotService.Modify
already takes two overloads and a third felt like drift).
Server (Yavsc.Blogs / Yavsc.Server)
- PUT /api/BlogApi/{id}/publish body { publish: bool }
Returns 204 on success, 404 when the post doesn't exist,
Challenge() (401) when the caller is not the author
(EditPermission gate). Idempotent: PUT because the
resulting state matches the body, not the request.
- BlogSpotService.SetPublishAsync(user, postId, publish)
factored out of the existing
Modify(BlogPostEditViewModel) inline toggle, so the new
endpoint reuses the same BlogSpotPublication row logic
(add row if missing on publish=true, remove row if
present on publish=false).
- BlogPost.IsPublished (NotMapped) is now hydrated by the
service after each Index/Details fetch — a single bulk
lookup, not N+1 — and surfaces through the wire JSON
so PostIt can show the current state without a follow-up
request.
- ApplicationUser nav properties (Posts, Book,
DeviceDeclaration, Connections, Circles, BlackList,
Rooms, RoomAccess, Membership, BlogComments) now carry
BOTH [JsonIgnore] (Newtonsoft) and
[System.Text.Json.Serialization.JsonIgnore] so the
Yavsc.Blogs test fixture (System.Text.Json) stops
exploding on object cycles when serialising
BlogPost.Author.Posts.Author.Posts. Production
(Yavsc.Org, NewtonsoftJson) was already safe via the
Newtonsoft-only attribute; this commit just makes the
Yavsc.Blogs side consistent.
Client (Yavsc.Api.Client)
- BlogApiClient.SetPublishAsync(id, publish) → PUT to the
new endpoint.
DTO wire (Yavsc.Abstract.Blogspot.BlogPost)
- BlogPostDto.IsPublished added. Same shape as the entity
field; serialised as a plain bool in JSON.
UI (PostIt)
- MainPageViewModel.DraftIsPublished (ObservableProperty)
mirrors the existing DraftTitle/DraftArticle pattern;
hydrated from SelectedPost.IsPublished on selection
change. TogglePublish command pushes the new state to
SetPublishAsync and updates both the buffer and the
selected post locally so the UI reflects the change
without a full Refresh.
- MainPage.axaml: a CheckBox 'Publié' in the toolbar,
bound to DraftIsPublished TwoWay and wired to
TogglePublishCommand. The toggle is its own action
(not part of Save), matching the wire contract.
Tests (Yavsc.Blogs.Tests)
- PublishEndpointTests (4 [Fact]):
* PUT publish=true returns 204 and IsPublished is true
in the next GET
* PUT publish=false clears IsPublished
* PUT on an unknown post returns 404
* PUT by a non-author does not return 204 (Challenge)
- BlogsWebServerFixture now wires
app.UseDeveloperExceptionPage() so 500s in tests
surface a real stack trace instead of an empty
InternalServerError body — much easier to diagnose
future regressions.
Test totals: 24/24 Yavsc.Blogs.Tests (was 20, +4
PublishEndpoint), 51/51 PostIt.Tests (no change), 44/44
Yavsc.Org.Tests (no change).
Out of scope (tracked in MEMORY.md, 2026-08-18):
- i18n: only the new 'Publié' label is localised; the
rest of MainPage.axaml is still hard-coded French.
- BlogPostEditViewModel.Publish ↔ IsPublished reconciliation
in the admin web Yavsc (the Org UI already edits Publish
inline; no work needed there).
2026-08-18 16:10:45 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Whether this post is published. Not a column: the
|
|
|
|
|
/// existence of a row in <c>BlogSpotPublication</c>
|
|
|
|
|
/// is the source of truth. EF skips this property via
|
|
|
|
|
/// <c>[NotMapped]</c> so no migration is needed. The
|
|
|
|
|
/// service hydrates it after each fetch (single bulk
|
|
|
|
|
/// lookup, not N+1) and it surfaces through the wire
|
|
|
|
|
/// as part of the JSON-serialised <c>BlogPost</c>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[NotMapped]
|
|
|
|
|
public bool IsPublished { get; set; }
|
|
|
|
|
|
2026-08-28 20:10:32 +01:00
|
|
|
[JsonIgnore]
|
fix(blog): replace IApplicationUser Author with concrete BlogPostAuthorDto
System.Text.Json cannot materialise an interface without a
polymorphic converter. Until this commit, BlogPostDto.Author
was typed as the abstract interface IApplicationUser, which
crashed the "load posts" call in PostIt whenever the server
returned a post with a populated Author object (the common
case — GET /api/BlogApi).
Fix:
* Introduce a minimum-viable wire DTO BlogPostAuthorDto in
Yavsc.Abstract.Blogspot (record: Id, UserName, Avatar).
These are the only fields the client UI actually needs;
the server-side ApplicationUser navigation is preserved
for permission checks and authorisation.
* Change IBlogPost.Author and BlogPostDto.Author from
IApplicationUser to BlogPostAuthorDto? (interface change,
breaking). The EF entity BlogPost keeps its full
ApplicationUser navigation property and exposes
IBlogPost.Author via an explicit interface implementation
that projects to BlogPostAuthorDto on demand (so EF can
still lazy-load the navigation without forcing an eager
join on every read).
* Restore the using directive that was accidentally removed
when the BlogPostDto property was rewritten (needed for
ICircleAuthorization in GetACL()).
Regression coverage (the missing test Paul flagged):
* Add BlogPostAuthorDtoTests in PostIt.Tests with four
scenarios that exercise the wire shape on the client side:
- A BlogPostDto JSON with a populated Author round-trips
through JsonSerializer without throwing and the three
fields (Id, UserName, Avatar) survive intact.
- A BlogPostDto JSON with explicit "author": null
deserialises with Author == null.
- A BlogPostDto JSON without any Author field at all
deserialises with Author == null (forward compat).
- The serialised shape of BlogPostAuthorDto uses camelCase
property names (matching the server's Web defaults), so
the field names on the wire don't drift without a test
catching it.
Tests: 55/55 PostIt.Tests (+4 new), 24/24 Yavsc.Blogs.Tests,
44/44 Yavsc.Org.Tests. No regressions.
Side note: yavsc.sln picks up Yavsc.Api.Client (added by
'feat/postit-acl' in 1.0.7 but never registered in the
solution file until now — probably auto-added by a recent
'dotnet build' that discovered the .csproj).
2026-08-18 22:01:09 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Explicit interface implementation of
|
|
|
|
|
/// <see cref="IBlogPost.Author"/>. The underlying
|
|
|
|
|
/// navigation property is <see cref="Author"/>
|
|
|
|
|
/// (an <c>ApplicationUser</c> entity), but the wire
|
|
|
|
|
/// DTO is a thin <see cref="BlogPostAuthorDto"/> with
|
|
|
|
|
/// only the fields the client UI consumes. We project
|
|
|
|
|
/// on demand so EF can lazy-load the navigation
|
|
|
|
|
/// without forcing an eager join on every read.
|
|
|
|
|
/// Returns <c>null</c> when the navigation hasn't been
|
|
|
|
|
/// loaded (caller should pre-Include <c>Author</c> if
|
|
|
|
|
/// they need it).
|
|
|
|
|
/// </summary>
|
|
|
|
|
BlogPostAuthorDto? IBlogPost.Author
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var a = Author;
|
|
|
|
|
if (a == null) return null;
|
|
|
|
|
return new BlogPostAuthorDto
|
|
|
|
|
{
|
|
|
|
|
Id = a.Id,
|
|
|
|
|
UserName = a.UserName,
|
|
|
|
|
Avatar = a.Avatar
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-30 19:37:55 +01:00
|
|
|
|
|
|
|
|
ICollection<CircleAuthorization> ICircleAuthorized.ACL
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return ACL?.Select(a => new CircleAuthorization
|
|
|
|
|
{
|
|
|
|
|
CircleId = a.CircleId
|
|
|
|
|
}).ToList() ?? new List<CircleAuthorization>();
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-05-17 18:22:18 +02:00
|
|
|
}
|
|
|
|
|
}
|