diff --git a/core/Piranha.AttributeBuilder/ContentTypeBuilder.cs b/core/Piranha.AttributeBuilder/ContentTypeBuilder.cs index dbaa6c355..db34f2736 100644 --- a/core/Piranha.AttributeBuilder/ContentTypeBuilder.cs +++ b/core/Piranha.AttributeBuilder/ContentTypeBuilder.cs @@ -85,7 +85,7 @@ public ContentTypeBuilder AddType(Type type) else { // Type is a non abstract class, check if it is a content type - if (typeof(IContent).IsAssignableFrom(type)) + if (typeof(GenericContent).IsAssignableFrom(type)) { if (type.GetCustomAttribute() != null) { diff --git a/core/Piranha/Api.cs b/core/Piranha/Api.cs index 916a6164a..df6bb2e78 100644 --- a/core/Piranha/Api.cs +++ b/core/Piranha/Api.cs @@ -34,6 +34,11 @@ public sealed class Api : IApi, IDisposable /// public IArchiveService Archives { get; } + /// + /// Gets the content service. + /// + public IContentService Content { get; } + /// /// Gets the content group service. /// @@ -103,6 +108,7 @@ public Api( IContentFactory contentFactory, IAliasRepository aliasRepository, IArchiveRepository archiveRepository, + IContentRepository contentRepository, IContentGroupRepository contentGroupRepository, IContentTypeRepository contentTypeRepository, ILanguageRepository languageRepository, @@ -132,6 +138,7 @@ public Api( SiteTypes = new SiteTypeService(siteTypeRepository, cache); // Create services with dependencies + Content = new ContentService(contentRepository, contentFactory, Languages, cache, search); Sites = new SiteService(siteRepository, contentFactory, Languages,cache); Aliases = new AliasService(aliasRepository, Sites, cache); Media = new MediaService(mediaRepository, Params, storage, processor, cache); diff --git a/core/Piranha/Extend/Blocks/HtmlBlock.cs b/core/Piranha/Extend/Blocks/HtmlBlock.cs index 20b59a22b..46c133b3e 100644 --- a/core/Piranha/Extend/Blocks/HtmlBlock.cs +++ b/core/Piranha/Extend/Blocks/HtmlBlock.cs @@ -17,7 +17,7 @@ namespace Piranha.Extend.Blocks /// Single column HTML block. /// [BlockType(Name = "Content", Category = "Content", Icon = "fas fa-paragraph", Component = "html-block")] - public class HtmlBlock : Block, ISearchable + public class HtmlBlock : Block, ISearchable, ITranslatable { /// /// Gets/sets the HTML body. diff --git a/core/Piranha/Extend/Blocks/QuoteBlock.cs b/core/Piranha/Extend/Blocks/QuoteBlock.cs index 343f94823..3878270b8 100644 --- a/core/Piranha/Extend/Blocks/QuoteBlock.cs +++ b/core/Piranha/Extend/Blocks/QuoteBlock.cs @@ -16,7 +16,7 @@ namespace Piranha.Extend.Blocks /// Single column quote block. /// [BlockType(Name = "Quote", Category = "Content", Icon = "fas fa-quote-right", Component = "quote-block")] - public class QuoteBlock : Block, ISearchable + public class QuoteBlock : Block, ISearchable, ITranslatable { /// /// Gets/sets the author diff --git a/core/Piranha/Extend/Blocks/TextBlock.cs b/core/Piranha/Extend/Blocks/TextBlock.cs index 400c30a26..35a00c43b 100644 --- a/core/Piranha/Extend/Blocks/TextBlock.cs +++ b/core/Piranha/Extend/Blocks/TextBlock.cs @@ -16,7 +16,7 @@ namespace Piranha.Extend.Blocks /// Single column text block. /// [BlockType(Name = "Text", Category = "Content", Icon = "fas fa-font", Component = "text-block")] - public class TextBlock : Block, ISearchable + public class TextBlock : Block, ISearchable, ITranslatable { /// /// Gets/sets the text body. diff --git a/core/Piranha/Extend/Fields/HtmlField.cs b/core/Piranha/Extend/Fields/HtmlField.cs index 36b36c139..a57413b67 100644 --- a/core/Piranha/Extend/Fields/HtmlField.cs +++ b/core/Piranha/Extend/Fields/HtmlField.cs @@ -13,7 +13,7 @@ namespace Piranha.Extend.Fields { [FieldType(Name = "Html", Shorthand = "Html", Component = "html-field")] - public class HtmlField : SimpleField, ISearchable + public class HtmlField : SimpleField, ISearchable, ITranslatable { /// /// Implicit operator for converting a string to a field. diff --git a/core/Piranha/Extend/Fields/MarkdownField.cs b/core/Piranha/Extend/Fields/MarkdownField.cs index ff1248ca3..5473d96f2 100644 --- a/core/Piranha/Extend/Fields/MarkdownField.cs +++ b/core/Piranha/Extend/Fields/MarkdownField.cs @@ -11,7 +11,7 @@ namespace Piranha.Extend.Fields { [FieldType(Name = "Markdown", Shorthand = "Markdown", Component = "markdown-field")] - public class MarkdownField : SimpleField, ISearchable + public class MarkdownField : SimpleField, ISearchable, ITranslatable { /// /// Implicit operator for converting a string to a field. diff --git a/core/Piranha/Extend/Fields/StringField.cs b/core/Piranha/Extend/Fields/StringField.cs index 3b2278acb..2237ce542 100644 --- a/core/Piranha/Extend/Fields/StringField.cs +++ b/core/Piranha/Extend/Fields/StringField.cs @@ -11,7 +11,7 @@ namespace Piranha.Extend.Fields { [FieldType(Name = "String", Shorthand = "String", Component = "string-field")] - public class StringField : SimpleField, ISearchable + public class StringField : SimpleField, ISearchable, ITranslatable { /// /// Implicit operator for converting a string to a field. diff --git a/core/Piranha/Extend/Fields/TextField.cs b/core/Piranha/Extend/Fields/TextField.cs index 2a0b247bd..7442b21c0 100644 --- a/core/Piranha/Extend/Fields/TextField.cs +++ b/core/Piranha/Extend/Fields/TextField.cs @@ -11,7 +11,7 @@ namespace Piranha.Extend.Fields { [FieldType(Name = "Text", Shorthand = "Text", Component = "text-field")] - public class TextField : SimpleField, ISearchable + public class TextField : SimpleField, ISearchable, ITranslatable { /// /// Implicit operator for converting a string to a field. diff --git a/core/Piranha/Extend/ITranslatable.cs b/core/Piranha/Extend/ITranslatable.cs new file mode 100644 index 000000000..09ce28a1c --- /dev/null +++ b/core/Piranha/Extend/ITranslatable.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +namespace Piranha.Extend +{ + /// + /// Interface for marking a block or field as translatable. + /// + public interface ITranslatable { } +} diff --git a/core/Piranha/IApi.cs b/core/Piranha/IApi.cs index e3599c3fc..b4227b28b 100644 --- a/core/Piranha/IApi.cs +++ b/core/Piranha/IApi.cs @@ -28,6 +28,11 @@ public interface IApi : IDisposable /// IArchiveService Archives { get; } + /// + /// Gets the content service. + /// + IContentService Content { get; } + /// /// Gets the content group service. /// diff --git a/core/Piranha/Models/Content.cs b/core/Piranha/Models/Content.cs index 47b7f4a3f..38c99d736 100644 --- a/core/Piranha/Models/Content.cs +++ b/core/Piranha/Models/Content.cs @@ -8,25 +8,25 @@ * */ -using System.Collections.Generic; -using Piranha.Extend.Fields; +using System.Threading.Tasks; namespace Piranha.Models { /// - /// Base class for project defined content. + /// Base class for generic content. /// - /// The type - public abstract class Content : ContentBase, IContent where T : Content + /// The content type + public abstract class Content : GenericContent where T : Content { /// - /// Gets/sets the optional primary image. + /// Creates a new page model using the given page type id. /// - public ImageField PrimaryImage { get; set; } = new ImageField(); - - /// - /// Gets/sets the optional excerpt. - /// - public string Excerpt { get; set; } + /// The current api + /// The unique page type id + /// The new model + public static Task CreateAsync(IApi api, string typeId = null) + { + return api.Content.CreateAsync(typeId); + } } } \ No newline at end of file diff --git a/core/Piranha/Models/GenericContent.cs b/core/Piranha/Models/GenericContent.cs new file mode 100644 index 000000000..7418914c5 --- /dev/null +++ b/core/Piranha/Models/GenericContent.cs @@ -0,0 +1,30 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using Piranha.Extend.Fields; + +namespace Piranha.Models +{ + /// + /// Base class for generic content. + /// + public abstract class GenericContent : ContentBase + { + /// + /// Gets/sets the optional primary image. + /// + public ImageField PrimaryImage { get; set; } = new ImageField(); + + /// + /// Gets/sets the optional excerpt. + /// + public string Excerpt { get; set; } + } +} \ No newline at end of file diff --git a/core/Piranha/Repositories/IContentRepository.cs b/core/Piranha/Repositories/IContentRepository.cs new file mode 100644 index 000000000..62c0afaec --- /dev/null +++ b/core/Piranha/Repositories/IContentRepository.cs @@ -0,0 +1,41 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * http://github.com/piranhacms/piranha + * + */ + +using System; +using System.Threading.Tasks; +using Piranha.Models; + +namespace Piranha.Repositories +{ + public interface IContentRepository + { + /// + /// Gets the content model with the specified id. + /// + /// The model type + /// The unique id + /// The selected language id + /// The content model + Task GetById(Guid id, Guid languageId) where T : GenericContent; + + /// + /// Saves the given content model + /// + /// The content model + /// The selected language id + Task Save(T model, Guid languageId) where T : GenericContent; + + /// + /// Deletes the content model with the specified id. + /// + /// The unique id + Task Delete(Guid id); + } +} diff --git a/core/Piranha/Services/ContentService.cs b/core/Piranha/Services/ContentService.cs new file mode 100644 index 000000000..b0a97428f --- /dev/null +++ b/core/Piranha/Services/ContentService.cs @@ -0,0 +1,246 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * http://github.com/piranhacms/piranha + * + */ + +using System; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Threading.Tasks; +using Piranha.Models; +using Piranha.Repositories; + +namespace Piranha.Services +{ + public class ContentService : IContentService + { + private readonly IContentRepository _pageRepo; + private readonly IContentFactory _factory; + private readonly ILanguageService _langService; + private readonly ICache _cache; + private readonly ISearch _search; + + /// + /// Default constructor. + /// + /// The main page repository + /// The content factory + /// The language service + /// The optional cache service + /// The optional search service + public ContentService(IContentRepository pageRepo, IContentFactory factory, ILanguageService langService, ICache cache = null, ISearch search = null) + { + _pageRepo = pageRepo; + _factory = factory; + _langService = langService; + + if ((int)App.CacheLevel > 2) + { + _cache = cache; + } + _search = search; + } + + /// + /// Creates and initializes a new content model. + /// + /// The content type id + /// The created page + public async Task CreateAsync(string typeId) where T : GenericContent + { + if (string.IsNullOrEmpty(typeId)) + { + typeId = typeof(T).Name; + } + + var type = App.ContentTypes.GetById(typeId); + + if (type != null) + { + var model = await _factory.CreateAsync(type).ConfigureAwait(false); + + return model; + } + return null; + } + + /// + /// Gets the content model with the specified id. + /// + /// The model type + /// The unique id + /// The optional language id + /// The content model + public async Task GetByIdAsync(Guid id, Guid? languageId = null) where T : GenericContent + { + T model = null; + + // Make sure we have a language id + if (languageId == null) + { + languageId = (await _langService.GetDefaultAsync())?.Id; + } + + // First, try to get the model from cache + if (typeof(IDynamicContent).IsAssignableFrom(typeof(T))) + { + model = null; // TODO: _cache?.Get($"DynamicContent_{ id.ToString() }"); + } + else + { + model = null; // TODO: _cache?.Get(id.ToString()); + } + + // If we have a model, let's initialize it + if (model != null) + { + await _factory.InitAsync(model, App.ContentTypes.GetById(model.TypeId)).ConfigureAwait(false); + } + + // If we don't have a model, get it from the repository + if (model == null) + { + model = await _pageRepo.GetById(id, languageId.Value).ConfigureAwait(false); + + await OnLoadAsync(model).ConfigureAwait(false); + } + + // Check that we got back the requested type from the + // repository + if (model != null && model is T) + { + return model; + } + return null; + } + + /// + /// Saves the given content model + /// + /// The content model + /// The optional language id + public async Task SaveAsync(T model, Guid? languageId = null) where T : GenericContent + { + // Make sure we have an Id + if (model.Id == Guid.Empty) + { + model.Id = Guid.NewGuid(); + } + + // Make sure we have a language id + if (languageId == null) + { + languageId = (await _langService.GetDefaultAsync())?.Id; + } + + // Validate model + var context = new ValidationContext(model); + Validator.ValidateObject(model, context, true); + + // Ensure category + if (model is ICategorizedContent categorizedModel) + { + if (categorizedModel.Category == null || (string.IsNullOrWhiteSpace(categorizedModel.Category.Title) && string.IsNullOrWhiteSpace(categorizedModel.Category.Slug))) + { + throw new ValidationException("The Category field is required"); + } + } + + // Call hooks and save + App.Hooks.OnBeforeSave(model); + await _pageRepo.Save(model, languageId.Value); + App.Hooks.OnAfterSave(model); + + // Remove from cache + await RemoveFromCacheAsync(model).ConfigureAwait(false); + } + + /// + /// Deletes the content model with the specified id. + /// + /// The unique id + public async Task DeleteAsync(Guid id) + { + var model = await GetByIdAsync(id).ConfigureAwait(false); + + if (model != null) + { + await DeleteAsync(model).ConfigureAwait(false); + } + } + + /// + /// Deletes the given content model. + /// + /// The content model + public async Task DeleteAsync(T model) where T : GenericContent + { + // Call hooks and delete + App.Hooks.OnBeforeDelete(model); + await _pageRepo.Delete(model.Id).ConfigureAwait(false); + App.Hooks.OnAfterDelete(model); + + // Delete search document + if (_search != null) + { + // TODO + // await _search.DeletePageAsync(model); + } + + // Remove from cache + await RemoveFromCacheAsync(model).ConfigureAwait(false); + } + + /// + /// Processes the model after it has been loaded from + /// the repository. + /// + /// The content model + private async Task OnLoadAsync(GenericContent model) + { + // Make sure we have a model + if (model == null) return; + + // Initialize the model + await _factory.InitAsync(model, App.ContentTypes.GetById(model.TypeId)); + + // Execute on load hook + App.Hooks.OnLoad(model); + + // Update the cache if available + if (_cache != null) + { + // Store the model + if (model is IDynamicContent) + { + _cache.Set($"DynamicContent_{ model.Id.ToString() }", model); + } + else + { + _cache.Set(model.Id.ToString(), model); + } + } + } + + /// + /// Removes the given model from the cache. + /// + /// The model + private Task RemoveFromCacheAsync(GenericContent model) + { + return Task.Run(() => + { + if (_cache != null) + { + _cache.Remove(model.Id.ToString()); + _cache.Remove($"DynamicContent_{ model.Id.ToString() }"); + } + }); + } + } +} diff --git a/core/Piranha/Services/IContentService.cs b/core/Piranha/Services/IContentService.cs new file mode 100644 index 000000000..73f28b8d9 --- /dev/null +++ b/core/Piranha/Services/IContentService.cs @@ -0,0 +1,54 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * http://github.com/piranhacms/piranha + * + */ + +using System; +using System.Threading.Tasks; +using Piranha.Models; + +namespace Piranha.Services +{ + public interface IContentService + { + /// + /// Creates and initializes a new content model. + /// + /// The content type id + /// The created page + Task CreateAsync(string typeId) where T : GenericContent; + + /// + /// Gets the content model with the specified id. + /// + /// The model type + /// The unique id + /// The optional language id + /// The content model + Task GetByIdAsync(Guid id, Guid? languageId = null) where T : GenericContent; + + /// + /// Saves the given content model + /// + /// The content model + /// The optional language id + Task SaveAsync(T model, Guid? languageId = null) where T : GenericContent; + + /// + /// Deletes the content model with the specified id. + /// + /// The unique id + Task DeleteAsync(Guid id); + + /// + /// Deletes the given content model. + /// + /// The content model + Task DeleteAsync(T model) where T : GenericContent; + } +} diff --git a/data/Piranha.Data.EF.SQLite/Migrations/20200930095842_AddContent.Designer.cs b/data/Piranha.Data.EF.SQLite/Migrations/20200930095842_AddContent.Designer.cs new file mode 100644 index 000000000..15156cc5b --- /dev/null +++ b/data/Piranha.Data.EF.SQLite/Migrations/20200930095842_AddContent.Designer.cs @@ -0,0 +1,1561 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Piranha.Data.EF.SQLite; + +namespace Piranha.Data.EF.SQLite.Migrations +{ + [DbContext(typeof(SQLiteDb))] + [Migration("20200930095842_AddContent")] + partial class AddContent + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("Piranha.Data.Alias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AliasUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("RedirectUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("SiteId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SiteId", "AliasUrl") + .IsUnique(); + + b.ToTable("Piranha_Aliases"); + }); + + modelBuilder.Entity("Piranha.Data.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsReusable") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("Id"); + + b.ToTable("Piranha_Blocks"); + }); + + modelBuilder.Entity("Piranha.Data.BlockField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlockId") + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("FieldId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BlockId", "FieldId", "SortOrder") + .IsUnique(); + + b.ToTable("Piranha_BlockFields"); + }); + + modelBuilder.Entity("Piranha.Data.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlogId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.HasKey("Id"); + + b.HasIndex("BlogId", "Slug") + .IsUnique(); + + b.ToTable("Piranha_Categories"); + }); + + modelBuilder.Entity("Piranha.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Excerpt") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PrimaryImageId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TypeId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("TypeId"); + + b.ToTable("Piranha_Content"); + }); + + modelBuilder.Entity("Piranha.Data.ContentField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("ContentId") + .HasColumnType("TEXT"); + + b.Property("FieldId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("RegionId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContentId", "RegionId", "FieldId", "SortOrder"); + + b.ToTable("Piranha_ContentFields"); + }); + + modelBuilder.Entity("Piranha.Data.ContentFieldTranslation", b => + { + b.Property("FieldId") + .HasColumnType("TEXT"); + + b.Property("LanguageId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("FieldId", "LanguageId"); + + b.HasIndex("LanguageId"); + + b.ToTable("Piranha_ContentFieldTranslations"); + }); + + modelBuilder.Entity("Piranha.Data.ContentGroup", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Icon") + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("Id"); + + b.ToTable("Piranha_ContentGroups"); + }); + + modelBuilder.Entity("Piranha.Data.ContentTaxonomy", b => + { + b.Property("ContentId") + .HasColumnType("TEXT"); + + b.Property("TaxonomyId") + .HasColumnType("TEXT"); + + b.HasKey("ContentId", "TaxonomyId"); + + b.HasIndex("TaxonomyId"); + + b.ToTable("Piranha_ContentTaxonomies"); + }); + + modelBuilder.Entity("Piranha.Data.ContentTranslation", b => + { + b.Property("ContentId") + .HasColumnType("TEXT"); + + b.Property("LanguageId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("ContentId", "LanguageId"); + + b.HasIndex("LanguageId"); + + b.ToTable("Piranha_ContentTranslations"); + }); + + modelBuilder.Entity("Piranha.Data.ContentType", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("CLRType") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Group") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Piranha_ContentTypes"); + }); + + modelBuilder.Entity("Piranha.Data.Language", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT") + .HasMaxLength(6); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.HasKey("Id"); + + b.ToTable("Piranha_Languages"); + }); + + modelBuilder.Entity("Piranha.Data.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AltText") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Filename") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("FolderId") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("PublicUrl") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FolderId"); + + b.ToTable("Piranha_Media"); + }); + + modelBuilder.Entity("Piranha.Data.MediaFolder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Piranha_MediaFolders"); + }); + + modelBuilder.Entity("Piranha.Data.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("FileExtension") + .HasColumnType("TEXT") + .HasMaxLength(8); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("MediaId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaId", "Width", "Height") + .IsUnique(); + + b.ToTable("Piranha_MediaVersions"); + }); + + modelBuilder.Entity("Piranha.Data.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CloseCommentsAfterDays") + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasMaxLength(255) + .HasDefaultValue("Page"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("EnableComments") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("Excerpt") + .HasColumnType("TEXT"); + + b.Property("IsHidden") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MetaDescription") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("MetaKeywords") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("MetaTitle") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("NavigationTitle") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("OgDescription") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("OgImageId") + .HasColumnType("TEXT"); + + b.Property("OgTitle") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("OriginalPageId") + .HasColumnType("TEXT"); + + b.Property("PageTypeId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("PrimaryImageId") + .HasColumnType("TEXT"); + + b.Property("Published") + .HasColumnType("TEXT"); + + b.Property("RedirectType") + .HasColumnType("INTEGER"); + + b.Property("RedirectUrl") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Route") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("SiteId") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("Id"); + + b.HasIndex("PageTypeId"); + + b.HasIndex("ParentId"); + + b.HasIndex("SiteId", "Slug") + .IsUnique(); + + b.ToTable("Piranha_Pages"); + }); + + modelBuilder.Entity("Piranha.Data.PageBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlockId") + .HasColumnType("TEXT"); + + b.Property("PageId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("PageId", "SortOrder") + .IsUnique(); + + b.ToTable("Piranha_PageBlocks"); + }); + + modelBuilder.Entity("Piranha.Data.PageComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("IsApproved") + .HasColumnType("INTEGER"); + + b.Property("PageId") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Piranha_PageComments"); + }); + + modelBuilder.Entity("Piranha.Data.PageField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("FieldId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("PageId") + .HasColumnType("TEXT"); + + b.Property("RegionId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PageId", "RegionId", "FieldId", "SortOrder"); + + b.ToTable("Piranha_PageFields"); + }); + + modelBuilder.Entity("Piranha.Data.PagePermission", b => + { + b.Property("PageId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasColumnType("TEXT"); + + b.HasKey("PageId", "Permission"); + + b.ToTable("Piranha_PagePermissions"); + }); + + modelBuilder.Entity("Piranha.Data.PageRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("PageId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Piranha_PageRevisions"); + }); + + modelBuilder.Entity("Piranha.Data.PageType", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("CLRType") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Piranha_PageTypes"); + }); + + modelBuilder.Entity("Piranha.Data.Param", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Piranha_Params"); + }); + + modelBuilder.Entity("Piranha.Data.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlogId") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("CloseCommentsAfterDays") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("EnableComments") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("Excerpt") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("MetaDescription") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("MetaKeywords") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("MetaTitle") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("OgDescription") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("OgImageId") + .HasColumnType("TEXT"); + + b.Property("OgTitle") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("PostTypeId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("PrimaryImageId") + .HasColumnType("TEXT"); + + b.Property("Published") + .HasColumnType("TEXT"); + + b.Property("RedirectType") + .HasColumnType("INTEGER"); + + b.Property("RedirectUrl") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Route") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("PostTypeId"); + + b.HasIndex("BlogId", "Slug") + .IsUnique(); + + b.ToTable("Piranha_Posts"); + }); + + modelBuilder.Entity("Piranha.Data.PostBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlockId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("PostId", "SortOrder") + .IsUnique(); + + b.ToTable("Piranha_PostBlocks"); + }); + + modelBuilder.Entity("Piranha.Data.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("IsApproved") + .HasColumnType("INTEGER"); + + b.Property("PostId") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.ToTable("Piranha_PostComments"); + }); + + modelBuilder.Entity("Piranha.Data.PostField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("FieldId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("PostId") + .HasColumnType("TEXT"); + + b.Property("RegionId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PostId", "RegionId", "FieldId", "SortOrder"); + + b.ToTable("Piranha_PostFields"); + }); + + modelBuilder.Entity("Piranha.Data.PostPermission", b => + { + b.Property("PostId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasColumnType("TEXT"); + + b.HasKey("PostId", "Permission"); + + b.ToTable("Piranha_PostPermissions"); + }); + + modelBuilder.Entity("Piranha.Data.PostRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.ToTable("Piranha_PostRevisions"); + }); + + modelBuilder.Entity("Piranha.Data.PostTag", b => + { + b.Property("PostId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("Piranha_PostTags"); + }); + + modelBuilder.Entity("Piranha.Data.PostType", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("CLRType") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Piranha_PostTypes"); + }); + + modelBuilder.Entity("Piranha.Data.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ContentLastModified") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT") + .HasMaxLength(6); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Hostnames") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("InternalId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LanguageId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LogoId") + .HasColumnType("TEXT"); + + b.Property("SiteTypeId") + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Title") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("Id"); + + b.HasIndex("InternalId") + .IsUnique(); + + b.HasIndex("LanguageId"); + + b.ToTable("Piranha_Sites"); + }); + + modelBuilder.Entity("Piranha.Data.SiteField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("FieldId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("RegionId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SiteId") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SiteId", "RegionId", "FieldId", "SortOrder"); + + b.ToTable("Piranha_SiteFields"); + }); + + modelBuilder.Entity("Piranha.Data.SiteType", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("CLRType") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Piranha_SiteTypes"); + }); + + modelBuilder.Entity("Piranha.Data.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlogId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.HasKey("Id"); + + b.HasIndex("BlogId", "Slug") + .IsUnique(); + + b.ToTable("Piranha_Tags"); + }); + + modelBuilder.Entity("Piranha.Data.Taxonomy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId", "Type", "Slug") + .IsUnique(); + + b.ToTable("Piranha_Taxonomies"); + }); + + modelBuilder.Entity("Piranha.Data.Alias", b => + { + b.HasOne("Piranha.Data.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.BlockField", b => + { + b.HasOne("Piranha.Data.Block", "Block") + .WithMany("Fields") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Category", b => + { + b.HasOne("Piranha.Data.Page", "Blog") + .WithMany() + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Content", b => + { + b.HasOne("Piranha.Data.Taxonomy", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("Piranha.Data.ContentType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentField", b => + { + b.HasOne("Piranha.Data.Content", "Content") + .WithMany("Fields") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentFieldTranslation", b => + { + b.HasOne("Piranha.Data.ContentField", "Field") + .WithMany("Translations") + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Language", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentTaxonomy", b => + { + b.HasOne("Piranha.Data.Content", "Content") + .WithMany("Tags") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Taxonomy", "Taxonomy") + .WithMany() + .HasForeignKey("TaxonomyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentTranslation", b => + { + b.HasOne("Piranha.Data.Content", "Content") + .WithMany("Translations") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Language", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Media", b => + { + b.HasOne("Piranha.Data.MediaFolder", "Folder") + .WithMany("Media") + .HasForeignKey("FolderId"); + }); + + modelBuilder.Entity("Piranha.Data.MediaVersion", b => + { + b.HasOne("Piranha.Data.Media", "Media") + .WithMany("Versions") + .HasForeignKey("MediaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Page", b => + { + b.HasOne("Piranha.Data.PageType", "PageType") + .WithMany() + .HasForeignKey("PageTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Page", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.HasOne("Piranha.Data.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PageBlock", b => + { + b.HasOne("Piranha.Data.Block", "Block") + .WithMany() + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Page", "Page") + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PageComment", b => + { + b.HasOne("Piranha.Data.Page", "Page") + .WithMany() + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PageField", b => + { + b.HasOne("Piranha.Data.Page", "Page") + .WithMany("Fields") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PagePermission", b => + { + b.HasOne("Piranha.Data.Page", "Page") + .WithMany("Permissions") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PageRevision", b => + { + b.HasOne("Piranha.Data.Page", "Page") + .WithMany() + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Post", b => + { + b.HasOne("Piranha.Data.Page", "Blog") + .WithMany() + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Piranha.Data.PostType", "PostType") + .WithMany() + .HasForeignKey("PostTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PostBlock", b => + { + b.HasOne("Piranha.Data.Block", "Block") + .WithMany() + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Post", "Post") + .WithMany("Blocks") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PostComment", b => + { + b.HasOne("Piranha.Data.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PostField", b => + { + b.HasOne("Piranha.Data.Post", "Post") + .WithMany("Fields") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PostPermission", b => + { + b.HasOne("Piranha.Data.Post", "Post") + .WithMany("Permissions") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PostRevision", b => + { + b.HasOne("Piranha.Data.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.PostTag", b => + { + b.HasOne("Piranha.Data.Post", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Site", b => + { + b.HasOne("Piranha.Data.Language", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.SiteField", b => + { + b.HasOne("Piranha.Data.Site", "Site") + .WithMany("Fields") + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.Tag", b => + { + b.HasOne("Piranha.Data.Page", "Blog") + .WithMany() + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/data/Piranha.Data.EF.SQLite/Migrations/20200930095842_AddContent.cs b/data/Piranha.Data.EF.SQLite/Migrations/20200930095842_AddContent.cs new file mode 100644 index 000000000..180fc3a72 --- /dev/null +++ b/data/Piranha.Data.EF.SQLite/Migrations/20200930095842_AddContent.cs @@ -0,0 +1,212 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Piranha.Data.EF.SQLite.Migrations +{ + public partial class AddContent : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Piranha_Taxonomies", + columns: table => new + { + Id = table.Column(nullable: false), + Title = table.Column(maxLength: 64, nullable: false), + Slug = table.Column(maxLength: 64, nullable: false), + Created = table.Column(nullable: false), + LastModified = table.Column(nullable: false), + GroupId = table.Column(maxLength: 64, nullable: false), + Type = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Piranha_Taxonomies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Piranha_Content", + columns: table => new + { + Id = table.Column(nullable: false), + Title = table.Column(nullable: true), + Created = table.Column(nullable: false), + LastModified = table.Column(nullable: false), + CategoryId = table.Column(nullable: true), + TypeId = table.Column(maxLength: 64, nullable: false), + PrimaryImageId = table.Column(nullable: true), + Excerpt = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Piranha_Content", x => x.Id); + table.ForeignKey( + name: "FK_Piranha_Content_Piranha_Taxonomies_CategoryId", + column: x => x.CategoryId, + principalTable: "Piranha_Taxonomies", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Piranha_Content_Piranha_ContentTypes_TypeId", + column: x => x.TypeId, + principalTable: "Piranha_ContentTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Piranha_ContentFields", + columns: table => new + { + Id = table.Column(nullable: false), + RegionId = table.Column(maxLength: 64, nullable: false), + FieldId = table.Column(maxLength: 64, nullable: false), + SortOrder = table.Column(nullable: false), + CLRType = table.Column(maxLength: 256, nullable: false), + Value = table.Column(nullable: true), + ContentId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Piranha_ContentFields", x => x.Id); + table.ForeignKey( + name: "FK_Piranha_ContentFields_Piranha_Content_ContentId", + column: x => x.ContentId, + principalTable: "Piranha_Content", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Piranha_ContentTaxonomies", + columns: table => new + { + ContentId = table.Column(nullable: false), + TaxonomyId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Piranha_ContentTaxonomies", x => new { x.ContentId, x.TaxonomyId }); + table.ForeignKey( + name: "FK_Piranha_ContentTaxonomies_Piranha_Content_ContentId", + column: x => x.ContentId, + principalTable: "Piranha_Content", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Piranha_ContentTaxonomies_Piranha_Taxonomies_TaxonomyId", + column: x => x.TaxonomyId, + principalTable: "Piranha_Taxonomies", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Piranha_ContentTranslations", + columns: table => new + { + ContentId = table.Column(nullable: false), + LanguageId = table.Column(nullable: false), + Title = table.Column(maxLength: 128, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Piranha_ContentTranslations", x => new { x.ContentId, x.LanguageId }); + table.ForeignKey( + name: "FK_Piranha_ContentTranslations_Piranha_Content_ContentId", + column: x => x.ContentId, + principalTable: "Piranha_Content", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Piranha_ContentTranslations_Piranha_Languages_LanguageId", + column: x => x.LanguageId, + principalTable: "Piranha_Languages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Piranha_ContentFieldTranslations", + columns: table => new + { + FieldId = table.Column(nullable: false), + LanguageId = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Piranha_ContentFieldTranslations", x => new { x.FieldId, x.LanguageId }); + table.ForeignKey( + name: "FK_Piranha_ContentFieldTranslations_Piranha_ContentFields_FieldId", + column: x => x.FieldId, + principalTable: "Piranha_ContentFields", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Piranha_ContentFieldTranslations_Piranha_Languages_LanguageId", + column: x => x.LanguageId, + principalTable: "Piranha_Languages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_Content_CategoryId", + table: "Piranha_Content", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_Content_TypeId", + table: "Piranha_Content", + column: "TypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_ContentFields_ContentId_RegionId_FieldId_SortOrder", + table: "Piranha_ContentFields", + columns: new[] { "ContentId", "RegionId", "FieldId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_ContentFieldTranslations_LanguageId", + table: "Piranha_ContentFieldTranslations", + column: "LanguageId"); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_ContentTaxonomies_TaxonomyId", + table: "Piranha_ContentTaxonomies", + column: "TaxonomyId"); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_ContentTranslations_LanguageId", + table: "Piranha_ContentTranslations", + column: "LanguageId"); + + migrationBuilder.CreateIndex( + name: "IX_Piranha_Taxonomies_GroupId_Type_Slug", + table: "Piranha_Taxonomies", + columns: new[] { "GroupId", "Type", "Slug" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Piranha_ContentFieldTranslations"); + + migrationBuilder.DropTable( + name: "Piranha_ContentTaxonomies"); + + migrationBuilder.DropTable( + name: "Piranha_ContentTranslations"); + + migrationBuilder.DropTable( + name: "Piranha_ContentFields"); + + migrationBuilder.DropTable( + name: "Piranha_Content"); + + migrationBuilder.DropTable( + name: "Piranha_Taxonomies"); + } + } +} diff --git a/data/Piranha.Data.EF.SQLite/Migrations/SQLiteDbModelSnapshot.cs b/data/Piranha.Data.EF.SQLite/Migrations/SQLiteDbModelSnapshot.cs index 14fc4052a..b64df2bf8 100644 --- a/data/Piranha.Data.EF.SQLite/Migrations/SQLiteDbModelSnapshot.cs +++ b/data/Piranha.Data.EF.SQLite/Migrations/SQLiteDbModelSnapshot.cs @@ -150,6 +150,99 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Piranha_Categories"); }); + modelBuilder.Entity("Piranha.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Excerpt") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PrimaryImageId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TypeId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("TypeId"); + + b.ToTable("Piranha_Content"); + }); + + modelBuilder.Entity("Piranha.Data.ContentField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CLRType") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("ContentId") + .HasColumnType("TEXT"); + + b.Property("FieldId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("RegionId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContentId", "RegionId", "FieldId", "SortOrder"); + + b.ToTable("Piranha_ContentFields"); + }); + + modelBuilder.Entity("Piranha.Data.ContentFieldTranslation", b => + { + b.Property("FieldId") + .HasColumnType("TEXT"); + + b.Property("LanguageId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("FieldId", "LanguageId"); + + b.HasIndex("LanguageId"); + + b.ToTable("Piranha_ContentFieldTranslations"); + }); + modelBuilder.Entity("Piranha.Data.ContentGroup", b => { b.Property("Id") @@ -181,6 +274,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Piranha_ContentGroups"); }); + modelBuilder.Entity("Piranha.Data.ContentTaxonomy", b => + { + b.Property("ContentId") + .HasColumnType("TEXT"); + + b.Property("TaxonomyId") + .HasColumnType("TEXT"); + + b.HasKey("ContentId", "TaxonomyId"); + + b.HasIndex("TaxonomyId"); + + b.ToTable("Piranha_ContentTaxonomies"); + }); + + modelBuilder.Entity("Piranha.Data.ContentTranslation", b => + { + b.Property("ContentId") + .HasColumnType("TEXT"); + + b.Property("LanguageId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.HasKey("ContentId", "LanguageId"); + + b.HasIndex("LanguageId"); + + b.ToTable("Piranha_ContentTranslations"); + }); + modelBuilder.Entity("Piranha.Data.ContentType", b => { b.Property("Id") @@ -1094,6 +1222,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Piranha_Tags"); }); + modelBuilder.Entity("Piranha.Data.Taxonomy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId", "Type", "Slug") + .IsUnique(); + + b.ToTable("Piranha_Taxonomies"); + }); + modelBuilder.Entity("Piranha.Data.Alias", b => { b.HasOne("Piranha.Data.Site", "Site") @@ -1121,6 +1287,73 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Piranha.Data.Content", b => + { + b.HasOne("Piranha.Data.Taxonomy", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("Piranha.Data.ContentType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentField", b => + { + b.HasOne("Piranha.Data.Content", "Content") + .WithMany("Fields") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentFieldTranslation", b => + { + b.HasOne("Piranha.Data.ContentField", "Field") + .WithMany("Translations") + .HasForeignKey("FieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Language", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentTaxonomy", b => + { + b.HasOne("Piranha.Data.Content", "Content") + .WithMany("Tags") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Taxonomy", "Taxonomy") + .WithMany() + .HasForeignKey("TaxonomyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Piranha.Data.ContentTranslation", b => + { + b.HasOne("Piranha.Data.Content", "Content") + .WithMany("Translations") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Piranha.Data.Language", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Piranha.Data.Media", b => { b.HasOne("Piranha.Data.MediaFolder", "Folder") diff --git a/data/Piranha.Data.EF/Data/Category.cs b/data/Piranha.Data.EF/Data/Category.cs index caf5b3598..a44649559 100644 --- a/data/Piranha.Data.EF/Data/Category.cs +++ b/data/Piranha.Data.EF/Data/Category.cs @@ -14,7 +14,7 @@ namespace Piranha.Data { [Serializable] - public sealed class Category : Taxonomy + public sealed class Category : TaxonomyBase { /// /// Gets/sets the id of the blog page this diff --git a/data/Piranha.Data.EF/Data/Content.cs b/data/Piranha.Data.EF/Data/Content.cs new file mode 100644 index 000000000..6f889529f --- /dev/null +++ b/data/Piranha.Data.EF/Data/Content.cs @@ -0,0 +1,90 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Piranha.Data +{ + [Serializable] + public sealed class Content : ContentBase, ICategorized, ITranslatable + { + /// + /// Gets/sets the optional category id. + /// + public Guid? CategoryId { get; set; } + + /// + /// Gets/sets the id of the content type. + /// + public string TypeId { get; set; } + + /// + /// Gets/sets the optional primary image id. + /// + public Guid? PrimaryImageId { get; set; } + + /// + /// Gets/sets the optional excerpt. + /// + public string Excerpt { get; set; } + + /// + /// Gets/sets the optional category. + /// + public Taxonomy Category { get; set; } + + /// + /// Gets/sets the available tags. + /// + public IList Tags { get; set; } = new List(); + + /// + /// Gets/sets the available translations. + /// + public IList Translations { get; set; } = new List(); + + /// + /// Gets/sets the content type. + /// + public ContentType Type { get; set; } + + /// + /// Sets the translation for the specified language. + /// + /// The parent id + /// The language id + /// The model + public void SetTranslation(Guid parentId, Guid languageId, object model) + { + if (model is Models.GenericContent content) + { + var translation = Translations.FirstOrDefault(t => t.LanguageId == languageId); + + if (translation == null) + { + translation = new ContentTranslation + { + ContentId = content.Id, + LanguageId = languageId + }; + Translations.Add(translation); + } + translation.Title = content.Title; + } + } + + public object GetTranslation(Guid languageId) + { + return Translations.FirstOrDefault(t => t.LanguageId == languageId); + } + } +} diff --git a/data/Piranha.Data.EF/Data/ContentField.cs b/data/Piranha.Data.EF/Data/ContentField.cs new file mode 100644 index 000000000..2105feced --- /dev/null +++ b/data/Piranha.Data.EF/Data/ContentField.cs @@ -0,0 +1,64 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Piranha.Data +{ + [Serializable] + public sealed class ContentField : ContentFieldBase, ITranslatable + { + /// + /// Gets/sets the content id. + /// + public Guid ContentId { get; set; } + + /// + /// Gets/sets the content. + /// + [JsonIgnore] + public Content Content { get; set; } + + /// + /// Gets/sets the available translations. + /// + public IList Translations { get; set; } = new List(); + + /// + /// Sets the translation for the specified language. + /// + /// The parent id + /// The language id + /// The model + public void SetTranslation(Guid parentId, Guid languageId, object model) + { + var translation = Translations.FirstOrDefault(t => t.LanguageId == languageId); + + if (translation == null) + { + translation = new ContentFieldTranslation + { + FieldId = parentId, + LanguageId = languageId + }; + Translations.Add(translation); + } + translation.Value = App.SerializeObject(model, model.GetType()); + } + + public object GetTranslation(Guid languageId) + { + return Translations.FirstOrDefault(t => t.LanguageId == languageId)?.Value; + } + } +} diff --git a/data/Piranha.Data.EF/Data/ContentFieldTranslation.cs b/data/Piranha.Data.EF/Data/ContentFieldTranslation.cs new file mode 100644 index 000000000..f7148d498 --- /dev/null +++ b/data/Piranha.Data.EF/Data/ContentFieldTranslation.cs @@ -0,0 +1,43 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; + +namespace Piranha.Data +{ + [Serializable] + public sealed class ContentFieldTranslation + { + /// + /// Gets/sets the unique id. + /// + public Guid FieldId { get; set; } + + /// + /// Gets/sets the language id. + /// + public Guid LanguageId { get; set; } + + /// + /// Gets/sets the serialized value. + /// + public string Value { get; set; } + + /// + /// Gets/sets the field. + /// + public ContentField Field { get; set; } + + /// + /// Gets/sets the language. + /// + public Language Language { get; set; } + } +} diff --git a/data/Piranha.Data.EF/Data/ContentTaxonomy.cs b/data/Piranha.Data.EF/Data/ContentTaxonomy.cs new file mode 100644 index 000000000..28e03ec04 --- /dev/null +++ b/data/Piranha.Data.EF/Data/ContentTaxonomy.cs @@ -0,0 +1,40 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; +using Newtonsoft.Json; + +namespace Piranha.Data +{ + [Serializable] + public sealed class ContentTaxonomy + { + /// + /// Gets/sets the content id. + /// + public Guid ContentId { get; set; } + + /// + /// Gets/sets the taxonomy id. + /// + public Guid TaxonomyId { get; set; } + + /// + /// Gets/sets the content. + /// + [JsonIgnore] + public Content Content { get; set; } + + /// + /// Gets/sets the taxonomy. + /// + public Taxonomy Taxonomy { get; set; } + } +} diff --git a/data/Piranha.Data.EF/Data/ContentTranslation.cs b/data/Piranha.Data.EF/Data/ContentTranslation.cs new file mode 100644 index 000000000..ea64943c2 --- /dev/null +++ b/data/Piranha.Data.EF/Data/ContentTranslation.cs @@ -0,0 +1,43 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; + +namespace Piranha.Data +{ + [Serializable] + public sealed class ContentTranslation + { + /// + /// Gets/sets the content id. + /// + public Guid ContentId { get; set; } + + /// + /// Gets/sets the language id. + /// + public Guid LanguageId { get; set; } + + /// + /// Gets/sets the main title. + /// + public string Title { get; set; } + + /// + /// Gets/sets the content. + /// + public Content Content { get; set; } + + /// + /// Gets/sets the language. + /// + public Language Language { get; set; } + } +} diff --git a/data/Piranha.Data.EF/Data/ICategorized.cs b/data/Piranha.Data.EF/Data/ICategorized.cs new file mode 100644 index 000000000..c333a1e4d --- /dev/null +++ b/data/Piranha.Data.EF/Data/ICategorized.cs @@ -0,0 +1,25 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; + +namespace Piranha.Data +{ + /// + /// Interface for categorized content. + /// + public interface ICategorized + { + /// + /// Gets/sets the category id. + /// + Guid? CategoryId { get; set; } + } +} diff --git a/data/Piranha.Data.EF/Data/ITranslatable.cs b/data/Piranha.Data.EF/Data/ITranslatable.cs new file mode 100644 index 000000000..ef567c0c9 --- /dev/null +++ b/data/Piranha.Data.EF/Data/ITranslatable.cs @@ -0,0 +1,30 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; + +namespace Piranha.Data +{ + /// + /// Interface for translatable data. + /// + public interface ITranslatable + { + /// + /// Sets the translation for the specified language. + /// + /// The parent id + /// The language id + /// The model + void SetTranslation(Guid parentId, Guid languageId, object model); + + object GetTranslation(Guid languageId); + } +} diff --git a/data/Piranha.Data.EF/Data/Tag.cs b/data/Piranha.Data.EF/Data/Tag.cs index 7cc0d578e..cc225c698 100644 --- a/data/Piranha.Data.EF/Data/Tag.cs +++ b/data/Piranha.Data.EF/Data/Tag.cs @@ -14,7 +14,7 @@ namespace Piranha.Data { [Serializable] - public sealed class Tag : Taxonomy + public sealed class Tag : TaxonomyBase { /// /// Gets/sets the id of the blog page this diff --git a/data/Piranha.Data.EF/Data/Taxonomy.cs b/data/Piranha.Data.EF/Data/Taxonomy.cs index 3a78e9a15..1b777247b 100644 --- a/data/Piranha.Data.EF/Data/Taxonomy.cs +++ b/data/Piranha.Data.EF/Data/Taxonomy.cs @@ -13,31 +13,16 @@ namespace Piranha.Data { [Serializable] - public abstract class Taxonomy + public sealed class Taxonomy : TaxonomyBase { /// - /// Gets/sets the unique id. + /// Gets/sets the id used for grouping. /// - public Guid Id { get; set; } + public string GroupId { get; set; } /// - /// Gets/sets the title. + /// Gets/sets the taxonomy type. /// - public string Title { get; set; } - - /// - /// Gets/sets the slug. - /// - public string Slug { get; set; } - - /// - /// Gets/sets the created date. - /// - public DateTime Created { get; set; } - - /// - /// Gets/sets the last modification date. - /// - public DateTime LastModified { get; set; } + public TaxonomyType Type { get; set; } } } diff --git a/core/Piranha/Models/IContent.cs b/data/Piranha.Data.EF/Data/TaxonomyBase.cs similarity index 53% rename from core/Piranha/Models/IContent.cs rename to data/Piranha.Data.EF/Data/TaxonomyBase.cs index c720c8e3b..973c9212d 100644 --- a/core/Piranha/Models/IContent.cs +++ b/data/Piranha.Data.EF/Data/TaxonomyBase.cs @@ -9,43 +9,35 @@ */ using System; -using System.Collections.Generic; -namespace Piranha.Models +namespace Piranha.Data { - /// - /// Interface for generic content models. - /// - public interface IContent + [Serializable] + public abstract class TaxonomyBase { /// /// Gets/sets the unique id. /// - Guid Id { get; set; } - - /// - /// Gets/sets the content type id. - /// - string TypeId { get; set; } + public Guid Id { get; set; } /// /// Gets/sets the title. /// - string Title { get; set; } + public string Title { get; set; } /// - /// Gets/sets the permissions needed to access the page. + /// Gets/sets the slug. /// - IList Permissions { get; set; } + public string Slug { get; set; } /// /// Gets/sets the created date. /// - DateTime Created { get; set; } + public DateTime Created { get; set; } /// /// Gets/sets the last modification date. /// - DateTime LastModified { get; set; } + public DateTime LastModified { get; set; } } -} \ No newline at end of file +} diff --git a/data/Piranha.Data.EF/Data/TaxonomyType.cs b/data/Piranha.Data.EF/Data/TaxonomyType.cs new file mode 100644 index 000000000..6ecccf3e7 --- /dev/null +++ b/data/Piranha.Data.EF/Data/TaxonomyType.cs @@ -0,0 +1,24 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; + +namespace Piranha.Data +{ + /// + /// The different types of taxonomies + /// + [Serializable] + public enum TaxonomyType + { + Category, + Tag + } +} diff --git a/data/Piranha.Data.EF/Db.cs b/data/Piranha.Data.EF/Db.cs index 66ff8300b..125260444 100644 --- a/data/Piranha.Data.EF/Db.cs +++ b/data/Piranha.Data.EF/Db.cs @@ -47,6 +47,31 @@ public abstract class Db : DbContext, IDb where T : Db /// public DbSet Categories { get; set; } + /// + /// Gets/sets the content set. + /// + public DbSet Content { get; set; } + + /// + /// Gets/sets the content field set. + /// + public DbSet ContentFields { get; set; } + + /// + /// Gets/sets the content field translation set. + /// + public DbSet ContentFieldTranslations { get; set; } + + /// + /// Gets/sets the content taxonomy set. + /// + public DbSet ContentTaxonomies { get; set; } + + /// + /// Gets/sets the content translation set. + /// + public DbSet ContentTranslations { get; set; } + /// /// Gets/sets the content group set. /// @@ -177,6 +202,11 @@ public abstract class Db : DbContext, IDb where T : Db /// public DbSet Tags { get; set; } + /// + /// Gets/sets the taxonomy set. + /// + public DbSet Taxonomies { get; set; } + /// /// Default constructor. /// @@ -225,11 +255,32 @@ protected override void OnModelCreating(ModelBuilder mb) mb.Entity().Property(c => c.Slug).IsRequired().HasMaxLength(64); mb.Entity().HasIndex(c => new { c.BlogId, c.Slug }).IsUnique(); + + mb.Entity().ToTable("Piranha_Content"); + mb.Entity().Property(p => p.TypeId).HasMaxLength(64).IsRequired(); + + mb.Entity().ToTable("Piranha_ContentFields"); + mb.Entity().Property(f => f.RegionId).HasMaxLength(64).IsRequired(); + mb.Entity().Property(f => f.FieldId).HasMaxLength(64).IsRequired(); + mb.Entity().Property(f => f.CLRType).HasMaxLength(256).IsRequired(); + mb.Entity().HasIndex(f => new { f.ContentId, f.RegionId, f.FieldId, f.SortOrder }); + + mb.Entity().ToTable("Piranha_ContentFieldTranslations"); + mb.Entity().HasKey(t => new { t.FieldId, t.LanguageId }); + mb.Entity().ToTable("Piranha_ContentGroups"); mb.Entity().Property(t => t.Id).IsRequired().HasMaxLength(64); mb.Entity().Property(t => t.CLRType).IsRequired().HasMaxLength(255); mb.Entity().Property(t => t.Title).IsRequired().HasMaxLength(128); + mb.Entity().ToTable("Piranha_ContentTaxonomies"); + mb.Entity().HasKey(t => new { t.ContentId, t.TaxonomyId }); + mb.Entity().HasOne(t => t.Taxonomy).WithMany().IsRequired().OnDelete(DeleteBehavior.Restrict); + + mb.Entity().ToTable("Piranha_ContentTranslations"); + mb.Entity().HasKey(t => new { t.ContentId, t.LanguageId }); + mb.Entity().Property(p => p.Title).HasMaxLength(128).IsRequired(); + mb.Entity().ToTable("Piranha_ContentTypes"); mb.Entity().Property(t => t.Group).IsRequired().HasMaxLength(64); @@ -363,6 +414,12 @@ protected override void OnModelCreating(ModelBuilder mb) mb.Entity().Property(t => t.Title).IsRequired().HasMaxLength(64); mb.Entity().Property(t => t.Slug).IsRequired().HasMaxLength(64); mb.Entity().HasIndex(t => new { t.BlogId, t.Slug }).IsUnique(); + + mb.Entity().ToTable("Piranha_Taxonomies"); + mb.Entity().Property(t => t.GroupId).IsRequired().HasMaxLength(64); + mb.Entity().Property(t => t.Title).IsRequired().HasMaxLength(64); + mb.Entity().Property(t => t.Slug).IsRequired().HasMaxLength(64); + mb.Entity().HasIndex(t => new { t.GroupId, t.Type, t.Slug }).IsUnique(); } /// diff --git a/data/Piranha.Data.EF/IDb.cs b/data/Piranha.Data.EF/IDb.cs index 0c0953fb2..2f2990726 100644 --- a/data/Piranha.Data.EF/IDb.cs +++ b/data/Piranha.Data.EF/IDb.cs @@ -40,6 +40,31 @@ public interface IDb : IDisposable /// DbSet Categories { get; set; } + /// + /// Gets/sets the content set. + /// + DbSet Content { get; set; } + + /// + /// Gets/sets the content field set. + /// + DbSet ContentFields { get; set; } + + /// + /// Gets/sets the content field translation set. + /// + DbSet ContentFieldTranslations { get; set; } + + /// + /// Gets/sets the content taxonomy set. + /// + DbSet ContentTaxonomies { get; set; } + + /// + /// Gets/sets the content translation set. + /// + DbSet ContentTranslations { get; set; } + /// /// Gets/sets the content group set. /// @@ -170,6 +195,11 @@ public interface IDb : IDisposable /// DbSet Tags { get; set; } + /// + /// Gets/sets the taxonomy set. + /// + DbSet Taxonomies { get; set; } + /// /// Gets the entity set for the specified type. /// diff --git a/data/Piranha.Data.EF/Module.cs b/data/Piranha.Data.EF/Module.cs index 62d836bca..79ac682d5 100644 --- a/data/Piranha.Data.EF/Module.cs +++ b/data/Piranha.Data.EF/Module.cs @@ -67,10 +67,30 @@ static Module() .ForMember(c => c.Created, o => o.Ignore()); cfg.CreateMap() .ForMember(c => c.Type, o => o.MapFrom(m => Models.TaxonomyType.Category)); + cfg.CreateMap() + .ForMember(p => p.PrimaryImage, o => o.MapFrom(m => m.PrimaryImageId)) + .ForMember(p => p.Permissions, o => o.Ignore()); + cfg.CreateMap() + .ForMember(c => c.CategoryId, o => o.Ignore()) + .ForMember(c => c.Category, o => o.Ignore()) + .ForMember(c => c.Fields, o => o.Ignore()) + .ForMember(c => c.Tags, o => o.Ignore()) + .ForMember(c => c.Type, o => o.Ignore()) + .ForMember(c => c.Translations, o => o.Ignore()) + .ForMember(c => c.Created, o => o.Ignore()) + .ForMember(c => c.LastModified, o => o.Ignore()); cfg.CreateMap(); cfg.CreateMap() .ForMember(g => g.Created, o => o.Ignore()) .ForMember(g => g.LastModified, o => o.Ignore()); + cfg.CreateMap() + .ForMember(c => c.Id, o => o.Ignore()) + .ForMember(c => c.TypeId, o => o.Ignore()) + .ForMember(c => c.PrimaryImage, o => o.Ignore()) + .ForMember(c => c.Excerpt, o => o.Ignore()) + .ForMember(c => c.Created, o => o.Ignore()) + .ForMember(c => c.LastModified, o => o.Ignore()) + .ForMember(c => c.Permissions, o => o.Ignore()); cfg.CreateMap() .ForMember(f => f.Id, o => o.Ignore()) .ForMember(f => f.Created, o => o.Ignore()) diff --git a/data/Piranha.Data.EF/PiranhaEFExtensions.cs b/data/Piranha.Data.EF/PiranhaEFExtensions.cs index 62e6c1206..6ac7d66d5 100644 --- a/data/Piranha.Data.EF/PiranhaEFExtensions.cs +++ b/data/Piranha.Data.EF/PiranhaEFExtensions.cs @@ -42,6 +42,7 @@ public static IServiceCollection AddPiranhaEF(this IServiceCollection service // Register repositories services.Add(new ServiceDescriptor(typeof(IAliasRepository), typeof(AliasRepository), scope)); services.Add(new ServiceDescriptor(typeof(IArchiveRepository), typeof(ArchiveRepository), scope)); + services.Add(new ServiceDescriptor(typeof(IContentRepository), typeof(ContentRepository), scope)); services.Add(new ServiceDescriptor(typeof(IContentGroupRepository), typeof(ContentGroupRepository), scope)); services.Add(new ServiceDescriptor(typeof(IContentTypeRepository), typeof(ContentTypeRepository), scope)); services.Add(new ServiceDescriptor(typeof(ILanguageRepository), typeof(LanguageRepository), scope)); diff --git a/data/Piranha.Data.EF/Repositories/ContentRepository.cs b/data/Piranha.Data.EF/Repositories/ContentRepository.cs new file mode 100644 index 000000000..8a5061285 --- /dev/null +++ b/data/Piranha.Data.EF/Repositories/ContentRepository.cs @@ -0,0 +1,254 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * https://github.com/piranhacms/piranha.core + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Piranha.Data; +using Piranha.Services; + +namespace Piranha.Repositories +{ + public class ContentRepository : IContentRepository + { + private readonly IDb _db; + private readonly IContentService _service; + + /// + /// Default constructor. + /// + /// The current db connection + /// The content service factory + public ContentRepository(IDb db, IContentServiceFactory factory) + { + _db = db; + _service = factory.CreateContentService(); + } + + /// + /// Gets the content model with the specified id. + /// + /// The model type + /// The unique id + /// The selected language id + /// The content model + public async Task GetById(Guid id, Guid languageId) where T : Models.GenericContent + { + var content = await GetQuery() + .FirstOrDefaultAsync(c => c.Id == id) + .ConfigureAwait(false); + + if (content != null) + { + return await _service.TransformAsync(content, App.ContentTypes.GetById(content.TypeId), languageId: languageId) + .ConfigureAwait(false); + } + return null; + } + + /// + /// Saves the given content model + /// + /// The content model + /// The selected language id + public async Task Save(T model, Guid languageId) where T : Models.GenericContent + { + var type = App.ContentTypes.GetById(model.TypeId); + var lastModified = DateTime.MinValue; + + if (type != null) + { + // Ensure category + if (model is Models.ICategorizedContent categorized) + { + var category = await _db.Taxonomies + .FirstOrDefaultAsync(c => c.Id == categorized.Category.Id) + .ConfigureAwait(false); + + if (category == null) + { + if (!string.IsNullOrWhiteSpace(categorized.Category.Slug)) + { + category = await _db.Taxonomies + .FirstOrDefaultAsync(c => c.GroupId == type.Group && c.Slug == categorized.Category.Slug && c.Type == TaxonomyType.Category) + .ConfigureAwait(false); + } + if (category == null && !string.IsNullOrWhiteSpace(categorized.Category.Title)) + { + category = await _db.Taxonomies + .FirstOrDefaultAsync(c => c.GroupId == type.Group && c.Title == categorized.Category.Title && c.Type == TaxonomyType.Category) + .ConfigureAwait(false); + } + + if (category == null) + { + category = new Taxonomy + { + Id = categorized.Category.Id != Guid.Empty ? categorized.Category.Id : Guid.NewGuid(), + GroupId = type.Group, + Type = TaxonomyType.Category, + Title = categorized.Category.Title, + Slug = Utils.GenerateSlug(categorized.Category.Title), + Created = DateTime.Now, + LastModified = DateTime.Now + }; + await _db.Taxonomies.AddAsync(category).ConfigureAwait(false); + } + categorized.Category.Id = category.Id; + categorized.Category.Title = category.Title; + categorized.Category.Slug = category.Slug; + } + } + + // Ensure tags + if (model is Models.ITaggedContent tagged) + { + foreach (var t in tagged.Tags) + { + var tag = await _db.Taxonomies + .FirstOrDefaultAsync(tg => tg.Id == t.Id) + .ConfigureAwait(false); + + if (tag == null) + { + if (!string.IsNullOrWhiteSpace(t.Slug)) + { + tag = await _db.Taxonomies + .FirstOrDefaultAsync(tg => tg.GroupId == type.Group && tg.Slug == t.Slug && tg.Type == TaxonomyType.Tag) + .ConfigureAwait(false); + } + if (tag == null && !string.IsNullOrWhiteSpace(t.Title)) + { + tag = await _db.Taxonomies + .FirstOrDefaultAsync(tg => tg.GroupId == type.Group && tg.Title == t.Title && tg.Type == TaxonomyType.Tag) + .ConfigureAwait(false); + } + + if (tag == null) + { + tag = new Taxonomy + { + Id = t.Id != Guid.Empty ? t.Id : Guid.NewGuid(), + GroupId = type.Group, + Type = TaxonomyType.Tag, + Title = t.Title, + Slug = Utils.GenerateSlug(t.Title), + Created = DateTime.Now, + LastModified = DateTime.Now + }; + await _db.Taxonomies.AddAsync(tag).ConfigureAwait(false); + } + t.Id = tag.Id; + } + t.Title = tag.Title; + t.Slug = tag.Slug; + } + } + + var content = await _db.Content + .Include(c => c.Translations) + .Include(c => c.Fields).ThenInclude(f => f.Translations) + .Include(c => c.Category) + .Include(c => c.Tags).ThenInclude(t => t.Taxonomy) + .FirstOrDefaultAsync(p => p.Id == model.Id) + .ConfigureAwait(false); + + // If not, create new content + if (content == null) + { + content = new Content + { + Id = model.Id != Guid.Empty ? model.Id : Guid.NewGuid(), + Created = DateTime.Now, + LastModified = DateTime.Now + }; + model.Id = content.Id; + + await _db.Content.AddAsync(content).ConfigureAwait(false); + } + else + { + content.LastModified = DateTime.Now; + } + content = _service.Transform(model, type, content, languageId); + + // Process fields + foreach (var field in content.Fields) + { + // Ensure foreign key for new fields + if (field.ContentId == Guid.Empty) + { + field.ContentId = content.Id; + await _db.ContentFields.AddAsync(field).ConfigureAwait(false); + } + } + + if (model is Models.ITaggedContent taggedModel) + { + // Remove tags + var removedTags = new List(); + foreach (var tag in content.Tags) + { + if (!taggedModel.Tags.Any(t => t.Id == tag.TaxonomyId)) + { + removedTags.Add(tag); + } + } + foreach (var removed in removedTags) + { + content.Tags.Remove(removed); + } + + // Add tags + foreach (var tag in taggedModel.Tags) + { + if (!content.Tags.Any(t => t.ContentId == content.Id && t.TaxonomyId == tag.Id)) + { + var contentTaxonomy = new ContentTaxonomy + { + ContentId = content.Id, + TaxonomyId = tag.Id + }; + content.Tags.Add(contentTaxonomy); + } + } + } + + await _db.SaveChangesAsync().ConfigureAwait(false); + //await DeleteUnusedCategories(model.BlogId).ConfigureAwait(false); + //await DeleteUnusedTags(model.BlogId).ConfigureAwait(false); + } + } + + /// + /// Deletes the content model with the specified id. + /// + /// The unique id + public Task Delete(Guid id) + { + throw new NotImplementedException(); + } + + /// + /// Gets the base query for content. + /// + /// The queryable + private IQueryable GetQuery() + { + return (IQueryable)_db.Content + .AsNoTracking() + .Include(c => c.Translations) + .Include(c => c.Fields).ThenInclude(f => f.Translations) + .AsQueryable(); + } + } +} diff --git a/data/Piranha.Data.EF/Services/ContentService.cs b/data/Piranha.Data.EF/Services/ContentService.cs index 26d4a9c87..5161cfde6 100644 --- a/data/Piranha.Data.EF/Services/ContentService.cs +++ b/data/Piranha.Data.EF/Services/ContentService.cs @@ -48,14 +48,17 @@ public ContentService(IContentFactory factory, IMapper mapper) /// The content entity /// The content type /// Optional func that should be called after transformation + /// The optional language id /// The page model - public async Task TransformAsync(TContent content, Models.ContentTypeBase type, Func process = null) + public async Task TransformAsync(TContent content, Models.ContentTypeBase type, Func process = null, Guid? languageId = null) where T : Models.ContentBase, TModelBase { if (type != null) { + // + // 1: Get the requested model type + // var modelType = typeof(T); - if (!typeof(Models.IDynamicContent).IsAssignableFrom(modelType) && !typeof(Models.IContentInfo).IsAssignableFrom(modelType)) { modelType = Type.GetType(type.CLRType); @@ -66,12 +69,19 @@ public async Task TransformAsync(TContent content, Models.ContentTypeBase } } - // Create an initialized model + // + // 2: Create an initialized model + // var model = await _factory.CreateAsync(type); - // Map basic fields + // + // 3: Map basic fields + // _mapper.Map(content, model); + // + // 4: Map routes + // if (model is Models.RoutedContentBase routeModel) { // Map route (if available) @@ -79,7 +89,22 @@ public async Task TransformAsync(TContent content, Models.ContentTypeBase routeModel.Route = type.Routes.First(); } - // Map regions + // + // 5: Map translation + // + if (content is ITranslatable translatableContent && languageId.HasValue) + { + var translation = translatableContent.GetTranslation(languageId.Value); + + if (translation != null) + { + _mapper.Map(translation, model); + } + } + + // + // 6: Map regions + // if (!(model is IContentInfo)) { var currentRegions = type.Regions.Select(r => r.Id).ToArray(); @@ -99,12 +124,12 @@ public async Task TransformAsync(TContent content, Models.ContentTypeBase { if (region.Fields.Count == 1) { - SetSimpleValue(model, regionKey, field); + SetSimpleValue(model, regionKey, field, languageId); break; } else { - SetComplexValue(model, regionKey, fieldDef.Id, field); + SetComplexValue(model, regionKey, fieldDef.Id, field, languageId); } } } @@ -120,11 +145,11 @@ public async Task TransformAsync(TContent content, Models.ContentTypeBase { var field = fields.SingleOrDefault(f => f.FieldId == region.Fields[0].Id && f.SortOrder == sortOrder); if (field != null) - AddSimpleValue(model, regionKey, field); + AddSimpleValue(model, regionKey, field, languageId); } else { - await AddComplexValueAsync(model, type, regionKey, fields.Where(f => f.SortOrder == sortOrder).ToList()) + await AddComplexValueAsync(model, type, regionKey, fields.Where(f => f.SortOrder == sortOrder).ToList(), languageId) .ConfigureAwait(false); } sortOrder++; @@ -148,13 +173,16 @@ await AddComplexValueAsync(model, type, regionKey, fields.Where(f => f.SortOrder /// The model /// The conten type /// The optional dest object + /// The optional language id /// The content data - public TContent Transform(T model, Models.ContentTypeBase type, TContent dest = null) + public TContent Transform(T model, Models.ContentTypeBase type, TContent dest = null, Guid? languageId = null) where T : Models.ContentBase, TModelBase { var content = dest == null ? Activator.CreateInstance() : dest; - // Map id + // + // 1: Map id + // if (model.Id != Guid.Empty) { content.Id = model.Id; @@ -165,10 +193,33 @@ public TContent Transform(T model, Models.ContentTypeBase type, TContent dest } content.Created = DateTime.Now; - // Map basic fields + // + // 2: Map basic fields + // _mapper.Map(model, content); - // Map regions + // + // 3: Map translation + // + if (content is ITranslatable translatableContent && languageId.HasValue) + { + translatableContent.SetTranslation(content.Id, languageId.Value, model); + } + + // + // 4: Map category + // + if (model is Models.ICategorizedContent categorized) + { + if (content is ICategorized categorizedContent) + { + categorizedContent.CategoryId = categorized.Category.Id; + } + } + + // + // 5: Map regions + // var currentRegions = type.Regions.Select(r => r.Id).ToArray(); foreach (var regionKey in currentRegions) @@ -180,7 +231,7 @@ public TContent Transform(T model, Models.ContentTypeBase type, TContent dest if (!regionType.Collection) { - MapRegion(content, GetRegion(model, regionKey), regionType, regionKey); + MapRegion(content, GetRegion(model, regionKey), regionType, regionKey, languageId: languageId); } else { @@ -188,7 +239,7 @@ public TContent Transform(T model, Models.ContentTypeBase type, TContent dest var sortOrder = 0; foreach (var region in GetEnumerable(model, regionKey)) { - var fields = MapRegion(content, region, regionType, regionKey, sortOrder++); + var fields = MapRegion(content, region, regionType, regionKey, sortOrder++, languageId); if (fields.Count > 0) items.AddRange(fields); @@ -403,7 +454,8 @@ private bool HasRegion(T model, string regionId) where T : Models.ContentBase /// The region type /// The region id /// The optional sort order - private IList MapRegion(TContent content, object region, Models.RegionType regionType, string regionId, int sortOrder = 0) + /// The optional language id + private IList MapRegion(TContent content, object region, Models.RegionType regionType, string regionId, int sortOrder = 0, Guid? languageId = null) { var items = new List(); @@ -452,11 +504,21 @@ private IList MapRegion(TContent content, object region, Models.RegionType content.Fields.Add(field); } - // Update field info & value + // Update field info field.CLRType = fieldType.TypeName; field.SortOrder = sortOrder; - field.Value = App.SerializeObject(fieldValue, fieldType.Type); + // Update field value + if (fieldValue is Extend.ITranslatable && field is ITranslatable translatableField && languageId.HasValue) + { + // This is a translatable value + translatableField.SetTranslation(field.Id, languageId.Value, fieldValue); + } + else + { + // this is a non translatable valuye + field.Value = App.SerializeObject(fieldValue, fieldType.Type); + } items.Add(field.Id); } } @@ -471,12 +533,13 @@ private IList MapRegion(TContent content, object region, Models.RegionType /// The model /// The region id /// The field - private void SetSimpleValue(T model, string regionId, TField field) where T : Models.ContentBase + /// The languageId + private void SetSimpleValue(T model, string regionId, TField field, Guid? languageId) where T : Models.ContentBase { if (model is Models.IDynamicContent dynamicModel) { ((IDictionary)dynamicModel.Regions)[regionId] = - DeserializeValue(field); + DeserializeValue(field, languageId); } else { @@ -484,7 +547,7 @@ private void SetSimpleValue(T model, string regionId, TField field) where T : if (regionProp != null) { - regionProp.SetValue(model, DeserializeValue(field)); + regionProp.SetValue(model, DeserializeValue(field, languageId)); } } } @@ -496,12 +559,13 @@ private void SetSimpleValue(T model, string regionId, TField field) where T : /// The model /// The region id /// The field - private void AddSimpleValue(T model, string regionId, TField field) where T : Models.ContentBase + /// The languageId + private void AddSimpleValue(T model, string regionId, TField field, Guid? languageId) where T : Models.ContentBase { if (model is Models.IDynamicContent dynamicModel) { ((IList)((IDictionary)dynamicModel.Regions)[regionId]).Add( - DeserializeValue(field)); + DeserializeValue(field, languageId)); } else { @@ -509,7 +573,7 @@ private void AddSimpleValue(T model, string regionId, TField field) where T : if (regionProp != null) { - ((IList)regionProp.GetValue(model)).Add(DeserializeValue(field)); + ((IList)regionProp.GetValue(model)).Add(DeserializeValue(field, languageId)); } } } @@ -522,12 +586,14 @@ private void AddSimpleValue(T model, string regionId, TField field) where T : /// The region id /// The field id /// The field - private void SetComplexValue(T model, string regionId, string fieldId, TField field) where T : Models.ContentBase + /// The languageId + private void SetComplexValue(T model, string regionId, string fieldId, TField field, Guid? languageId) + where T : Models.ContentBase { if (model is Models.IDynamicContent dynamicModel) { ((IDictionary)((IDictionary)dynamicModel.Regions)[regionId])[fieldId] = - DeserializeValue(field); + DeserializeValue(field, languageId); } else { @@ -542,7 +608,7 @@ private void SetComplexValue(T model, string regionId, string fieldId, TField if (fieldProp != null) { - fieldProp.SetValue(obj, DeserializeValue(field)); + fieldProp.SetValue(obj, DeserializeValue(field, languageId)); } } } @@ -557,7 +623,9 @@ private void SetComplexValue(T model, string regionId, string fieldId, TField /// The content type /// The region id /// The field - private async Task AddComplexValueAsync(T model, Models.ContentTypeBase contentType, string regionId, IList fields) where T : Models.ContentBase + /// The languageId + private async Task AddComplexValueAsync(T model, Models.ContentTypeBase contentType, string regionId, IList fields, Guid? languageId) + where T : Models.ContentBase { if (fields.Count > 0) { @@ -571,7 +639,7 @@ private async Task AddComplexValueAsync(T model, Models.ContentTypeBase conte if (((IDictionary)obj).ContainsKey(field.FieldId)) { ((IDictionary)obj)[field.FieldId] = - DeserializeValue(field); + DeserializeValue(field, languageId); } } list.Add(obj); @@ -590,7 +658,7 @@ private async Task AddComplexValueAsync(T model, Models.ContentTypeBase conte var fieldProp = obj.GetType().GetProperty(field.FieldId, App.PropertyBindings); if (fieldProp != null) { - fieldProp.SetValue(obj, DeserializeValue(field)); + fieldProp.SetValue(obj, DeserializeValue(field, languageId)); } } list.Add(obj); @@ -603,13 +671,18 @@ private async Task AddComplexValueAsync(T model, Models.ContentTypeBase conte /// Deserializes the given field value. /// /// The page field + /// The optional language id /// The value - private object DeserializeValue(TField field) + private object DeserializeValue(TField field, Guid? languageId) { var type = App.Fields.GetByType(field.CLRType); if (type != null) { + if (typeof(Extend.ITranslatable).IsAssignableFrom(type.Type) && field is ITranslatable translatable && languageId.HasValue) + { + return App.DeserializeObject((string)translatable.GetTranslation(languageId.Value), type.Type); + } return App.DeserializeObject(field.Value, type.Type); } return null; diff --git a/data/Piranha.Data.EF/Services/ContentServiceFactory.cs b/data/Piranha.Data.EF/Services/ContentServiceFactory.cs index a5305f2d2..6cf7f2b4e 100644 --- a/data/Piranha.Data.EF/Services/ContentServiceFactory.cs +++ b/data/Piranha.Data.EF/Services/ContentServiceFactory.cs @@ -40,6 +40,15 @@ public IContentService Create(_factory, mapper); } + /// + /// Creates a new content service. + /// + /// The content service + public IContentService CreateContentService() + { + return new ContentService(_factory, Module.Mapper); + } + /// /// Creates a new page content service. /// diff --git a/data/Piranha.Data.EF/Services/IContentService.cs b/data/Piranha.Data.EF/Services/IContentService.cs index 403e6b2d9..d1928cac7 100644 --- a/data/Piranha.Data.EF/Services/IContentService.cs +++ b/data/Piranha.Data.EF/Services/IContentService.cs @@ -26,8 +26,9 @@ public interface IContentService /// The content entity /// The content type /// Optional func that should be called after transformation + /// The optional language id /// The page model - Task TransformAsync(TContent content, Models.ContentTypeBase type, Func process = null) + Task TransformAsync(TContent content, Models.ContentTypeBase type, Func process = null, Guid? languageId = null) where T : Models.ContentBase, TModelBase; /// @@ -36,8 +37,9 @@ Task TransformAsync(TContent content, Models.ContentTypeBase type, FuncThe model /// The conten type /// The optional dest object + /// The optional language id /// The content data - TContent Transform(T model, Models.ContentTypeBase type, TContent dest = null) + TContent Transform(T model, Models.ContentTypeBase type, TContent dest = null, Guid? languageId = null) where T : Models.ContentBase, TModelBase; /// diff --git a/data/Piranha.Data.EF/Services/IContentServiceFactory.cs b/data/Piranha.Data.EF/Services/IContentServiceFactory.cs index 86117d403..ac01dcafc 100644 --- a/data/Piranha.Data.EF/Services/IContentServiceFactory.cs +++ b/data/Piranha.Data.EF/Services/IContentServiceFactory.cs @@ -24,6 +24,12 @@ IContentService Create + /// Creates a new content service. + /// + /// The content service + IContentService CreateContentService(); + /// /// Creates a new page content service. /// diff --git a/examples/RazorWeb/Models/Content/Product.cs b/examples/RazorWeb/Models/Content/Product.cs index 9a63c4166..032beb450 100644 --- a/examples/RazorWeb/Models/Content/Product.cs +++ b/examples/RazorWeb/Models/Content/Product.cs @@ -8,18 +8,21 @@ * */ +using System.Collections.Generic; using Piranha.Extend; using Piranha.Models; namespace RazorWeb.Models { [ContentGroup(Title = "Products", Icon = "fas fa-hammer")] - public abstract class Product : Content, ICategorizedContent + public abstract class Product : Content, ICategorizedContent, ITaggedContent where T : Product { /// /// Gets/sets the product category. /// public Taxonomy Category { get; set; } + + public IList Tags { get; set; } = new List(); } } \ No newline at end of file diff --git a/examples/RazorWeb/Seed.cs b/examples/RazorWeb/Seed.cs index 284a8e471..39c59df02 100644 --- a/examples/RazorWeb/Seed.cs +++ b/examples/RazorWeb/Seed.cs @@ -68,6 +68,22 @@ await api.Media.SaveAsync(new Piranha.Models.StreamMediaContent } } + var content = await Models.StandardProduct.CreateAsync(api).ConfigureAwait(false); + content.Title = "My content"; + content.Category = "Uncategorized"; + content.Tags.Add("Lorem"); + content.Tags.Add("Ipsum"); + content.AllFields.Date = DateTime.Now; + content.AllFields.Text = "Lorum ipsum"; + await api.Content.SaveAsync(content); + + content.Title = "Mitt innehåll"; + content.AllFields.Text = "Svenskum dansum"; + await api.Content.SaveAsync(content, lang2Id); + + var loadedContent = await api.Content.GetByIdAsync(content.Id); + var swedishContent = await api.Content.GetByIdAsync(content.Id, lang2Id); + // Create the start page var startpage = await Models.TeaserPage.CreateAsync(api).ConfigureAwait(false); startpage.SiteId = siteId; diff --git a/test/Piranha.Tests/App.cs b/test/Piranha.Tests/App.cs index 117b1ceb5..9cbd14812 100644 --- a/test/Piranha.Tests/App.cs +++ b/test/Piranha.Tests/App.cs @@ -91,6 +91,7 @@ private IApi CreateApi() factory, new AliasRepository(db), new ArchiveRepository(db), + new ContentRepository(db, serviceFactory), new ContentGroupRepository(db), new ContentTypeRepository(db), new LanguageRepository(db), diff --git a/test/Piranha.Tests/BaseTestsAsync.cs b/test/Piranha.Tests/BaseTestsAsync.cs index 68ec84722..8e504f4ef 100644 --- a/test/Piranha.Tests/BaseTestsAsync.cs +++ b/test/Piranha.Tests/BaseTestsAsync.cs @@ -69,6 +69,7 @@ protected virtual IApi CreateApi() factory, new AliasRepository(db), new ArchiveRepository(db), + new ContentRepository(db, serviceFactory), new ContentGroupRepository(db), new ContentTypeRepository(db), new LanguageRepository(db), diff --git a/test/Piranha.Tests/Fields.cs b/test/Piranha.Tests/Fields.cs index 69d161265..69ea3aead 100644 --- a/test/Piranha.Tests/Fields.cs +++ b/test/Piranha.Tests/Fields.cs @@ -881,6 +881,7 @@ private IApi CreateApi() factory, new AliasRepository(db), new ArchiveRepository(db), + new ContentRepository(db, serviceFactory), new ContentGroupRepository(db), new ContentTypeRepository(db), new LanguageRepository(db), diff --git a/test/Piranha.Tests/Utils/UI.cs b/test/Piranha.Tests/Utils/UI.cs index 19b59ea04..acaa22fdc 100644 --- a/test/Piranha.Tests/Utils/UI.cs +++ b/test/Piranha.Tests/Utils/UI.cs @@ -85,6 +85,7 @@ private IApi CreateApi() factory, new AliasRepository(db), new ArchiveRepository(db), + new ContentRepository(db, serviceFactory), new ContentGroupRepository(db), new ContentTypeRepository(db), new LanguageRepository(db),