Merge remote-tracking branch 'upstream/master'

This commit is contained in:
kjac
2014-06-17 18:39:34 +02:00
22 changed files with 1881 additions and 114 deletions
+3 -1
View File
@@ -55,6 +55,7 @@ module.exports = function(grunt) {
'app/controllers/controller.js',
'app/controllers/config.controller.js',
'app/directives/archetypeproperty.js',
'app/directives/archetypecustomview.js',
'app/directives/localize.js',
'app/services/localization.js',
'app/resources/propertyeditor.js'
@@ -66,7 +67,7 @@ module.exports = function(grunt) {
copy: {
html: {
cwd: 'app/views/',
src: ['archetype.html', 'archetype.config.html'],
src: ['archetype.html', 'archetype.default.html', 'archetype.config.html'],
dest: '<%= basePath %>/views',
expand: true
},
@@ -190,6 +191,7 @@ module.exports = function(grunt) {
stdout: true,
verbosity: 'quiet',
maxCpuCount: 4,
version: 4.0,
buildParameters: {
WarningLevel: 2,
NoWarn: 1607
@@ -222,6 +222,10 @@
<Compile Include="Models\FieldsetTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PropertyValueConverter\PropertyValueConverterTests.cs" />
<Compile Include="Serialization\ArchetypeJsonConverterComplexModelTest.cs" />
<Compile Include="Serialization\ArchetypeJsonConverterTest.cs" />
<Compile Include="Serialization\JsonTestStrings.cs" />
<Compile Include="Serialization\JsonTestModels.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
@@ -234,6 +238,7 @@
<Name>Archetype.Umbraco</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
@@ -0,0 +1,216 @@
using System.Linq;
using Archetype.PropertyConverters;
using Newtonsoft.Json;
using NUnit.Framework;
namespace Archetype.Tests.Serialization
{
[TestFixture]
public class ArchetypeJsonConverterComplexModelTest
{
private TextList _textList;
private Captions _captions;
private PageDetails _pageDetails;
private SlideShow _slides;
private Seo _seo;
private TextPageList _textPages;
private PageList _pages;
[SetUp]
public void SetUp()
{
InitComplexModels();
InitComplexTreeModel();
}
#region complex nested model tests
[Test]
public void PageDetailsModel_Serializes_To_Archetype_Property()
{
var result = JsonConvert.SerializeObject(_pageDetails, Formatting.Indented);
Assert.AreEqual(JsonTestStrings._PAGE_DETAILS_JSON, result);
}
[Test]
public void ConvertComplexModelToArchetype()
{
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(_pageDetails, Formatting.Indented);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
[Test]
public void DeserializeComplexModelFromArchetype()
{
var result = JsonConvert.DeserializeObject<PageDetails>(JsonTestStrings._PAGE_DETAILS_JSON);
Assert.NotNull(result);
Assert.IsInstanceOf<PageDetails>(result);
Assert.AreEqual("2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453", result.Pages);
Assert.AreEqual("First Page", result.Captions.TextStringArray.ElementAt(0).TextString);
Assert.AreEqual("Second Page", result.Captions.TextStringArray.ElementAt(1).TextString);
Assert.AreEqual("Third Page", result.Captions.TextStringArray.ElementAt(2).TextString);
Assert.AreEqual("Fourth Page", result.Captions.TextStringArray.ElementAt(3).TextString);
}
[Test]
public void SerializeThenDeserializeComplexModelFromArchetype()
{
var json = JsonConvert.SerializeObject(_pageDetails);
var result = JsonConvert.DeserializeObject<PageDetails>(json);
Assert.NotNull(result);
Assert.IsInstanceOf<PageDetails>(result);
Assert.AreEqual(_pageDetails.Pages, result.Pages);
Assert.AreEqual(_pageDetails.Captions.TextStringArray.ElementAt(0).TextString, result.Captions.TextStringArray.ElementAt(0).TextString);
Assert.AreEqual(_pageDetails.Captions.TextStringArray.ElementAt(1).TextString, result.Captions.TextStringArray.ElementAt(1).TextString);
Assert.AreEqual(_pageDetails.Captions.TextStringArray.ElementAt(2).TextString, result.Captions.TextStringArray.ElementAt(2).TextString);
Assert.AreEqual(_pageDetails.Captions.TextStringArray.ElementAt(3).TextString, result.Captions.TextStringArray.ElementAt(3).TextString);
}
#endregion
#region complex nested tree model tests
[Test]
public void ConvertComplexNestedModelToArchetype()
{
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(_pages, Formatting.Indented);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
[Test]
public void PagesModel_Serializes_To_Archetype_Property()
{
var result = JsonConvert.SerializeObject(_pages, Formatting.Indented);
Assert.AreEqual(JsonTestStrings._PAGES_JSON, result);
}
[Test]
public void DeserializeComplexTreeModelFromArchetype()
{
var result = JsonConvert.DeserializeObject<PageList>(JsonTestStrings._PAGES_JSON);
Assert.NotNull(result);
Assert.IsInstanceOf<PageList>(result);
Assert.AreEqual("1,2,3,4,5,6,7,8", result.Pages.ElementAt(0).Media.Slides);
Assert.AreEqual("1,2,3,4,5,6,7,8", result.Pages.ElementAt(1).Media.Slides);
Assert.AreEqual("1,2,3,4,5,6,7,8", result.Pages.ElementAt(2).Media.Slides);
Assert.AreEqual("Test Meta Title", result.Pages.ElementAt(0).Seo.MetaTitle);
Assert.AreEqual("Test Meta Description", result.Pages.ElementAt(0).Seo.MetaDescription);
Assert.AreEqual("Test Meta Title", result.Pages.ElementAt(1).Seo.MetaTitle);
Assert.AreEqual("Test Meta Description", result.Pages.ElementAt(1).Seo.MetaDescription);
Assert.AreEqual("Test Meta Title", result.Pages.ElementAt(2).Seo.MetaTitle);
Assert.AreEqual("Test Meta Description", result.Pages.ElementAt(2).Seo.MetaDescription);
Assert.AreEqual("Home Page Body", result.Pages.ElementAt(0).BodyText);
Assert.AreEqual("Home Page", result.Pages.ElementAt(0).PageTitle);
Assert.AreEqual("About us Page Body", result.Pages.ElementAt(1).BodyText);
Assert.AreEqual("About us Page", result.Pages.ElementAt(1).PageTitle);
Assert.AreEqual("Contact us Page Body", result.Pages.ElementAt(2).BodyText);
Assert.AreEqual("Contact us Page", result.Pages.ElementAt(2).PageTitle);
}
[Test]
public void SerializeThenDeserializeComplexTreeModelFromArchetype()
{
var json = JsonConvert.SerializeObject(_pages);
var result = JsonConvert.DeserializeObject<PageList>(json);
Assert.NotNull(result);
Assert.IsInstanceOf<PageList>(result);
Assert.AreEqual(_pages.Pages.ElementAt(0).Media.Slides, result.Pages.ElementAt(0).Media.Slides);
Assert.AreEqual(_pages.Pages.ElementAt(1).Media.Slides, result.Pages.ElementAt(1).Media.Slides);
Assert.AreEqual(_pages.Pages.ElementAt(2).Media.Slides, result.Pages.ElementAt(2).Media.Slides);
Assert.AreEqual(_pages.Pages.ElementAt(0).Seo.MetaTitle, result.Pages.ElementAt(0).Seo.MetaTitle);
Assert.AreEqual(_pages.Pages.ElementAt(0).Seo.MetaDescription, result.Pages.ElementAt(0).Seo.MetaDescription);
Assert.AreEqual(_pages.Pages.ElementAt(1).Seo.MetaTitle, result.Pages.ElementAt(1).Seo.MetaTitle);
Assert.AreEqual(_pages.Pages.ElementAt(1).Seo.MetaDescription, result.Pages.ElementAt(1).Seo.MetaDescription);
Assert.AreEqual(_pages.Pages.ElementAt(2).Seo.MetaTitle, result.Pages.ElementAt(2).Seo.MetaTitle);
Assert.AreEqual(_pages.Pages.ElementAt(2).Seo.MetaDescription, result.Pages.ElementAt(2).Seo.MetaDescription);
Assert.AreEqual(_pages.Pages.ElementAt(0).BodyText, result.Pages.ElementAt(0).BodyText);
Assert.AreEqual(_pages.Pages.ElementAt(0).PageTitle, result.Pages.ElementAt(0).PageTitle);
Assert.AreEqual(_pages.Pages.ElementAt(1).BodyText, result.Pages.ElementAt(1).BodyText);
Assert.AreEqual(_pages.Pages.ElementAt(1).PageTitle, result.Pages.ElementAt(1).PageTitle);
Assert.AreEqual(_pages.Pages.ElementAt(2).BodyText, result.Pages.ElementAt(2).BodyText);
Assert.AreEqual(_pages.Pages.ElementAt(2).PageTitle, result.Pages.ElementAt(2).PageTitle);
}
#endregion
#region utility methods
private void InitComplexModels()
{
_textList = new TextList
{
new TextItem{TextString = "First Page"},
new TextItem{TextString = "Second Page"},
new TextItem{TextString = "Third Page"},
new TextItem{TextString = "Fourth Page"}
};
_captions = new Captions {TextStringArray = _textList};
_pageDetails = new PageDetails
{
Captions = _captions,
Pages = "2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453"
};
}
private void InitComplexTreeModel()
{
_slides = new SlideShow{Slides = "1,2,3,4,5,6,7,8"};
_seo = new Seo{MetaDescription = "Test Meta Description", MetaTitle = "Test Meta Title"};
_textPages = new TextPageList
{
new TextPage()
{
PageTitle = "Home Page",
BodyText = "Home Page Body",
Media = _slides,
Seo = _seo
},
new TextPage()
{
PageTitle = "About us Page",
BodyText = "About us Page Body",
Media = _slides,
Seo = _seo
},
new TextPage()
{
PageTitle = "Contact us Page",
BodyText = "Contact us Page Body",
Media = _slides,
Seo = _seo
}
};
_pages = new PageList
{
Pages = _textPages
};
}
#endregion
}
}
@@ -0,0 +1,367 @@
using System;
using Archetype.PropertyConverters;
using Newtonsoft.Json;
using NUnit.Framework;
namespace Archetype.Tests.Serialization
{
[TestFixture]
public class ArchetypeJsonConverterTest
{
private ContactDetails _contactDetails;
private CompanyDetails _companyDetails;
private AnnualStatement _annualStatement;
private MergerDetails _mergerDetails;
private UrlPicker _webSite;
[SetUp]
public void SetUp()
{
_webSite = new UrlPicker
{
Url = "http://test.com",
Title = "The Test Company",
OpenInNewWindow = true
};
_mergerDetails = new MergerDetails
{
MergerDate = new DateTime(2014,6,7,8,9,10),
MergerValue = 12345676890.12m
};
_contactDetails = new ContactDetails
{
Address = "Test Address",
Email = "test@test.com",
Fax = "000",
Mobile = "000",
Name = "Test",
Telephone = "111",
WebSite = _webSite
};
_companyDetails = new CompanyDetails
{
Region = "Test Region",
ContactDetails = _contactDetails
};
_annualStatement = new AnnualStatement
{
FiscalYearStart = new DateTime(2013, 9, 1),
FiscalYearEnd = new DateTime(2014, 8, 31),
TotalShares = 345678,
Sales = 123456700.89,
Profit = 1123456.78m
};
}
#region serialization tests
[Test]
public void MergerDetailsModel_Serializes_To_Archetype_Property()
{
var result = JsonConvert.SerializeObject(_mergerDetails, Formatting.Indented);
Assert.AreEqual(JsonTestStrings._MERGER_DETAILS_JSON, result);
}
[Test]
public void ContactDetailsModel_Serializes_To_Archetype_Property()
{
var result = JsonConvert.SerializeObject(_contactDetails, Formatting.Indented);
Assert.AreEqual(JsonTestStrings._CONTACT_DETAILS_JSON, result);
}
[Test]
public void CompanyDetailsModel_Fieldset_Serializes_As_Expected()
{
var result = JsonConvert.SerializeObject(_companyDetails, Formatting.Indented);
Assert.AreEqual(JsonTestStrings._COMPANY_DETAILS_JSON, result);
}
[Test]
public void AnnualStatementModel_Fieldset_Serializes_As_Expected()
{
var result = JsonConvert.SerializeObject(_annualStatement, Formatting.Indented);
Assert.AreEqual(JsonTestStrings._ANNUAL_STATEMENT_JSON, result);
}
[Test]
public void NullValues_Serialize_To_Empty_String()
{
var result = JsonConvert.SerializeObject(new ContactDetails());
Assert.AreEqual(JsonTestStrings._NULL_VALUES_JSON, result);
}
[Test]
public void ConvertModelToArchetype()
{
var model = new ContactDetails
{
Address = "addr",
Email = "email"
};
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(model);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
[Test]
public void ConvertNumericAndDateModelToArchetype()
{
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(_annualStatement);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
[Test]
public void ConvertCompoundModelToArchetype()
{
var item1 = new ContactDetails
{
Address = "addr",
Email = "email"
};
var item2 = new ContactDetails
{
Address = "addr1",
Name = "email2"
};
var model = new AllContactDetails
{
UserDetails = item1,
AdminDetails = item2
};
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
[Test]
public void ConvertEnumerableModelToArchetype()
{
var item1 = new ContactDetails
{
Address = "addr",
Email = "email"
};
var item2 = new ContactDetails
{
Address = "addr1",
Name = "email2"
};
var model = new ContactDetailsList
{
item1,
item2
};
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
[Test]
public void ConvertNestedModelToArchetype()
{
var model = new CompanyDetails
{
Region = "Test Region",
ContactDetails = new ContactDetails
{
Address = "addr1",
Name = "email2"
}
};
var converter = new ArchetypeValueConverter();
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
var archetype = (Archetype.Models.ArchetypeModel)converter.ConvertDataToSource(null, json, false);
Assert.NotNull(archetype);
}
#endregion
#region deserialization tests
[Test]
public void DeserializeModelFromArchetype()
{
var result = JsonConvert.DeserializeObject<ContactDetails>(JsonTestStrings._CONTACT_DETAILS_JSON);
Assert.NotNull(result);
Assert.IsInstanceOf<ContactDetails>(result);
Assert.AreEqual("Test Address", result.Address);
Assert.AreEqual("test@test.com", result.Email);
Assert.AreEqual("000", result.Fax);
Assert.AreEqual("000", result.Mobile);
Assert.AreEqual("Test", result.Name);
Assert.AreEqual("111", result.Telephone);
Assert.AreEqual(_webSite.Content, result.WebSite.Content);
Assert.AreEqual(_webSite.Media, result.WebSite.Media);
Assert.AreEqual(_webSite.OpenInNewWindow, result.WebSite.OpenInNewWindow);
Assert.AreEqual(_webSite.Title, result.WebSite.Title);
Assert.AreEqual(_webSite.Url, result.WebSite.Url);
}
[Test]
public void DeserializeNumericAndDateModelFromArchetype()
{
var result = JsonConvert.DeserializeObject<AnnualStatement>(JsonTestStrings._ANNUAL_STATEMENT_JSON);
Assert.NotNull(result);
Assert.IsInstanceOf<AnnualStatement>(result);
Assert.AreEqual(new DateTime(2013, 9, 1), result.FiscalYearStart);
Assert.AreEqual(new DateTime(2014, 8, 31), result.FiscalYearEnd);
Assert.IsNull(result.DividendPaymentDate);
Assert.AreEqual(345678, result.TotalShares);
Assert.AreEqual(123456700.89, result.Sales);
Assert.AreEqual(1123456.78m, result.Profit);
}
[Test]
public void DeserializeNumericAndDateModel_NullableDate_HasValue_FromArchetype()
{
var annualStatement = new AnnualStatement
{
FiscalYearStart = new DateTime(2013, 9, 1),
FiscalYearEnd = new DateTime(2014, 8, 31),
DividendPaymentDate = new DateTime(2014,9,15,10,15,30),
TotalShares = 345678,
Sales = 123456700.89,
Profit = 1123456.78m
};
var json = JsonConvert.SerializeObject(annualStatement);
var result = JsonConvert.DeserializeObject<AnnualStatement>(json);
Assert.NotNull(result);
Assert.IsInstanceOf<AnnualStatement>(result);
Assert.AreEqual(annualStatement.FiscalYearStart, result.FiscalYearStart);
Assert.AreEqual(annualStatement.FiscalYearEnd, result.FiscalYearEnd);
Assert.AreEqual(annualStatement.DividendPaymentDate, result.DividendPaymentDate);
Assert.AreEqual(annualStatement.TotalShares, result.TotalShares);
Assert.AreEqual(annualStatement.Sales, result.Sales);
Assert.AreEqual(annualStatement.Profit, result.Profit);
}
[Test]
public void DeserializeCompoundModelFromArchetype()
{
var item1 = new ContactDetails
{
Address = "addr",
Email = "email"
};
var item2 = new ContactDetails
{
Address = "addr1",
Name = "email2"
};
var model = new AllContactDetails
{
UserDetails = item1,
AdminDetails = item2
};
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
var result = JsonConvert.DeserializeObject<AllContactDetails>(json);
Assert.NotNull(result);
Assert.IsInstanceOf<AllContactDetails>(result);
Assert.AreEqual(item1.Address, result.UserDetails.Address);
Assert.AreEqual(item1.Email, result.UserDetails.Email);
Assert.AreEqual(item2.Address, result.AdminDetails.Address);
Assert.AreEqual(item2.Name, result.AdminDetails.Name);
}
[Test]
public void DeserializeEnumerableModelFromArchetype()
{
var item1 = new ContactDetails
{
Address = "addr",
Email = "email"
};
var item2 = new ContactDetails
{
Address = "addr1",
Name = "email2"
};
var model = new ContactDetailsList
{
item1,
item2
};
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
var result = JsonConvert.DeserializeObject<ContactDetailsList>(json);
Assert.NotNull(result);
Assert.IsInstanceOf<ContactDetailsList>(result);
Assert.AreEqual(result.Count, 2);
Assert.AreEqual(item1.Address, result[0].Address);
Assert.AreEqual(item1.Email, result[0].Email);
Assert.AreEqual(item2.Address, result[1].Address);
Assert.AreEqual(item2.Name, result[1].Name);
}
[Test]
public void DeserializeNestedModelFromArchetype()
{
var model = new CompanyDetails
{
Region = "Test Region",
ContactDetails = new ContactDetails
{
Address = "addr1",
Name = "email2"
}
};
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
var result = JsonConvert.DeserializeObject<CompanyDetails>(json);
Assert.NotNull(result);
Assert.IsInstanceOf<CompanyDetails>(result);
Assert.AreEqual(model.Region, result.Region);
Assert.AreEqual(model.ContactDetails.Address, result.ContactDetails.Address);
Assert.AreEqual(model.ContactDetails.Name, result.ContactDetails.Name);
}
#endregion
}
}
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using Archetype.Serialization;
using Newtonsoft.Json;
namespace Archetype.Tests.Serialization
{
#region json test Models
[AsArchetype("urlPicker")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class UrlPicker
{
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("content")]
public int? Content { get; set; }
[JsonProperty("media")]
public int? Media { get; set; }
[JsonProperty("openInNewWindow")]
public bool OpenInNewWindow { get; set; }
}
[AsArchetype("contactDetails")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class ContactDetails
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("telephone")]
public string Telephone { get; set; }
[JsonProperty("mobile")]
public string Mobile { get; set; }
[JsonProperty("fax")]
public string Fax { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("webSite")]
public UrlPicker WebSite { get; set; }
}
[AsArchetype("companyDetails")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class CompanyDetails
{
[JsonProperty("region")]
public string Region { get; set; }
[JsonProperty("contactDetails")]
public ContactDetails ContactDetails { get; set; }
}
[AsArchetype("allContactDetails")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class AllContactDetails
{
public ContactDetails UserDetails { get; set; }
public ContactDetails AdminDetails { get; set; }
}
[AsArchetype("contactDetailsList")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class ContactDetailsList : List<ContactDetails>
{
}
[AsArchetype("annualStatement")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class AnnualStatement
{
public DateTime FiscalYearStart { get; set; }
public DateTime FiscalYearEnd { get; set; }
public DateTime? DividendPaymentDate { get; set; }
public Int32 TotalShares { get; set; }
public Double Sales { get; set; }
public Decimal Profit { get; set; }
}
[AsArchetype("mergerDetails")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class MergerDetails
{
public DateTime MergerDate { get; set; }
[JsonIgnore]
public decimal MergerValue { get; set; }
}
#endregion
#region complex nested model
[AsArchetype("pages")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class PageDetails
{
[JsonProperty("pages")]
public string Pages { get; set; }
[AsFieldset]
[JsonProperty("captions")]
public Captions Captions { get; set; }
}
[AsArchetype("captions")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class Captions
{
[JsonProperty("captions")]
public TextList TextStringArray { get; set; }
}
[AsArchetype("textstringArray")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class TextList : List<TextItem>
{
}
[AsArchetype("textstringArray")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class TextItem
{
[JsonProperty("textstring")]
public string TextString { get; set; }
}
#endregion
#region complex nested tree model
public abstract class PageBase
{
public string PageTitle { get; set; }
public string BodyText { get; set; }
}
[AsArchetype("slideShow")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class SlideShow
{
public string Slides { get; set; }
}
[AsArchetype("seo")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class Seo
{
public string MetaTitle { get; set; }
public string MetaDescription { get; set; }
}
[AsArchetype("TextPage")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class TextPage : PageBase
{
[AsFieldset]
public SlideShow Media { get; set; }
[AsFieldset]
public Seo Seo { get; set; }
}
[AsArchetype("TextPageList")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class TextPageList : List<TextPage>
{
}
[AsArchetype("Pages")]
[JsonConverter(typeof(ArchetypeJsonConverter))]
public class PageList
{
public TextPageList Pages { get; set; }
}
#endregion
}
@@ -0,0 +1,435 @@
namespace Archetype.Tests.Serialization
{
public class JsonTestStrings
{
public const string _MERGER_DETAILS_JSON =
@"{
""fieldsets"": [
{
""alias"": ""mergerDetails"",
""properties"": [
{
""alias"": ""MergerDate"",
""value"": ""07/06/2014 08:09:10""
}
]
}
]
}";
public const string _ANNUAL_STATEMENT_JSON =
@"{
""fieldsets"": [
{
""alias"": ""annualStatement"",
""properties"": [
{
""alias"": ""FiscalYearStart"",
""value"": ""01/09/2013 00:00:00""
},
{
""alias"": ""FiscalYearEnd"",
""value"": ""31/08/2014 00:00:00""
},
{
""alias"": ""DividendPaymentDate"",
""value"": """"
},
{
""alias"": ""TotalShares"",
""value"": ""345678""
},
{
""alias"": ""Sales"",
""value"": ""123456700.89""
},
{
""alias"": ""Profit"",
""value"": ""1123456.78""
}
]
}
]
}";
public const string _CONTACT_DETAILS_JSON =
@"{
""fieldsets"": [
{
""alias"": ""contactDetails"",
""properties"": [
{
""alias"": ""name"",
""value"": ""Test""
},
{
""alias"": ""address"",
""value"": ""Test Address""
},
{
""alias"": ""telephone"",
""value"": ""111""
},
{
""alias"": ""mobile"",
""value"": ""000""
},
{
""alias"": ""fax"",
""value"": ""000""
},
{
""alias"": ""email"",
""value"": ""test@test.com""
},
{
""alias"": ""webSite"",
""value"": {
""fieldsets"": [
{
""alias"": ""urlPicker"",
""properties"": [
{
""alias"": ""title"",
""value"": ""The Test Company""
},
{
""alias"": ""url"",
""value"": ""http://test.com""
},
{
""alias"": ""content"",
""value"": """"
},
{
""alias"": ""media"",
""value"": """"
},
{
""alias"": ""openInNewWindow"",
""value"": ""1""
}
]
}
]
}
}
]
}
]
}";
public const string _COMPANY_DETAILS_JSON =
@"{
""fieldsets"": [
{
""alias"": ""companyDetails"",
""properties"": [
{
""alias"": ""region"",
""value"": ""Test Region""
},
{
""alias"": ""contactDetails"",
""value"": {
""fieldsets"": [
{
""alias"": ""contactDetails"",
""properties"": [
{
""alias"": ""name"",
""value"": ""Test""
},
{
""alias"": ""address"",
""value"": ""Test Address""
},
{
""alias"": ""telephone"",
""value"": ""111""
},
{
""alias"": ""mobile"",
""value"": ""000""
},
{
""alias"": ""fax"",
""value"": ""000""
},
{
""alias"": ""email"",
""value"": ""test@test.com""
},
{
""alias"": ""webSite"",
""value"": {
""fieldsets"": [
{
""alias"": ""urlPicker"",
""properties"": [
{
""alias"": ""title"",
""value"": ""The Test Company""
},
{
""alias"": ""url"",
""value"": ""http://test.com""
},
{
""alias"": ""content"",
""value"": """"
},
{
""alias"": ""media"",
""value"": """"
},
{
""alias"": ""openInNewWindow"",
""value"": ""1""
}
]
}
]
}
}
]
}
]
}
}
]
}
]
}";
public const string _PAGE_DETAILS_JSON =
@"{
""fieldsets"": [
{
""alias"": ""pages"",
""properties"": [
{
""alias"": ""pages"",
""value"": ""2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453""
}
]
},
{
""alias"": ""captions"",
""properties"": [
{
""alias"": ""captions"",
""value"": {
""fieldsets"": [
{
""alias"": ""textstringArray"",
""properties"": [
{
""alias"": ""textstring"",
""value"": ""First Page""
}
]
},
{
""alias"": ""textstringArray"",
""properties"": [
{
""alias"": ""textstring"",
""value"": ""Second Page""
}
]
},
{
""alias"": ""textstringArray"",
""properties"": [
{
""alias"": ""textstring"",
""value"": ""Third Page""
}
]
},
{
""alias"": ""textstringArray"",
""properties"": [
{
""alias"": ""textstring"",
""value"": ""Fourth Page""
}
]
}
]
}
}
]
}
]
}";
public const string _PAGES_JSON =
@"{
""fieldsets"": [
{
""alias"": ""Pages"",
""properties"": [
{
""alias"": ""Pages"",
""value"": {
""fieldsets"": [
{
""alias"": ""TextPage"",
""properties"": [
{
""alias"": ""Media"",
""value"": {
""fieldsets"": [
{
""alias"": ""slideShow"",
""properties"": [
{
""alias"": ""Slides"",
""value"": ""1,2,3,4,5,6,7,8""
}
]
}
]
}
},
{
""alias"": ""Seo"",
""value"": {
""fieldsets"": [
{
""alias"": ""seo"",
""properties"": [
{
""alias"": ""MetaTitle"",
""value"": ""Test Meta Title""
},
{
""alias"": ""MetaDescription"",
""value"": ""Test Meta Description""
}
]
}
]
}
},
{
""alias"": ""PageTitle"",
""value"": ""Home Page""
},
{
""alias"": ""BodyText"",
""value"": ""Home Page Body""
}
]
},
{
""alias"": ""TextPage"",
""properties"": [
{
""alias"": ""Media"",
""value"": {
""fieldsets"": [
{
""alias"": ""slideShow"",
""properties"": [
{
""alias"": ""Slides"",
""value"": ""1,2,3,4,5,6,7,8""
}
]
}
]
}
},
{
""alias"": ""Seo"",
""value"": {
""fieldsets"": [
{
""alias"": ""seo"",
""properties"": [
{
""alias"": ""MetaTitle"",
""value"": ""Test Meta Title""
},
{
""alias"": ""MetaDescription"",
""value"": ""Test Meta Description""
}
]
}
]
}
},
{
""alias"": ""PageTitle"",
""value"": ""About us Page""
},
{
""alias"": ""BodyText"",
""value"": ""About us Page Body""
}
]
},
{
""alias"": ""TextPage"",
""properties"": [
{
""alias"": ""Media"",
""value"": {
""fieldsets"": [
{
""alias"": ""slideShow"",
""properties"": [
{
""alias"": ""Slides"",
""value"": ""1,2,3,4,5,6,7,8""
}
]
}
]
}
},
{
""alias"": ""Seo"",
""value"": {
""fieldsets"": [
{
""alias"": ""seo"",
""properties"": [
{
""alias"": ""MetaTitle"",
""value"": ""Test Meta Title""
},
{
""alias"": ""MetaDescription"",
""value"": ""Test Meta Description""
}
]
}
]
}
},
{
""alias"": ""PageTitle"",
""value"": ""Contact us Page""
},
{
""alias"": ""BodyText"",
""value"": ""Contact us Page Body""
}
]
}
]
}
}
]
}
]
}";
public const string _NULL_VALUES_JSON = @"{""fieldsets"":[{""alias"":""contactDetails"",""properties"":[{""alias"":""name"",""value"":""""},{""alias"":""address"",""value"":""""},{""alias"":""telephone"",""value"":""""},{""alias"":""mobile"",""value"":""""},{""alias"":""fax"",""value"":""""},{""alias"":""email"",""value"":""""},{""alias"":""webSite"",""value"":""""}]}]}";
}
}
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@@ -42,6 +42,7 @@
</Reference>
<Reference Include="businesslogic">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\businesslogic.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ClientDependency.Core">
<HintPath>..\packages\ClientDependency.1.7.1.2\lib\ClientDependency.Core.dll</HintPath>
@@ -53,9 +54,11 @@
</Reference>
<Reference Include="cms">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\cms.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="controls">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\controls.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="CookComputing.XmlRpcV2">
<HintPath>..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll</HintPath>
@@ -63,6 +66,7 @@
</Reference>
<Reference Include="Examine">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\Examine.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="HtmlAgilityPack">
<HintPath>..\packages\HtmlAgilityPack.1.4.6\lib\Net45\HtmlAgilityPack.dll</HintPath>
@@ -74,15 +78,19 @@
</Reference>
<Reference Include="ImageProcessor">
<HintPath>..\packages\ImageProcessor.1.9.0.0\lib\ImageProcessor.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImageProcessor.Web">
<HintPath>..\packages\ImageProcessor.Web.3.2.3.0\lib\net45\ImageProcessor.Web.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="interfaces">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\interfaces.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="log4net">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\log4net.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lucene.Net">
<HintPath>..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll</HintPath>
@@ -90,9 +98,11 @@
</Reference>
<Reference Include="Microsoft.ApplicationBlocks.Data">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\Microsoft.ApplicationBlocks.Data.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Web.Helpers">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\Microsoft.Web.Helpers.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<Private>False</Private>
@@ -116,15 +126,16 @@
</Reference>
<Reference Include="SQLCE4Umbraco">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\SQLCE4Umbraco.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<Private>True</Private>
<Private>False</Private>
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\System.Data.SqlServerCe.dll</HintPath>
</Reference>
<Reference Include="System.Data.SqlServerCe.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<Private>True</Private>
<Private>False</Private>
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\System.Data.SqlServerCe.Entity.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http" />
@@ -173,36 +184,47 @@
<Reference Include="System.Xml" />
<Reference Include="TidyNet">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\TidyNet.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="umbraco">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\umbraco.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Umbraco.Core">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\Umbraco.Core.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="umbraco.DataLayer">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\umbraco.DataLayer.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="umbraco.editorControls">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\umbraco.editorControls.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="umbraco.MacroEngines">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\umbraco.MacroEngines.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="umbraco.providers">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\umbraco.providers.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Umbraco.Web.UI">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\Umbraco.Web.UI.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="umbraco.XmlSerializers">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\umbraco.XmlSerializers.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UmbracoExamine">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\UmbracoExamine.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UrlRewritingNet.UrlRewriter">
<HintPath>..\packages\UmbracoCms.Core.7.1.2\lib\UrlRewritingNet.UrlRewriter.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
@@ -220,6 +242,10 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\VersionInfo.cs" />
<Compile Include="PropertyConverters\ArchetypeValueConverter.cs" />
<Compile Include="Serialization\AsArchetypeAttribute.cs" />
<Compile Include="Serialization\ArchetypeJsonConverter.cs" />
<Compile Include="Serialization\AsFieldsetAttribute.cs" />
<Compile Include="Serialization\SerializationExtensions.cs" />
<Compile Include="PropertyEditors\ArchetypePropertyEditor.cs" />
</ItemGroup>
<ItemGroup>
@@ -1,9 +1,12 @@
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Web;
using Archetype.Models;
@@ -23,5 +26,59 @@ namespace Archetype.Extensions
{
return prop.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditorAlias);
}
public static IHtmlString RenderArchetypePartials(this HtmlHelper htmlHelper, ArchetypeModel archetypeModel)
{
return RenderPartials(htmlHelper, archetypeModel, null, null);
}
public static IHtmlString RenderArchetypePartials(this HtmlHelper htmlHelper, ArchetypeModel archetypeModel, string partialPath)
{
return RenderPartials(htmlHelper, archetypeModel, partialPath, null);
}
public static IHtmlString RenderArchetypePartials(this HtmlHelper htmlHelper, ArchetypeModel archetypeModel, ViewDataDictionary viewDataDictionary)
{
return RenderPartials(htmlHelper, archetypeModel, null, viewDataDictionary);
}
public static IHtmlString RenderArchetypePartials(this HtmlHelper htmlHelper, ArchetypeModel archetypeModel, string partialPath, ViewDataDictionary viewDataDictionary)
{
return RenderPartials(htmlHelper, archetypeModel, partialPath, viewDataDictionary);
}
private static IHtmlString RenderPartials(HtmlHelper htmlHelper, ArchetypeModel archetypeModel, string partialPath, ViewDataDictionary viewDataDictionary)
{
var context = HttpContext.Current;
if (archetypeModel == null || context == null)
{
return new HtmlString("");
}
var sb = new StringBuilder();
var pathToPartials = "~/Views/Partials/Archetype/";
if (!string.IsNullOrEmpty(partialPath))
{
pathToPartials = partialPath;
}
foreach (var fieldsetModel in archetypeModel)
{
var partial = pathToPartials + fieldsetModel.Alias + ".cshtml";
if (System.IO.File.Exists(context.Server.MapPath(partial)))
{
sb.AppendLine(htmlHelper.Partial(partial, fieldsetModel, viewDataDictionary).ToString());
}
else
{
LogHelper.Info<ArchetypeModel>(string.Format("The partial for {0} could not be found. Please create a partial with that name or rename your alias.", context.Server.MapPath(partial)));
}
}
return new HtmlString(sb.ToString());
}
}
}
@@ -44,6 +44,9 @@ namespace Archetype.Models
[JsonProperty("customJsPath")]
public string CustomJsPath { get; set; }
[JsonProperty("customViewPath")]
public string CustomViewPath { get; set; }
[JsonProperty("developerMode")]
public bool DeveloperMode { get; set; }
}
@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
[assembly: AssemblyVersion("1.0")]
[assembly: AssemblyFileVersion("1.0-beta")]
[assembly: AssemblyVersion("1.2")]
[assembly: AssemblyFileVersion("1.2-beta")]
@@ -16,8 +16,7 @@ using Umbraco.Core.Logging;
namespace Archetype.PropertyEditors
{
[PropertyEditorAsset(ClientDependencyType.Javascript, "/App_Plugins/Archetype/js/archetype.js")]
[PropertyEditor("Imulus.Archetype", "Archetype", "/App_Plugins/Archetype/views/archetype.html",
ValueType = "JSON")]
[PropertyEditor("Imulus.Archetype", "Archetype", "/App_Plugins/Archetype/views/archetype.html", ValueType = "JSON")]
public class ArchetypePropertyEditor : PropertyEditor
{
#region Pre Value Editor
@@ -0,0 +1,354 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Archetype.Serialization
{
public class ArchetypeJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var models = GenerateModels(value);
if (models.Count < 1)
return;
if (models.Count == 1 && models[0] == null)
return;
var jObj = new JObject
{
{
"fieldsets",
new JArray( new JRaw(SerializeModels(models)))
}
};
writer.WriteRaw(ApplyFormatting(jObj.ToString(), writer.Formatting));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jToken = JToken.ReadFrom(reader);
if (jToken == null)
return null;
var obj = Activator.CreateInstance(objectType);
if (null != obj as IEnumerable<object>
&& jToken["fieldsets"] != null && jToken["fieldsets"].Any())
{
return DeserializeEnumerableObject(obj, jToken);
}
return null == jToken as JArray
? DeserializeObject(obj, jToken)
: PopulateProperties(obj, jToken);
}
public override bool CanConvert(Type objectType)
{
return IsValueArchetypeDatatype(objectType);
}
#region private methods
private string ApplyFormatting(string json, Formatting formatting)
{
return JToken.Parse(json).ToString(formatting);
}
private IList GenerateModels(object value)
{
var models = value as IList;
if (null != models)
return models;
var properties = GetSerialiazableProperties(value).ToList();
if (!PropertyLayoutHasFieldsets(properties))
return new List<object>() { value };
var dynamicModel = new ExpandoObject() as IDictionary<string, object>;
foreach (var pInfo in properties.Where(pInfo => !HasAsFieldsetAttribute(pInfo)))
{
dynamicModel.Add(GetJsonPropertyName(pInfo), pInfo.GetValue(value));
}
var fieldsetModels = properties.Where(HasAsFieldsetAttribute)
.Select(pInfo => pInfo.GetValue(value));
models = new List<object>
{
dynamicModel,
};
models = ((IEnumerable<object>)models).Concat(fieldsetModels).ToList();
return models;
}
private object DeserializeEnumerableObject(object obj, JToken jToken)
{
var model = obj as IEnumerable<object>;
var itemType = model.GetType().BaseType.GetGenericArguments().First();
foreach (var fs in jToken["fieldsets"].Where(fs => fs["alias"].ToString().Equals(GetFieldsetName(itemType))))
{
var item = JsonConvert.DeserializeObject(
fs["properties"].ToString(), itemType, GetArchetypeDatatypeConverter());
obj.GetType().GetMethod("Add").Invoke(obj, new[] { item });
}
return obj;
}
private object DeserializeObject(object obj, JToken jToken)
{
var properties = GetSerialiazableProperties(obj).ToList();
var asFieldset = properties.Where(HasAsFieldsetAttribute).ToList();
foreach (var propInfo in asFieldset)
{
var propAlias = GetJsonPropertyName(propInfo);
var fsJToken = GetFieldsetProperties(propInfo.PropertyType, jToken);
var propJToken =
fsJToken.Single(p => p.SelectToken("alias").ToString().Equals(propAlias));
if (propJToken == null)
continue;
var propValue = IsValueArchetypeDatatype(propInfo.PropertyType)
? JsonConvert.DeserializeObject(fsJToken.ToString(), propInfo.PropertyType,
GetArchetypeDatatypeConverter())
: GetDeserializedPropertyValue(fsJToken, propInfo.PropertyType); ;
propInfo.SetValue(obj, propValue);
}
return PopulateProperties(obj, GetFieldsetProperties(obj.GetType(), jToken));
}
private JToken GetFieldsetProperties(Type objType, JToken jToken)
{
var objAlias = GetFieldsetName(objType);
return
jToken["fieldsets"].Single(p => p.SelectToken("alias").ToString().Equals(objAlias)).SelectToken("properties");
}
private object PopulateProperties(object obj, JToken jToken)
{
var properties = GetSerialiazableProperties(obj);
foreach (var propertyInfo in properties)
{
var propAlias = GetJsonPropertyName(propertyInfo);
var propJToken = jToken.SingleOrDefault(p => p.SelectToken("alias").ToString().Equals(propAlias));
if (propJToken == null)
continue;
var propValue = GetPropertyValue(propertyInfo, propJToken);
propertyInfo.SetValue(obj, propValue);
}
return obj;
}
private object GetPropertyValue(PropertyInfo propertyInfo, JToken propJToken)
{
return IsValueArchetypeDatatype(propertyInfo.PropertyType)
? JsonConvert.DeserializeObject(propJToken["value"].ToString(), propertyInfo.PropertyType,
GetArchetypeDatatypeConverter())
: GetDeserializedPropertyValue(propJToken["value"], propertyInfo.PropertyType);
}
private bool PropertyLayoutHasFieldsets(IEnumerable<PropertyInfo> properties)
{
return properties.Any(HasAsFieldsetAttribute);
}
private static bool HasAsFieldsetAttribute(PropertyInfo pInfo)
{
return pInfo
.GetCustomAttributes(typeof(AsFieldsetAttribute), true).Length > 0;
}
private IEnumerable SerializeModels(IEnumerable models)
{
var fieldsetJson = (from object model in models where null != model select SerializeModel(model)).ToList();
return String.Join(",", fieldsetJson);
}
private string SerializeModel(object value)
{
if (value == null)
return null;
var jObj = IsExpandoObject(value)
? GetJObjectFromExpandoObject(value as IDictionary<string, object>)
: GetJObject(value);
var fieldsetJson = new StringBuilder();
var fieldsetWriter = new StringWriter(fieldsetJson);
using (var jsonWriter = new JsonTextWriter(fieldsetWriter))
{
jObj.WriteTo(jsonWriter);
}
return fieldsetJson.ToString();
}
private bool IsExpandoObject(object value)
{
return value.GetType().Name.Equals(typeof (ExpandoObject).Name);
}
private JObject GetJObjectFromExpandoObject(IEnumerable<KeyValuePair<string, object>> obj)
{
var property = obj.ElementAt(0);
var alias = property.Key;
var value = property.Value;
var jObj = new JObject
{
{
"alias",
new JValue(alias)
}
};
var fsProperties = new List<JObject>
{
new JObject {new JProperty("alias", alias), new JProperty("value", value)}
};
jObj.Add("properties", new JRaw(JsonConvert.SerializeObject(fsProperties)));
return jObj;
}
private JObject GetJObject(object obj)
{
var jObj = new JObject
{
{
"alias",
new JValue(GetFieldsetName(obj.GetType()))
}
};
var properties = GetSerialiazableProperties(obj);
var fsProperties = new List<JObject>();
foreach (var propertyInfo in properties)
{
var fsProperty = new JObject();
var jProperty = new JProperty("alias", GetJsonPropertyName(propertyInfo));
fsProperty.Add(jProperty);
var propValue = propertyInfo.GetValue(obj, null);
fsProperty.Add(
new JProperty("value",
IsValueArchetypeDatatype(propValue)
? new JRaw(JsonConvert.SerializeObject(propValue,
GetArchetypeDatatypeConverter()))
: new JValue(GetSerializedPropertyValue(propValue))));
fsProperties.Add(fsProperty);
}
jObj.Add("properties", new JRaw(JsonConvert.SerializeObject(fsProperties)));
return jObj;
}
private IEnumerable<PropertyInfo> GetSerialiazableProperties(object obj)
{
return obj.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(prop => !Attribute.IsDefined(prop, typeof(JsonIgnoreAttribute)));
}
private string GetJsonPropertyName(PropertyInfo property)
{
var attributes = property.GetCustomAttributes(true);
var jsonPropAttribute = (JsonPropertyAttribute)attributes.FirstOrDefault(attr => attr is JsonPropertyAttribute);
return jsonPropAttribute != null ? jsonPropAttribute.PropertyName : property.Name;
}
private string GetFieldsetName(Type type)
{
var attributes = type.GetCustomAttributes(true);
var archetypeDatatypeAttribute = (AsArchetypeAttribute)attributes.FirstOrDefault(attr => attr is AsArchetypeAttribute);
return archetypeDatatypeAttribute != null ? archetypeDatatypeAttribute.FieldsetName : type.Name;
}
private bool IsValueArchetypeDatatype(object value)
{
return value != null &&
IsValueArchetypeDatatype(value.GetType());
}
private bool IsValueArchetypeDatatype(Type type)
{
return type.GetCustomAttributes(typeof(AsArchetypeAttribute), true).Length > 0;
}
private JsonConverter GetArchetypeDatatypeConverter()
{
return (JsonConverter)Activator.CreateInstance(GetType());
}
private string GetSerializedPropertyValue(object propValue)
{
if (propValue == null)
return String.Empty;
if (propValue is bool)
return (bool)propValue ? GetSerializedPropertyValue(1) : GetSerializedPropertyValue(0);
return String.Format("{0}", propValue);
}
private object GetDeserializedPropertyValue(JToken jToken, Type type)
{
if (String.IsNullOrEmpty(jToken.ToString()))
return GetDefault(type);
var localType = Nullable.GetUnderlyingType(type) ?? type;
if (localType == typeof(bool))
return jToken.ToString() == "1";
return localType == typeof(DateTime)
? Convert.ToDateTime(jToken.ToString())
: jToken.ToObject(localType);
}
private object GetDefault(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
#endregion
}
}
@@ -0,0 +1,14 @@
using System;
namespace Archetype.Serialization
{
public class AsArchetypeAttribute : Attribute
{
public string FieldsetName { get; set; }
public AsArchetypeAttribute(string fieldsetname)
{
FieldsetName = fieldsetname;
}
}
}
@@ -0,0 +1,8 @@
using System;
namespace Archetype.Serialization
{
public class AsFieldsetAttribute : Attribute
{
}
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Umbraco.Core.Models;
using Umbraco.Web;
namespace Archetype.Serialization
{
public static class SerializationExtensions
{
//temporary
public static string GetPropertyDataValue(this IPublishedContent contentPage, string propertyAlias)
{
if (contentPage == null || !contentPage.HasProperty(propertyAlias))
return null;
return (string)contentPage.Properties.Single(p => p.PropertyTypeAlias == propertyAlias).DataValue;
}
public static T GetModelFromArchetype<T>(this IPublishedContent content,
string cmsFieldAlias, bool returnInstanceIfNull = false)
where T : class, new()
{
var archetypeJson = content.GetPropertyDataValue(cmsFieldAlias);
return JsonConvert.DeserializeObject<T>(archetypeJson) ??
(returnInstanceIfNull ? Activator.CreateInstance<T>() : default(T));
}
}
}
+31
View File
@@ -0,0 +1,31 @@
angular.module("umbraco.directives").directive('archetypeCustomView', function ($compile, $http) {
var linker = function (scope, element, attrs) {
var config = scope.archetype.config.archetypeConfig;
var view = "/App_plugins/Archetype/views/archetype.default.html";
if(config.customViewPath) {
view = config.customViewPath;
}
$http.get(view).then(function(data) {
scope.model = {};
scope.model.value = scope.archetype.value;
scope.model.config = scope.archetype.config;
element.html(data.data).show();
$compile(element.contents())(scope);
});
}
return {
restrict: "A",
replace: true,
link: linker,
scope: {
archetype: "="
}
}
});
+73 -42
View File
@@ -1,5 +1,5 @@
angular.module("umbraco.directives").directive('archetypeProperty', function ($compile, $http, archetypePropertyEditorResource, umbPropEditorHelper, $timeout, $rootScope, $q) {
function getFieldsetByAlias(fieldsets, alias)
{
return _.find(fieldsets, function(fieldset){
@@ -56,6 +56,10 @@ angular.module("umbraco.directives").directive('archetypeProperty', function ($c
var alias = configFieldsetModel.properties[scope.propertyConfigIndex].alias;
var defaultValue = configFieldsetModel.properties[scope.propertyConfigIndex].value;
var umbracoPropertyAlias = scope.umbracoPropertyAlias;
// initialize container for invalid fieldset property identifiers (store on ngModelCtrl to separate Archetype validations, e.g. when there two Archetype properties on the same document)
if(ngModelCtrl.invalidProperties == null) {
ngModelCtrl.invalidProperties = [];
}
//try to convert the defaultValue to a JS object
defaultValue = jsonOrString(defaultValue, scope.archetypeConfig.developerMode, "defaultValue");
@@ -81,48 +85,77 @@ angular.module("umbraco.directives").directive('archetypeProperty', function ($c
var mergedConfig = _.extend(defaultConfigObj, config);
loadView(pathToView, mergedConfig, defaultValue, alias, umbracoPropertyAlias, scope, element, ngModelCtrl);
loadView(pathToView, mergedConfig, defaultValue, alias, umbracoPropertyAlias, scope, element, ngModelCtrl, validateProperty);
});
});
ngModelCtrl.$parsers.push(validate);
ngModelCtrl.$formatters.push(validate);
function validate(renderModel){
var valid = true;
_.each(renderModel, function(fieldset){
fieldset.isValid = true;
_.each(fieldset.properties, function(property){
property.isValid = true;
var propertyConfig = getPropertyByAlias(configFieldsetModel, property.alias);
if (propertyConfig) {
if(propertyConfig.required && (property.value == null || property.value === "")) {
fieldset.isValid = false;
property.isValid = false;
valid = false;
}
// issue 116: RegEx validate property value
// Only validate the property value if anything has been entered - RegEx is considered a supplement to "required".
if (valid == true && propertyConfig.regEx && property.value) {
var regEx = new RegExp(propertyConfig.regEx);
if (regEx.test(property.value) == false) {
fieldset.isValid = false;
property.isValid = false;
valid = false;
}
}
}
});
scope.$on("formSubmitting", function (ev, args) {
// "hard" validate to highlight any erroneous entries
_.each(scope.fieldset.properties, function (property) {
validateProperty(scope.fieldset, property, true);
});
});
ngModelCtrl.$setValidity('validation', valid);
return renderModel;
scope.$on("formSubmitted", function (ev, args) {
// reset the nested fieldset validation state after submit
ngModelCtrl.invalidProperties = [];
});
// need to pass the property fieldset here to clear any invalid state of the fieldset when validating a single fieldset property
// - it's the Umbraco way to hide the invalid state when altering an invalid property, even if the new value isn't valid either
function validateProperty(fieldset, property, markAsInvalid) {
var valid = true;
fieldset.isValid = true;
property.isValid = true;
var propertyConfig = getPropertyByAlias(configFieldsetModel, property.alias);
if (propertyConfig) {
// use property.value !== property.value to check for NaN values on numeric inputs
if (propertyConfig.required && (property.value == null || property.value === "" || property.value !== property.value)) {
valid = false;
}
// issue 116: RegEx validate property value
// Only validate the property value if anything has been entered - RegEx is considered a supplement to "required".
if (valid == true && propertyConfig.regEx && property.value) {
var regEx = new RegExp(propertyConfig.regEx);
if (regEx.test(property.value) == false) {
valid = false;
}
}
// only mark the property as invalid when doing a "hard" validation
if (valid == false && markAsInvalid == true) {
property.isValid = false;
}
}
// handle nested fieldset validation by storing the identifier of all invalid fieldset properties
var fieldsetIdentifier = scope.umbracoPropertyAlias + "_" + scope.fieldsetIndex;
var propertyIdentifier = fieldsetIdentifier + "_" + property.alias;
var propertyIdentifierIndex = ngModelCtrl.invalidProperties.indexOf(propertyIdentifier);
if (valid == false) {
if (propertyIdentifierIndex == -1) {
ngModelCtrl.invalidProperties.push(propertyIdentifier);
}
}
else {
if (propertyIdentifierIndex != -1) {
ngModelCtrl.invalidProperties.splice(propertyIdentifierIndex, 1);
}
}
if (markAsInvalid) {
// mark the entire fieldset as invalid if there are any invalid properties in the fieldset, otherwise mark it as valid
fieldset.isValid =
_.find(ngModelCtrl.invalidProperties, function (item) {
return item.indexOf(fieldsetIdentifier) == 0
}) == null;
}
// set invalid state if one or more fieldsets contain invalid properties
ngModelCtrl.$setValidity('validation', ngModelCtrl.invalidProperties.length == 0);
}
}
function loadView(view, config, defaultValue, alias, umbracoPropertyAlias, scope, element, ngModelCtrl) {
function loadView(view, config, defaultValue, alias, umbracoPropertyAlias, scope, element, ngModelCtrl, validateProperty) {
if (view)
{
$http.get(view).success(function (data) {
@@ -157,11 +190,9 @@ angular.module("umbraco.directives").directive('archetypeProperty', function ($c
scope.$watch('model.value', function (newValue, oldValue) {
scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties[renderModelPropertyIndex].value = newValue;
// don't set the current value twice - this will keep Umbraco from prompting to discard changes immediately after saving
if (newValue != oldValue) {
//trigger the validation pipeline
ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue);
}
// call validation method for the property when the value changes
// use "soft" validation to mimic the default umbraco validation style (show error highlights on submit, not while entering data)
validateProperty(scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex], scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties[renderModelPropertyIndex], false);
});
element.html(data).show();
@@ -171,7 +202,7 @@ angular.module("umbraco.directives").directive('archetypeProperty', function ($c
var def = $q.defer();
def.resolve(true);
$rootScope.$apply();
}, 500)
}, 500);
}
});
}
@@ -180,7 +211,7 @@ angular.module("umbraco.directives").directive('archetypeProperty', function ($c
return {
require: "^ngModel",
restrict: "E",
rep1ace: true,
replace: true,
link: linker,
scope: {
property: '=',
+4
View File
@@ -125,6 +125,10 @@
<label for="archetypeAdvancedOptionsCustomJs"><archetype-localize key="jsFile">JS File</archetype-localize><small><archetype-localize key="jsFileDescription">(Optional) Enter a path for a custom JS file to be included on the page.</archetype-localize></small></label>
<input type="text" id="archetypeAdvancedOptionsCustomJs" ng-model="archetypeConfigRenderModel.customJsPath"/>
</div>
<div>
<label for="archetypeAdvancedOptionsCustomView"><archetype-localize key="viewFile">View File</archetype-localize><small><archetype-localize key="viewFileDescription">(Optional) Enter a path for a custom view file to be used to render this Archetype.</archetype-localize></small></label>
<input type="text" id="archetypeAdvancedOptionsCustomView" ng-model="archetypeConfigRenderModel.customViewPath"/>
</div>
<div>
<label for="archetypeAdvancedOptionsDeveloperMode"><archetype-localize key="toggleDeveloperMode">Toggle Developer Mode</archetype-localize><small><archetype-localize key="toggleDeveloperModeDescription">Toggle Developer Mode</archetype-localize></small></label>
<input type="checkbox" id="archetypeAdvancedOptionsDeveloperMode" ng-model="archetypeConfigRenderModel.developerMode"/>
+62
View File
@@ -0,0 +1,62 @@
<div class="archetypeEditor ng-class:model.config.customCssClass" ng-controller="Imulus.ArchetypeController">
<textarea class="archetypeDeveloperModel" ng-show="model.config.developerMode" ng-model="model.value"></textarea>
<ul ui-sortable="sortableOptions" ng-model="model.value.fieldsets" ng-show="!showAddButton()">
<li ng-repeat="fieldset in model.value.fieldsets">
<fieldset ng-class="[fieldset.alias, (!fieldset.isValid ? 'archetypeFieldsetError' : '')]" ng-init="fieldsetConfigModel = getConfigFieldsetByAlias(fieldset.alias)">
<div class="archetypeFieldsetLabel" ng-class="{enableCollapsing: model.config.enableCollapsing}">
<div ng-click="focusFieldset(fieldset)" class="label-sub module-label">
<span class="caret" ng-hide="fieldset.collapse || !model.config.enableCollapsing"></span>
<span class="caret caret-right" ng-show="fieldset.collapse && model.config.enableCollapsing"></span>
<label>
<i class="fieldsetIcon icon ng-class:fieldsetConfigModel.icon"></i>
<span ng-bind="getFieldsetTitle(fieldsetConfigModel, $index)"></span>
</label>
</div>
<div class="archetypeEditorControls label-sub" ng-hide="model.config.hideFieldsetControls">
<i class="icon icon-add dropdown-toggle" ng-show="canAdd()" data-toggle="{{ model.config.fieldsets.length > 1 ? 'dropdown' : ''}}" ng-click="model.config.fieldsets.length == 1 ? addRow(fieldset.alias, $index) : return"></i>
<ul class="dropdown-menu umb-actions">
<li ng-repeat="fieldsetModel in model.config.fieldsets" ng-click="addRow(fieldsetModel.alias, $parent.$index)">
<a>
<i class="fieldsetIcon icon ng-class:fieldsetModel.icon"></i>
<span class="menu-label">{{fieldsetModel.label}}</span>
</a>
</li>
</ul>
<i class="icon icon-remove" ng-click="removeRow($index)" ng-show="canRemove()"></i>
<i class="icon icon-navigation handle" ng-show="canSort()"></i>
</div>
</div>
<div class="archetypeCollapser animate-hide" ng-hide="isCollapsed(fieldset)">
<form class="form-inline">
<div ng-class="[property.alias, (getPropertyValidity($parent.$index, property.alias) === false ? 'archetypePropertyError' : '')]" class="archetypeProperty control-group" ng-repeat="property in fieldsetConfigModel.properties">
<label ng-hide="archetypeConfig.hidePropertyLabels == '1'" class="control-label" for="archetype-property-{{model.alias}}-{{$parent.$index}}-{{$index}}">
<span>{{property.label}}</span>
<div class="archetypeFieldsetHelpText" ng-show="property.helpText">
<small>{{property.helpText}}</small>
</div>
</label>
<div class="controls">
<archetype-property class="archetypeEditor" property="property" fieldset-index="$parent.$index" fieldset="fieldset" archetype-config="model.config" property-config-index="$index" archetype-render-model="model.value" umbraco-property-alias="model.alias" umbraco-form = "form"></archetype-property>
</div>
</div>
</form>
</div>
</fieldset>
</li>
</ul>
<div ng-show="showAddButton()">
<a data-toggle="{{ model.config.fieldsets.length > 1 ? 'dropdown' : ''}}" ng-click="model.config.fieldsets.length == 1 ? addRow(model.config.fieldsets[0].alias, 0) : return" prevent-default>
<i class="icon icon-add dropdown-toggle"></i>
<localize key="general_add" class="archetypeAddButtonText">Add</localize>
</a>
<ul class="dropdown-menu umb-actions">
<li ng-repeat="fieldsetModel in model.config.fieldsets" ng-click="addRow(fieldsetModel.alias, 0)">
<a>
<i class="fieldsetIcon icon ng-class:fieldsetModel.icon"></i>
<span class="menu-label">{{fieldsetModel.label}}</span>
</a>
</li>
</ul>
</div>
</div>
+1 -62
View File
@@ -1,62 +1 @@
<div class="archetypeEditor ng-class:model.config.customCssClass" ng-controller="Imulus.ArchetypeController">
<textarea class="archetypeDeveloperModel" ng-show="model.config.developerMode" ng-model="model.value"></textarea>
<ul ui-sortable="sortableOptions" ng-model="model.value.fieldsets" ng-show="!showAddButton()">
<li ng-repeat="fieldset in model.value.fieldsets">
<fieldset class="ng-class:fieldset.alias" ng-class="{archetypeFieldsetError: !fieldset.isValid}" ng-init="fieldsetConfigModel = getConfigFieldsetByAlias(fieldset.alias)">
<div class="archetypeFieldsetLabel" ng-class="{enableCollapsing: model.config.enableCollapsing}">
<div ng-click="focusFieldset(fieldset)" class="label-sub module-label">
<span class="caret" ng-hide="fieldset.collapse || !model.config.enableCollapsing"></span>
<span class="caret caret-right" ng-show="fieldset.collapse && model.config.enableCollapsing"></span>
<label>
<i class="fieldsetIcon icon ng-class:fieldsetConfigModel.icon"></i>
<span ng-bind="getFieldsetTitle(fieldsetConfigModel, $index)"></span>
</label>
</div>
<div class="archetypeEditorControls label-sub" ng-hide="model.config.hideFieldsetControls">
<i class="icon icon-add dropdown-toggle" ng-show="canAdd()" data-toggle="{{ model.config.fieldsets.length > 1 ? 'dropdown' : ''}}" ng-click="model.config.fieldsets.length == 1 ? addRow(fieldset.alias, $index) : return"></i>
<ul class="dropdown-menu umb-actions">
<li ng-repeat="fieldsetModel in model.config.fieldsets" ng-click="addRow(fieldsetModel.alias, $parent.$index)">
<a>
<i class="fieldsetIcon icon ng-class:fieldsetModel.icon"></i>
<span class="menu-label">{{fieldsetModel.label}}</span>
</a>
</li>
</ul>
<i class="icon icon-remove" ng-click="removeRow($index)" ng-show="canRemove()"></i>
<i class="icon icon-navigation handle" ng-show="canSort()"></i>
</div>
</div>
<div class="archetypeCollapser animate-hide" ng-hide="isCollapsed(fieldset)">
<form class="form-inline">
<div ng-class="{archetypePropertyError: getPropertyValidity($parent.$index, property.alias) === false}" class="archetypeProperty control-group ng-class:property.alias" ng-repeat="property in fieldsetConfigModel.properties">
<label ng-hide="archetypeConfig.hidePropertyLabels == '1'" class="control-label" for="archetype-property-{{model.alias}}-{{$parent.$index}}-{{$index}}">
<span>{{property.label}}</span>
<div class="archetypeFieldsetHelpText" ng-show="property.helpText">
<small>{{property.helpText}}</small>
</div>
</label>
<div class="controls">
<archetype-property class="archetypeEditor" property="property" fieldset-index="$parent.$index" fieldset="fieldset" archetype-config="model.config" property-config-index="$index" archetype-render-model="model.value" umbraco-property-alias="model.alias" umbraco-form = "form"></archetype-property>
</div>
</div>
</form>
</div>
</fieldset>
</li>
</ul>
<div ng-show="showAddButton()">
<a data-toggle="{{ model.config.fieldsets.length > 1 ? 'dropdown' : ''}}" ng-click="model.config.fieldsets.length == 1 ? addRow(model.config.fieldsets[0].alias, 0) : return" prevent-default>
<i class="icon icon-add dropdown-toggle"></i>
<localize key="general_add" class="archetypeAddButtonText">Add</localize>
</a>
<ul class="dropdown-menu umb-actions">
<li ng-repeat="fieldsetModel in model.config.fieldsets" ng-click="addRow(fieldsetModel.alias, 0)">
<a>
<i class="fieldsetIcon icon ng-class:fieldsetModel.icon"></i>
<span class="menu-label">{{fieldsetModel.label}}</span>
</a>
</li>
</ul>
</div>
</div>
<div archetype-custom-view archetype="model"></div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Archetype",
"version": "1.0-beta",
"version": "1.2-beta",
"url": "http://github.com/imulus/archetype/",
"author": "Imulus",
"authorUrl": "http://imulus.com/",
+3
View File
@@ -27,5 +27,8 @@
"repository": {
"type": "git",
"url": "https://github.com/imulus/Archetype.git"
},
"devDependencies": {
"grunt-msbuild": "^0.1.12"
}
}