Implementing PackagingService and moving Import Export methods to this service to have it specialized and centralized around packaging operations.
This commit is contained in:
@@ -8,6 +8,17 @@ namespace Umbraco.Core.Models
|
||||
/// </summary>
|
||||
public class ContentTypeSort : IValueObject
|
||||
{
|
||||
public ContentTypeSort()
|
||||
{
|
||||
}
|
||||
|
||||
public ContentTypeSort(Lazy<int> id, int sortOrder, string @alias)
|
||||
{
|
||||
Id = id;
|
||||
SortOrder = sortOrder;
|
||||
Alias = alias;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Id of the ContentType
|
||||
/// </summary>
|
||||
|
||||
@@ -328,8 +328,8 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
|
||||
public ITemplate Get(string alias)
|
||||
{
|
||||
var sql = GetBaseQuery(false);
|
||||
sql.Where("cmsTemplate.alias = @Alias", new { Alias = alias });
|
||||
var sql = GetBaseQuery(false)
|
||||
.Where<TemplateDto>(x => x.Alias == alias);
|
||||
|
||||
var dto = Database.Fetch<TemplateDto, NodeDto>(sql).FirstOrDefault();
|
||||
|
||||
|
||||
@@ -1001,166 +1001,6 @@ namespace Umbraco.Core.Services
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IContent"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <returns>An enumrable list of generated content</returns>
|
||||
public IEnumerable<IContent> Import(XElement element)
|
||||
{
|
||||
var name = element.Name.LocalName;
|
||||
if (name.Equals("DocumentSet"))
|
||||
{
|
||||
//This is a regular deep-structured import
|
||||
var roots = from doc in element.Elements()
|
||||
where (string) doc.Attribute("isDoc") == ""
|
||||
select doc;
|
||||
|
||||
var contents = ParseRootXml(roots);
|
||||
Save(contents);
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
var attribute = element.Attribute("isDoc");
|
||||
if (attribute != null)
|
||||
{
|
||||
//This is a single doc import
|
||||
var elements = new List<XElement> { element };
|
||||
var contents = ParseRootXml(elements);
|
||||
Save(contents);
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
"The passed in XElement is not valid! It does not contain a root element called "+
|
||||
"'DocumentSet' (for structured imports) nor is the first element a Document (for single document import).");
|
||||
}
|
||||
|
||||
private IEnumerable<IContent> ParseRootXml(IEnumerable<XElement> roots)
|
||||
{
|
||||
var contentTypes = new Dictionary<string, IContentType>();
|
||||
var contents = new List<IContent>();
|
||||
foreach (var root in roots)
|
||||
{
|
||||
bool isLegacySchema = root.Name.LocalName.ToLowerInvariant().Equals("node");
|
||||
string contentTypeAlias = isLegacySchema
|
||||
? root.Attribute("nodeTypeAlias").Value
|
||||
: root.Name.LocalName;
|
||||
|
||||
if (contentTypes.ContainsKey(contentTypeAlias) == false)
|
||||
{
|
||||
var contentType = FindContentTypeByAlias(contentTypeAlias);
|
||||
contentTypes.Add(contentTypeAlias, contentType);
|
||||
}
|
||||
|
||||
var content = CreateContentFromXml(root, contentTypes[contentTypeAlias], null, -1, isLegacySchema);
|
||||
contents.Add(content);
|
||||
|
||||
var children = from child in root.Elements()
|
||||
where (string)child.Attribute("isDoc") == ""
|
||||
select child;
|
||||
if(children.Any())
|
||||
contents.AddRange(CreateContentFromXml(children, content, contentTypes, isLegacySchema));
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
private IEnumerable<IContent> CreateContentFromXml(IEnumerable<XElement> children, IContent parent, Dictionary<string, IContentType> contentTypes, bool isLegacySchema)
|
||||
{
|
||||
var list = new List<IContent>();
|
||||
foreach (var child in children)
|
||||
{
|
||||
string contentTypeAlias = isLegacySchema
|
||||
? child.Attribute("nodeTypeAlias").Value
|
||||
: child.Name.LocalName;
|
||||
|
||||
if (contentTypes.ContainsKey(contentTypeAlias) == false)
|
||||
{
|
||||
var contentType = FindContentTypeByAlias(contentTypeAlias);
|
||||
contentTypes.Add(contentTypeAlias, contentType);
|
||||
}
|
||||
|
||||
//Create and add the child to the list
|
||||
var content = CreateContentFromXml(child, contentTypes[contentTypeAlias], parent, default(int), isLegacySchema);
|
||||
list.Add(content);
|
||||
|
||||
//Recursive call
|
||||
XElement child1 = child;
|
||||
var grandChildren = from grand in child1.Elements()
|
||||
where (string) grand.Attribute("isDoc") == ""
|
||||
select grand;
|
||||
|
||||
if (grandChildren.Any())
|
||||
list.AddRange(CreateContentFromXml(grandChildren, content, contentTypes, isLegacySchema));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private IContent CreateContentFromXml(XElement element, IContentType contentType, IContent parent, int parentId, bool isLegacySchema)
|
||||
{
|
||||
var id = element.Attribute("id").Value;
|
||||
var level = element.Attribute("level").Value;
|
||||
var sortOrder = element.Attribute("sortOrder").Value;
|
||||
var nodeName = element.Attribute("nodeName").Value;
|
||||
var path = element.Attribute("path").Value;
|
||||
var template = element.Attribute("template").Value;
|
||||
|
||||
var properties = from property in element.Elements()
|
||||
where property.Attribute("isDoc") == null
|
||||
select property;
|
||||
|
||||
IContent content = parent == null
|
||||
? new Content(nodeName, parentId, contentType)
|
||||
{
|
||||
Level = int.Parse(level),
|
||||
SortOrder = int.Parse(sortOrder)
|
||||
}
|
||||
: new Content(nodeName, parent, contentType)
|
||||
{
|
||||
Level = int.Parse(level),
|
||||
SortOrder = int.Parse(sortOrder)
|
||||
};
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
string propertyTypeAlias = isLegacySchema ? property.Attribute("alias").Value : property.Name.LocalName;
|
||||
if(content.HasProperty(propertyTypeAlias))
|
||||
content.SetValue(propertyTypeAlias, property.Value);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private IContentType FindContentTypeByAlias(string contentTypeAlias)
|
||||
{
|
||||
using (var repository = _repositoryFactory.CreateContentTypeRepository(_uowProvider.GetUnitOfWork()))
|
||||
{
|
||||
var query = Query<IContentType>.Builder.Where(x => x.Alias == contentTypeAlias);
|
||||
var types = repository.GetByQuery(query);
|
||||
|
||||
if (!types.Any())
|
||||
throw new Exception(
|
||||
string.Format("No ContentType matching the passed in Alias: '{0}' was found",
|
||||
contentTypeAlias));
|
||||
|
||||
var contentType = types.First();
|
||||
|
||||
if (contentType == null)
|
||||
throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null",
|
||||
contentTypeAlias));
|
||||
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
|
||||
public XElement Export(IContent content, bool deep = false)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#region Internal Methods
|
||||
|
||||
/// <summary>
|
||||
@@ -1607,6 +1447,28 @@ namespace Umbraco.Core.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
private IContentType FindContentTypeByAlias(string contentTypeAlias)
|
||||
{
|
||||
using (var repository = _repositoryFactory.CreateContentTypeRepository(_uowProvider.GetUnitOfWork()))
|
||||
{
|
||||
var query = Query<IContentType>.Builder.Where(x => x.Alias == contentTypeAlias);
|
||||
var types = repository.GetByQuery(query);
|
||||
|
||||
if (!types.Any())
|
||||
throw new Exception(
|
||||
string.Format("No ContentType matching the passed in Alias: '{0}' was found",
|
||||
contentTypeAlias));
|
||||
|
||||
var contentType = types.First();
|
||||
|
||||
if (contentType == null)
|
||||
throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null",
|
||||
contentTypeAlias));
|
||||
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
@@ -474,61 +475,7 @@ namespace Umbraco.Core.Services
|
||||
}
|
||||
return dtd.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IContentType"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <returns>An enumrable list of generated ContentTypes</returns>
|
||||
public List<IContentType> Import(XElement element)
|
||||
{
|
||||
var name = element.Name.LocalName;
|
||||
if (name.Equals("DocumentTypes") == false)
|
||||
{
|
||||
throw new ArgumentException("The passed in XElement is not valid! It does not contain a root element called 'DocumentTypes'.");
|
||||
}
|
||||
|
||||
var list = new List<IContentType>();
|
||||
var documentTypes = from doc in element.Elements("DocumentType") select doc;
|
||||
foreach (var documentType in documentTypes)
|
||||
{
|
||||
//TODO Check if the ContentType already exists by looking up the alias
|
||||
list.Add(CreateContentTypeFromXml(documentType));
|
||||
}
|
||||
|
||||
Save(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private IContentType CreateContentTypeFromXml(XElement documentType)
|
||||
{
|
||||
var infoElement = documentType.Element("Info");
|
||||
var name = infoElement.Element("Name").Value;
|
||||
var alias = infoElement.Element("Alias").Value;
|
||||
var masterElement = infoElement.Element("Master");//Name of the master corresponds to the parent
|
||||
var icon = infoElement.Element("Icon").Value;
|
||||
var thumbnail = infoElement.Element("Thumbnail").Value;
|
||||
var description = infoElement.Element("Description").Value;
|
||||
var allowAtRoot = infoElement.Element("AllowAtRoot").Value;
|
||||
var defaultTemplate = infoElement.Element("DefaultTemplate").Value;
|
||||
var allowedTemplatesElement = infoElement.Elements("AllowedTemplates");
|
||||
|
||||
var structureElement = documentType.Element("Structure");
|
||||
var genericPropertiesElement = documentType.Element("GenericProperties");
|
||||
var tabElement = documentType.Element("Tab");
|
||||
|
||||
var contentType = new ContentType(-1)
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Icon = icon,
|
||||
Thumbnail = thumbnail,
|
||||
AllowedAsRoot = allowAtRoot.ToLowerInvariant().Equals("true"),
|
||||
Description = description
|
||||
};
|
||||
return contentType;
|
||||
}
|
||||
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Auditing;
|
||||
using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Models;
|
||||
@@ -19,6 +20,7 @@ namespace Umbraco.Core.Services
|
||||
{
|
||||
private readonly RepositoryFactory _repositoryFactory;
|
||||
private readonly IDatabaseUnitOfWorkProvider _uowProvider;
|
||||
private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
public DataTypeService()
|
||||
: this(new RepositoryFactory())
|
||||
@@ -121,18 +123,84 @@ namespace Umbraco.Core.Services
|
||||
{
|
||||
if (Saving.IsRaisedEventCancelled(new SaveEventArgs<IDataTypeDefinition>(dataTypeDefinition), this))
|
||||
return;
|
||||
|
||||
var uow = _uowProvider.GetUnitOfWork();
|
||||
using (var repository = _repositoryFactory.CreateDataTypeDefinitionRepository(uow))
|
||||
{
|
||||
dataTypeDefinition.CreatorId = userId;
|
||||
repository.AddOrUpdate(dataTypeDefinition);
|
||||
uow.Commit();
|
||||
|
||||
Saved.RaiseEvent(new SaveEventArgs<IDataTypeDefinition>(dataTypeDefinition, false), this);
|
||||
}
|
||||
using (new WriteLock(Locker))
|
||||
{
|
||||
var uow = _uowProvider.GetUnitOfWork();
|
||||
using (var repository = _repositoryFactory.CreateDataTypeDefinitionRepository(uow))
|
||||
{
|
||||
dataTypeDefinition.CreatorId = userId;
|
||||
repository.AddOrUpdate(dataTypeDefinition);
|
||||
uow.Commit();
|
||||
|
||||
Audit.Add(AuditTypes.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id);
|
||||
Saved.RaiseEvent(new SaveEventArgs<IDataTypeDefinition>(dataTypeDefinition, false), this);
|
||||
}
|
||||
}
|
||||
|
||||
Audit.Add(AuditTypes.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a collection of <see cref="IDataTypeDefinition"/>
|
||||
/// </summary>
|
||||
/// <param name="dataTypeDefinitions"><see cref="IDataTypeDefinition"/> to save</param>
|
||||
/// <param name="userId">Id of the user issueing the save</param>
|
||||
public void Save(IEnumerable<IDataTypeDefinition> dataTypeDefinitions, int userId = 0)
|
||||
{
|
||||
if (Saving.IsRaisedEventCancelled(new SaveEventArgs<IDataTypeDefinition>(dataTypeDefinitions), this))
|
||||
return;
|
||||
|
||||
using (new WriteLock(Locker))
|
||||
{
|
||||
var uow = _uowProvider.GetUnitOfWork();
|
||||
using (var repository = _repositoryFactory.CreateDataTypeDefinitionRepository(uow))
|
||||
{
|
||||
foreach (var dataTypeDefinition in dataTypeDefinitions)
|
||||
{
|
||||
dataTypeDefinition.CreatorId = userId;
|
||||
repository.AddOrUpdate(dataTypeDefinition);
|
||||
}
|
||||
uow.Commit();
|
||||
|
||||
Saved.RaiseEvent(new SaveEventArgs<IDataTypeDefinition>(dataTypeDefinitions, false), this);
|
||||
}
|
||||
}
|
||||
Audit.Add(AuditTypes.Save, string.Format("Save DataTypeDefinition performed by user"), userId, -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a list of PreValues for a given DataTypeDefinition
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the DataTypeDefinition to save PreValues for</param>
|
||||
/// <param name="values">List of string values to save</param>
|
||||
public void SavePreValues(int id, IEnumerable<string> values)
|
||||
{
|
||||
using (new WriteLock(Locker))
|
||||
{
|
||||
using (var uow = _uowProvider.GetUnitOfWork())
|
||||
{
|
||||
var sortOrderObj =
|
||||
uow.Database.ExecuteScalar<object>(
|
||||
"SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = id });
|
||||
int sortOrder;
|
||||
if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out sortOrder) == false)
|
||||
{
|
||||
sortOrder = 1;
|
||||
}
|
||||
|
||||
using (var transaction = uow.Database.GetTransaction())
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var dto = new DataTypePreValueDto { DataTypeNodeId = id, Value = value, SortOrder = sortOrder };
|
||||
uow.Database.Insert(dto);
|
||||
sortOrder++;
|
||||
}
|
||||
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -288,12 +288,5 @@ namespace Umbraco.Core.Services
|
||||
/// <param name="content"><see cref="IContent"/> to check if anscestors are published</param>
|
||||
/// <returns>True if the Content can be published, otherwise False</returns>
|
||||
bool IsPublishable(IContent content);
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IContent"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <returns>An enumrable list of generated content</returns>
|
||||
IEnumerable<IContent> Import(XElement element);
|
||||
}
|
||||
}
|
||||
@@ -150,12 +150,5 @@ namespace Umbraco.Core.Services
|
||||
/// <param name="id">Id of the <see cref="IMediaType"/></param>
|
||||
/// <returns>True if the media type has any children otherwise False</returns>
|
||||
bool MediaTypeHasChildren(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IContentType"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <returns>An enumrable list of generated ContentTypes</returns>
|
||||
List<IContentType> Import(XElement element);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,13 @@ namespace Umbraco.Core.Services
|
||||
/// <param name="userId">Id of the user issueing the save</param>
|
||||
void Save(IDataTypeDefinition dataTypeDefinition, int userId = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a collection of <see cref="IDataTypeDefinition"/>
|
||||
/// </summary>
|
||||
/// <param name="dataTypeDefinitions"><see cref="IDataTypeDefinition"/> to save</param>
|
||||
/// <param name="userId">Id of the user issueing the save</param>
|
||||
void Save(IEnumerable<IDataTypeDefinition> dataTypeDefinitions, int userId = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an <see cref="IDataTypeDefinition"/>
|
||||
/// </summary>
|
||||
@@ -75,5 +82,12 @@ namespace Umbraco.Core.Services
|
||||
/// <param name="id">Id of the <see cref="IDataTypeDefinition"/> to retrieve prevalues from</param>
|
||||
/// <returns>An enumerable list of string values</returns>
|
||||
IEnumerable<string> GetPreValuesByDataTypeId(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a list of PreValues for a given DataTypeDefinition
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the DataTypeDefinition to save PreValues for</param>
|
||||
/// <param name="values">List of string values to save</param>
|
||||
void SavePreValues(int id, IEnumerable<string> values);
|
||||
}
|
||||
}
|
||||
499
src/Umbraco.Core/Services/PackagingService.cs
Normal file
499
src/Umbraco.Core/Services/PackagingService.cs
Normal file
@@ -0,0 +1,499 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
using Umbraco.Core.Persistence.UnitOfWork;
|
||||
|
||||
namespace Umbraco.Core.Services
|
||||
{
|
||||
public class PackagingService : IService
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly RepositoryFactory _repositoryFactory;
|
||||
private readonly IDatabaseUnitOfWorkProvider _uowProvider;
|
||||
private Dictionary<string, IContentType> _importedContentTypes;
|
||||
//Support recursive locks because some of the methods that require locking call other methods that require locking.
|
||||
//for example, the Move method needs to be locked but this calls the Save method which also needs to be locked.
|
||||
private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
public PackagingService(IContentService contentService, IContentTypeService contentTypeService, IMediaService mediaService, IDataTypeService dataTypeService, IFileService fileService, RepositoryFactory repositoryFactory, IDatabaseUnitOfWorkProvider uowProvider)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_contentTypeService = contentTypeService;
|
||||
_mediaService = mediaService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_fileService = fileService;
|
||||
_repositoryFactory = repositoryFactory;
|
||||
_uowProvider = uowProvider;
|
||||
|
||||
_importedContentTypes = new Dictionary<string, IContentType>();
|
||||
}
|
||||
|
||||
#region Content
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IContent"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns>An enumrable list of generated content</returns>
|
||||
public IEnumerable<IContent> ImportContent(XElement element, int userId = 0)
|
||||
{
|
||||
var name = element.Name.LocalName;
|
||||
if (name.Equals("DocumentSet"))
|
||||
{
|
||||
//This is a regular deep-structured import
|
||||
var roots = from doc in element.Elements()
|
||||
where (string)doc.Attribute("isDoc") == ""
|
||||
select doc;
|
||||
|
||||
var contents = ParseDocumentRootXml(roots);
|
||||
_contentService.Save(contents, userId);
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
var attribute = element.Attribute("isDoc");
|
||||
if (attribute != null)
|
||||
{
|
||||
//This is a single doc import
|
||||
var elements = new List<XElement> { element };
|
||||
var contents = ParseDocumentRootXml(elements);
|
||||
_contentService.Save(contents, userId);
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
"The passed in XElement is not valid! It does not contain a root element called " +
|
||||
"'DocumentSet' (for structured imports) nor is the first element a Document (for single document import).");
|
||||
}
|
||||
|
||||
private IEnumerable<IContent> ParseDocumentRootXml(IEnumerable<XElement> roots)
|
||||
{
|
||||
var contents = new List<IContent>();
|
||||
foreach (var root in roots)
|
||||
{
|
||||
bool isLegacySchema = root.Name.LocalName.ToLowerInvariant().Equals("node");
|
||||
string contentTypeAlias = isLegacySchema
|
||||
? root.Attribute("nodeTypeAlias").Value
|
||||
: root.Name.LocalName;
|
||||
|
||||
if (_importedContentTypes.ContainsKey(contentTypeAlias) == false)
|
||||
{
|
||||
var contentType = FindContentTypeByAlias(contentTypeAlias);
|
||||
_importedContentTypes.Add(contentTypeAlias, contentType);
|
||||
}
|
||||
|
||||
var content = CreateContentFromXml(root, _importedContentTypes[contentTypeAlias], null, -1, isLegacySchema);
|
||||
contents.Add(content);
|
||||
|
||||
var children = from child in root.Elements()
|
||||
where (string)child.Attribute("isDoc") == ""
|
||||
select child;
|
||||
if (children.Any())
|
||||
contents.AddRange(CreateContentFromXml(children, content, isLegacySchema));
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
private IEnumerable<IContent> CreateContentFromXml(IEnumerable<XElement> children, IContent parent, bool isLegacySchema)
|
||||
{
|
||||
var list = new List<IContent>();
|
||||
foreach (var child in children)
|
||||
{
|
||||
string contentTypeAlias = isLegacySchema
|
||||
? child.Attribute("nodeTypeAlias").Value
|
||||
: child.Name.LocalName;
|
||||
|
||||
if (_importedContentTypes.ContainsKey(contentTypeAlias) == false)
|
||||
{
|
||||
var contentType = FindContentTypeByAlias(contentTypeAlias);
|
||||
_importedContentTypes.Add(contentTypeAlias, contentType);
|
||||
}
|
||||
|
||||
//Create and add the child to the list
|
||||
var content = CreateContentFromXml(child, _importedContentTypes[contentTypeAlias], parent, default(int), isLegacySchema);
|
||||
list.Add(content);
|
||||
|
||||
//Recursive call
|
||||
XElement child1 = child;
|
||||
var grandChildren = from grand in child1.Elements()
|
||||
where (string)grand.Attribute("isDoc") == ""
|
||||
select grand;
|
||||
|
||||
if (grandChildren.Any())
|
||||
list.AddRange(CreateContentFromXml(grandChildren, content, isLegacySchema));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private IContent CreateContentFromXml(XElement element, IContentType contentType, IContent parent, int parentId, bool isLegacySchema)
|
||||
{
|
||||
var id = element.Attribute("id").Value;
|
||||
var level = element.Attribute("level").Value;
|
||||
var sortOrder = element.Attribute("sortOrder").Value;
|
||||
var nodeName = element.Attribute("nodeName").Value;
|
||||
var path = element.Attribute("path").Value;
|
||||
var template = element.Attribute("template").Value;
|
||||
|
||||
var properties = from property in element.Elements()
|
||||
where property.Attribute("isDoc") == null
|
||||
select property;
|
||||
|
||||
IContent content = parent == null
|
||||
? new Content(nodeName, parentId, contentType)
|
||||
{
|
||||
Level = int.Parse(level),
|
||||
SortOrder = int.Parse(sortOrder)
|
||||
}
|
||||
: new Content(nodeName, parent, contentType)
|
||||
{
|
||||
Level = int.Parse(level),
|
||||
SortOrder = int.Parse(sortOrder)
|
||||
};
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
string propertyTypeAlias = isLegacySchema ? property.Attribute("alias").Value : property.Name.LocalName;
|
||||
if (content.HasProperty(propertyTypeAlias))
|
||||
content.SetValue(propertyTypeAlias, property.Value);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public XElement Export(IContent content, bool deep = false)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ContentTypes
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IContentType"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns>An enumrable list of generated ContentTypes</returns>
|
||||
public IEnumerable<IContentType> ImportContentTypes(XElement element, int userId = 0)
|
||||
{
|
||||
var name = element.Name.LocalName;
|
||||
if (name.Equals("DocumentTypes") == false)
|
||||
{
|
||||
throw new ArgumentException("The passed in XElement is not valid! It does not contain a root element called 'DocumentTypes'.");
|
||||
}
|
||||
|
||||
_importedContentTypes = new Dictionary<string, IContentType>();
|
||||
var documentTypes = (from doc in element.Elements("DocumentType") select doc).ToList();
|
||||
//NOTE it might be an idea to sort the doctype XElements based on dependencies
|
||||
//before creating the doc types - should also allow for a better structure/inheritance support.
|
||||
foreach (var documentType in documentTypes)
|
||||
{
|
||||
var alias = documentType.Element("Info").Element("Alias").Value;
|
||||
if (_importedContentTypes.ContainsKey(alias) == false)
|
||||
{
|
||||
var contentType = _contentTypeService.GetContentType(alias);
|
||||
_importedContentTypes.Add(alias, contentType == null
|
||||
? CreateContentTypeFromXml(documentType)
|
||||
: UpdateContentTypeFromXml(documentType, contentType));
|
||||
}
|
||||
}
|
||||
|
||||
var list = _importedContentTypes.Select(x => x.Value).ToList();
|
||||
_contentTypeService.Save(list, userId);
|
||||
|
||||
var updatedContentTypes = new List<IContentType>();
|
||||
//Update the structure here - we can't do it untill all DocTypes have been created
|
||||
foreach (var documentType in documentTypes)
|
||||
{
|
||||
var alias = documentType.Element("Info").Element("Alias").Value;
|
||||
var structureElement = documentType.Element("Structure");
|
||||
//Ensure that we only update ContentTypes which has actual structure-elements
|
||||
if (structureElement == null || structureElement.Elements("DocumentType").Any() == false) continue;
|
||||
|
||||
var updated = UpdateContentTypesStructure(_importedContentTypes[alias], structureElement);
|
||||
updatedContentTypes.Add(updated);
|
||||
}
|
||||
//Update ContentTypes with a newly added structure/list of allowed children
|
||||
if(updatedContentTypes.Any())
|
||||
_contentTypeService.Save(updatedContentTypes, userId);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private IContentType CreateContentTypeFromXml(XElement documentType)
|
||||
{
|
||||
var infoElement = documentType.Element("Info");
|
||||
|
||||
//Name of the master corresponds to the parent
|
||||
var masterElement = infoElement.Element("Master");
|
||||
IContentType parent = null;
|
||||
if (masterElement != null)
|
||||
{
|
||||
var masterAlias = masterElement.Value;
|
||||
parent = _importedContentTypes.ContainsKey(masterAlias)
|
||||
? _importedContentTypes[masterAlias]
|
||||
: _contentTypeService.GetContentType(masterAlias);
|
||||
}
|
||||
|
||||
var contentType = parent == null
|
||||
? new ContentType(-1)
|
||||
{
|
||||
Alias = infoElement.Element("Alias").Value
|
||||
}
|
||||
: new ContentType(parent)
|
||||
{
|
||||
Alias = infoElement.Element("Alias").Value
|
||||
};
|
||||
|
||||
return UpdateContentTypeFromXml(documentType, contentType);
|
||||
}
|
||||
|
||||
private IContentType UpdateContentTypeFromXml(XElement documentType, IContentType contentType)
|
||||
{
|
||||
var infoElement = documentType.Element("Info");
|
||||
var defaultTemplateElement = infoElement.Element("DefaultTemplate");
|
||||
|
||||
contentType.Name = infoElement.Element("Name").Value;
|
||||
contentType.Icon = infoElement.Element("Icon").Value;
|
||||
contentType.Thumbnail = infoElement.Element("Thumbnail").Value;
|
||||
contentType.Description = infoElement.Element("Description").Value;
|
||||
contentType.AllowedAsRoot = infoElement.Element("AllowAtRoot").Value.ToLowerInvariant().Equals("true");
|
||||
|
||||
UpdateContentTypesAllowedTemplates(contentType, infoElement.Element("AllowedTemplates"), defaultTemplateElement);
|
||||
UpdateContentTypesTabs(contentType, documentType.Element("Tab"));
|
||||
UpdateContentTypesProperties(contentType, documentType.Element("GenericProperties"));
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
private void UpdateContentTypesAllowedTemplates(IContentType contentType,
|
||||
XElement allowedTemplatesElement, XElement defaultTemplateElement)
|
||||
{
|
||||
if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any())
|
||||
{
|
||||
var allowedTemplates = contentType.AllowedTemplates.ToList();
|
||||
foreach (var templateElement in allowedTemplatesElement.Elements("Template"))
|
||||
{
|
||||
var alias = templateElement.Value;
|
||||
var template = _fileService.GetTemplate(alias);
|
||||
if (template != null)
|
||||
{
|
||||
allowedTemplates.Add(template);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.Warn<PackagingService>(
|
||||
string.Format(
|
||||
"Packager: Error handling allowed templates. Template with alias '{0}' could not be found.",
|
||||
alias));
|
||||
}
|
||||
}
|
||||
|
||||
contentType.AllowedTemplates = allowedTemplates;
|
||||
}
|
||||
|
||||
var defaultTemplate = _fileService.GetTemplate(defaultTemplateElement.Value);
|
||||
if (defaultTemplate != null)
|
||||
{
|
||||
contentType.SetDefaultTemplate(defaultTemplate);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.Warn<PackagingService>(
|
||||
string.Format(
|
||||
"Packager: Error handling default template. Default template with alias '{0}' could not be found.",
|
||||
defaultTemplateElement.Value));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateContentTypesTabs(IContentType contentType, XElement tabElement)
|
||||
{
|
||||
if(tabElement == null)
|
||||
return;
|
||||
|
||||
var tabs = tabElement.Elements("Tab");
|
||||
foreach (var tab in tabs)
|
||||
{
|
||||
var id = tab.Element("Id").Value;//Do we need to use this for tracking?
|
||||
var caption = tab.Element("Caption").Value;
|
||||
if (contentType.PropertyGroups.Contains(caption) == false)
|
||||
{
|
||||
contentType.AddPropertyGroup(caption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateContentTypesProperties(IContentType contentType, XElement genericPropertiesElement)
|
||||
{
|
||||
var properties = genericPropertiesElement.Elements("GenericProperty");
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var dataTypeId = new Guid(property.Element("Type").Value);//The DataType's Control Id
|
||||
var dataTypeDefinitionId = new Guid(property.Element("Definition").Value);//Unique Id for a DataTypeDefinition
|
||||
|
||||
var dataTypeDefinition = _dataTypeService.GetDataTypeDefinitionById(dataTypeDefinitionId);
|
||||
//If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id
|
||||
//We look up a DataTypeDefinition that matches
|
||||
if (dataTypeDefinition == null || dataTypeDefinition.ControlId != dataTypeId)
|
||||
{
|
||||
var dataTypeDefinitions = _dataTypeService.GetDataTypeDefinitionByControlId(dataTypeId);
|
||||
if (dataTypeDefinitions != null && dataTypeDefinitions.Any())
|
||||
{
|
||||
dataTypeDefinition = dataTypeDefinitions.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(
|
||||
String.Format(
|
||||
"Packager: Error handling creation of PropertyType '{0}'. Could not find DataTypeDefintion with unique id '{1}' nor one referencing the DataType with control id '{2}'.",
|
||||
property.Element("Name").Value, dataTypeDefinitionId, dataTypeId));
|
||||
}
|
||||
}
|
||||
|
||||
var propertyType = new PropertyType(dataTypeDefinition)
|
||||
{
|
||||
Alias = property.Element("Alias").Value,
|
||||
Name = property.Element("Name").Value,
|
||||
Description = property.Element("Description").Value,
|
||||
Mandatory = property.Element("Mandatory").Value.ToLowerInvariant().Equals("true"),
|
||||
ValidationRegExp = property.Element("Validation").Value
|
||||
};
|
||||
|
||||
var helpTextElement = property.Element("HelpText");
|
||||
if (helpTextElement != null)
|
||||
{
|
||||
propertyType.HelpText = helpTextElement.Value;
|
||||
}
|
||||
|
||||
var tab = property.Element("Tab").Value;
|
||||
if (string.IsNullOrEmpty(tab))
|
||||
{
|
||||
contentType.AddPropertyType(propertyType);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentType.AddPropertyType(propertyType, tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IContentType UpdateContentTypesStructure(IContentType contentType, XElement structureElement)
|
||||
{
|
||||
var allowedChildren = contentType.AllowedContentTypes.ToList();
|
||||
int sortOrder = allowedChildren.Any() ? allowedChildren.Last().SortOrder : 0;
|
||||
foreach (var element in structureElement.Elements("DocumentType"))
|
||||
{
|
||||
var alias = element.Value;
|
||||
if (_importedContentTypes.ContainsKey(alias))
|
||||
{
|
||||
var allowedChild = _importedContentTypes[alias];
|
||||
allowedChildren.Add(new ContentTypeSort(new Lazy<int>(() => allowedChild.Id), sortOrder, allowedChild.Alias));
|
||||
sortOrder++;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.Warn<PackagingService>(
|
||||
string.Format(
|
||||
"Packager: Error handling DocumentType structure. DocumentType with alias '{0}' could not be found and was not added to the structure for '{1}'.",
|
||||
alias, contentType.Alias));
|
||||
}
|
||||
}
|
||||
|
||||
contentType.AllowedContentTypes = allowedChildren;
|
||||
return contentType;
|
||||
}
|
||||
|
||||
private IContentType FindContentTypeByAlias(string contentTypeAlias)
|
||||
{
|
||||
using (var repository = _repositoryFactory.CreateContentTypeRepository(_uowProvider.GetUnitOfWork()))
|
||||
{
|
||||
var query = Query<IContentType>.Builder.Where(x => x.Alias == contentTypeAlias);
|
||||
var types = repository.GetByQuery(query);
|
||||
|
||||
if (!types.Any())
|
||||
throw new Exception(
|
||||
string.Format("No ContentType matching the passed in Alias: '{0}' was found",
|
||||
contentTypeAlias));
|
||||
|
||||
var contentType = types.First();
|
||||
|
||||
if (contentType == null)
|
||||
throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null",
|
||||
contentTypeAlias));
|
||||
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DataTypes
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="IDataTypeDefinition"/>
|
||||
/// </summary>
|
||||
/// <param name="element">Xml to import</param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns>An enumrable list of generated DataTypeDefinitions</returns>
|
||||
public IEnumerable<IDataTypeDefinition> ImportDataTypeDefinitions(XElement element, int userId = 0)
|
||||
{
|
||||
var name = element.Name.LocalName;
|
||||
if (name.Equals("DataTypes") == false)
|
||||
{
|
||||
throw new ArgumentException("The passed in XElement is not valid! It does not contain a root element called 'DataTypes'.");
|
||||
}
|
||||
|
||||
var dataTypes = new Dictionary<string, IDataTypeDefinition>();
|
||||
var dataTypeElements = element.Elements("DataType").ToList();
|
||||
foreach (var dataTypeElement in dataTypeElements)
|
||||
{
|
||||
var dataTypeDefinitionName = dataTypeElement.Attribute("Name").Value;
|
||||
var dataTypeId = new Guid(dataTypeElement.Attribute("Id").Value);
|
||||
var dataTypeDefinitionId = new Guid(dataTypeElement.Attribute("Definition").Value);
|
||||
|
||||
var definition = _dataTypeService.GetDataTypeDefinitionById(dataTypeDefinitionId);
|
||||
if (definition == null)
|
||||
{
|
||||
var dataTypeDefinition = new DataTypeDefinition(-1, dataTypeId) { Key = dataTypeDefinitionId, Name = dataTypeDefinitionName };
|
||||
dataTypes.Add(dataTypeDefinitionName, dataTypeDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
var list = dataTypes.Select(x => x.Value).ToList();
|
||||
_dataTypeService.Save(list, userId);
|
||||
|
||||
SavePrevaluesFromXml(list, dataTypeElements);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void SavePrevaluesFromXml(List<IDataTypeDefinition> dataTypes, IEnumerable<XElement> dataTypeElements)
|
||||
{
|
||||
foreach (var dataTypeElement in dataTypeElements)
|
||||
{
|
||||
var prevaluesElement = dataTypeElement.Element("PreValues");
|
||||
if (prevaluesElement == null) continue;
|
||||
|
||||
var dataTypeDefinitionName = dataTypeElement.Attribute("Name").Value;
|
||||
var dataTypeDefinition = dataTypes.First(x => x.Name == dataTypeDefinitionName);
|
||||
|
||||
var values = prevaluesElement.Elements("PreValue").Select(prevalue => prevalue.Attribute("Value").Value).ToList();
|
||||
_dataTypeService.SavePreValues(dataTypeDefinition.Id, values);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace Umbraco.Core.Services
|
||||
private Lazy<DataTypeService> _dataTypeService;
|
||||
private Lazy<FileService> _fileService;
|
||||
private Lazy<LocalizationService> _localizationService;
|
||||
private Lazy<PackagingService> _packagingService;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
@@ -70,6 +71,9 @@ namespace Umbraco.Core.Services
|
||||
|
||||
if(_localizationService == null)
|
||||
_localizationService = new Lazy<LocalizationService>(() => new LocalizationService(provider, repositoryFactory.Value));
|
||||
|
||||
if(_packagingService == null)
|
||||
_packagingService = new Lazy<PackagingService>(() => new PackagingService(_contentService.Value, _contentTypeService.Value, _mediaService.Value, _dataTypeService.Value, _fileService.Value, repositoryFactory.Value, provider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,6 +124,14 @@ namespace Umbraco.Core.Services
|
||||
get { return _mediaService.Value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="PackagingService"/>
|
||||
/// </summary>
|
||||
public PackagingService PackagingService
|
||||
{
|
||||
get { return _packagingService.Value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IMacroService"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -654,6 +654,7 @@
|
||||
<Compile Include="Services\LocalizationService.cs" />
|
||||
<Compile Include="Services\MacroService.cs" />
|
||||
<Compile Include="Services\MediaService.cs" />
|
||||
<Compile Include="Services\PackagingService.cs" />
|
||||
<Compile Include="Services\ServiceContext.cs" />
|
||||
<Compile Include="Services\UserService.cs" />
|
||||
<Compile Include="TypeExtensions.cs" />
|
||||
|
||||
@@ -26,16 +26,21 @@ namespace Umbraco.Tests.Services.Importing
|
||||
string strXml = ImportResources.package;
|
||||
var xml = XElement.Parse(strXml);
|
||||
var element = xml.Descendants("DocumentTypes").First();
|
||||
var contentTypeService = ServiceContext.ContentTypeService;
|
||||
var dataTypeElement = xml.Descendants("DataTypes").First();
|
||||
var packagingService = ServiceContext.PackagingService;
|
||||
|
||||
// Act
|
||||
var contentTypes = contentTypeService.Import(element);
|
||||
var dataTypeDefinitions = packagingService.ImportDataTypeDefinitions(dataTypeElement);
|
||||
var contentTypes = packagingService.ImportContentTypes(element);
|
||||
var numberOfDocTypes = (from doc in element.Elements("DocumentType") select doc).Count();
|
||||
|
||||
// Assert
|
||||
Assert.That(dataTypeDefinitions, Is.Not.Null);
|
||||
Assert.That(dataTypeDefinitions.Any(), Is.True);
|
||||
Assert.That(contentTypes, Is.Not.Null);
|
||||
Assert.That(contentTypes.Any(), Is.True);
|
||||
Assert.That(contentTypes.Count(), Is.EqualTo(numberOfDocTypes));
|
||||
Assert.That(contentTypes.Count(x => x.ParentId == -1), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -44,20 +49,22 @@ namespace Umbraco.Tests.Services.Importing
|
||||
// Arrange
|
||||
string strXml = ImportResources.package;
|
||||
var xml = XElement.Parse(strXml);
|
||||
var dataTypeElement = xml.Descendants("DataTypes").First();
|
||||
var docTypesElement = xml.Descendants("DocumentTypes").First();
|
||||
var element = xml.Descendants("DocumentSet").First();
|
||||
var contentService = ServiceContext.ContentService;
|
||||
var contentTypeService = ServiceContext.ContentTypeService;
|
||||
var packagingService = ServiceContext.PackagingService;
|
||||
|
||||
// Act
|
||||
var contentTypes = contentTypeService.Import(docTypesElement);
|
||||
var contents = contentService.Import(element);
|
||||
var dataTypeDefinitions = packagingService.ImportDataTypeDefinitions(dataTypeElement);
|
||||
var contentTypes = packagingService.ImportContentTypes(docTypesElement);
|
||||
var contents = packagingService.ImportContent(element);
|
||||
var numberOfDocs = (from doc in element.Descendants()
|
||||
where (string) doc.Attribute("isDoc") == ""
|
||||
select doc).Count();
|
||||
|
||||
// Assert
|
||||
Assert.That(contents, Is.Not.Null);
|
||||
Assert.That(dataTypeDefinitions.Any(), Is.True);
|
||||
Assert.That(contentTypes.Any(), Is.True);
|
||||
Assert.That(contents.Any(), Is.True);
|
||||
Assert.That(contents.Count(), Is.EqualTo(numberOfDocs));
|
||||
|
||||
@@ -424,7 +424,9 @@
|
||||
<Content Include="Migrations\SqlScripts\SqlCe-SchemaAndData-4110.sql" />
|
||||
<Content Include="Migrations\SqlScripts\SqlCeTotal-480.sql" />
|
||||
<Content Include="Migrations\SqlScripts\SqlServerTotal-480.sql" />
|
||||
<Content Include="Services\Importing\package.xml" />
|
||||
<Content Include="Services\Importing\package.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Content Include="TestHelpers\ExamineHelpers\media.xml" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
||||
@@ -762,7 +762,7 @@ namespace umbraco.cms.businesslogic.packager
|
||||
tabNames += tabs[t].Caption + ";";
|
||||
|
||||
|
||||
|
||||
//So the Tab is added to the DocumentType and then to this Hashtable, but its never used anywhere - WHY?
|
||||
Hashtable ht = new Hashtable();
|
||||
foreach (XmlNode t in n.SelectNodes("Tabs/Tab"))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user