/// <summary> /// Resolves the file name pattern for an export profile. /// </summary> /// <param name="profile">Export profile.</param> /// <param name="store">Store.</param> /// <param name="fileIndex">One based file index.</param> /// <param name="maxFileNameLength">The maximum length of the file name.</param> /// <returns>Resolved file name pattern.</returns> public static string ResolveFileNamePattern(this ExportProfile profile, Store store, int fileIndex, int maxFileNameLength) { using var psb = StringBuilderPool.Instance.Get(out var sb); sb.Append(profile.FileNamePattern); sb.Replace("%Profile.Id%", profile.Id.ToString()); sb.Replace("%Profile.FolderName%", profile.FolderName); sb.Replace("%Store.Id%", store.Id.ToString()); sb.Replace("%File.Index%", fileIndex.ToString("D4")); if (profile.FileNamePattern.Contains("%Profile.SeoName%")) { sb.Replace("%Profile.SeoName%", SeoHelper.BuildSlug(profile.Name, true, false, false).Replace("-", "")); } if (profile.FileNamePattern.Contains("%Store.SeoName%")) { sb.Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.BuildSlug(store.Name, true, false, true) : "allstores"); } if (profile.FileNamePattern.Contains("%Random.Number%")) { sb.Replace("%Random.Number%", CommonHelper.GenerateRandomInteger().ToString()); } if (profile.FileNamePattern.Contains("%Timestamp%")) { sb.Replace("%Timestamp%", DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture)); } var result = sb.ToString() .ToValidFileName(string.Empty) .Truncate(maxFileNameLength); return(result); }
public virtual async Task <ExportProfile> InsertExportProfileAsync( string providerSystemName, string name, string fileExtension, ExportFeatures features, bool isSystemProfile = false, string profileSystemName = null, int cloneFromProfileId = 0) { Guard.NotEmpty(providerSystemName, nameof(providerSystemName)); if (name.IsEmpty()) { name = providerSystemName; } if (!isSystemProfile) { var profileCount = await _db.ExportProfiles.CountAsync(x => x.ProviderSystemName == providerSystemName); name = $"{T("Common.My").Value} {name} {profileCount + 1}"; } TaskDescriptor task = null; ExportProfile cloneProfile = null; ExportProfile profile = null; if (cloneFromProfileId != 0) { cloneProfile = await _db.ExportProfiles .Include(x => x.Task) .Include(x => x.Deployments) .FindByIdAsync(cloneFromProfileId); } if (cloneProfile == null) { task = new TaskDescriptor { CronExpression = "0 */6 * * *", // Every six hours. Type = nameof(DataExportTask), Enabled = false, StopOnError = false, IsHidden = true }; } else { task = cloneProfile.Task.Clone(); } task.Name = name + " Task"; _db.TaskDescriptors.Add(task); // Get the task ID. await _db.SaveChangesAsync(); if (cloneProfile == null) { profile = new ExportProfile { FileNamePattern = _defaultFileNamePattern }; if (isSystemProfile) { profile.Enabled = true; profile.PerStore = false; profile.CreateZipArchive = false; profile.Cleanup = false; } else { // What we do here is to preset typical settings for feed creation // but on the other hand they may be untypical for generic data export\exchange. var projection = new ExportProjection { RemoveCriticalCharacters = true, CriticalCharacters = "¼,½,¾", PriceType = PriceDisplayType.PreSelectedPrice, NoGroupedProducts = features.HasFlag(ExportFeatures.CanOmitGroupedProducts), OnlyIndividuallyVisibleAssociated = true, DescriptionMerging = ExportDescriptionMerging.Description }; var filter = new ExportFilter { IsPublished = true, ShoppingCartTypeId = (int)ShoppingCartType.ShoppingCart }; using var writer1 = new StringWriter(); new XmlSerializer(typeof(ExportProjection)).Serialize(writer1, projection); profile.Projection = writer1.ToString(); using var writer2 = new StringWriter(); new XmlSerializer(typeof(ExportFilter)).Serialize(writer2, projection); profile.Filtering = writer2.ToString(); } } else { profile = cloneProfile.Clone(); } profile.IsSystemProfile = isSystemProfile; profile.Name = name; profile.ProviderSystemName = providerSystemName; profile.TaskId = task.Id; var cleanedSystemName = providerSystemName .Replace("Exports.", string.Empty) .Replace("Feeds.", string.Empty) .Replace("/", string.Empty) .Replace("-", string.Empty); var directoryName = SeoHelper.BuildSlug(cleanedSystemName, true, false, false) .ToValidPath() .Truncate(_dataExchangeSettings.MaxFileNameLength); // TODO: (mg) (core) find out how to correctly build new app relative paths for ExportProfile.FolderName like x '~/App_Data/Tenants/Default/ExportProfiles/bmecatproductxml-1-2' // ExportProfile.FolderName must fulfil PathHelper.IsSafeAppRootPath! See also below. var tenantRoot = DataSettings.Instance.TenantRoot; var profileDirectory = tenantRoot.GetDirectory("ExportProfiles"); // TODO: (mg) (core) TenantRoot is rooted to the tenant dir and DOES NOT CONTAIN "App_Data/Tenants/{TenantName}" anymore. // The result will be "ExportProfiles/{UniqueDirName}, which conflicts with how we saved the path in the past ("~/App_Data/..."). You must // "upgrade" legacy pathes to new root whenever an entity is loaded and processed. profile.FolderName = tenantRoot.PathCombine(profileDirectory.SubPath, tenantRoot.CreateUniqueDirectoryName(profileDirectory.SubPath, directoryName)); profile.SystemName = profileSystemName.IsEmpty() && isSystemProfile ? cleanedSystemName : profileSystemName; _db.ExportProfiles.Add(profile); // Get the export profile ID. await _db.SaveChangesAsync(); task.Alias = profile.Id.ToString(); if (fileExtension.HasValue() && !isSystemProfile) { if (cloneProfile == null) { if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) { var subFolder = tenantRoot.CreateUniqueDirectoryName(DataExporter.PublicFolder, directoryName); profile.Deployments.Add(new ExportDeployment { ProfileId = profile.Id, Enabled = true, DeploymentType = ExportDeploymentType.PublicFolder, Name = profile.Name, SubFolder = subFolder }); } } else { cloneProfile.Deployments.Each(x => profile.Deployments.Add(x.Clone())); } } // Finally update task and export profile. await _db.SaveChangesAsync(); return(profile); }
/// <summary> /// Creates an expando object with all product properties. /// This method is used when exporting products and when exporting variant combinations as products. /// </summary> private async Task <dynamic> ToDynamic(Product product, bool isParent, DataExporterContext ctx, DynamicProductContext productContext) { var combination = productContext.Combination; product.MergeWithCombination(combination); var productManufacturers = await ctx.ProductBatchContext.ProductManufacturers.GetOrLoadAsync(product.Id); var productCategories = await ctx.ProductBatchContext.ProductCategories.GetOrLoadAsync(product.Id); var productAttributes = await ctx.ProductBatchContext.Attributes.GetOrLoadAsync(product.Id); var productTags = await ctx.ProductBatchContext.ProductTags.GetOrLoadAsync(product.Id); var specificationAttributes = await ctx.ProductBatchContext.SpecificationAttributes.GetOrLoadAsync(product.Id); var selectedAttributes = combination?.AttributeSelection; var variantAttributeValues = combination?.AttributeSelection?.MaterializeProductVariantAttributeValues(productAttributes); // Price calculation. var calculationContext = new PriceCalculationContext(product, ctx.PriceCalculationOptions); calculationContext.AddSelectedAttributes(combination?.AttributeSelection, product.Id); var price = await _priceCalculationService.CalculatePriceAsync(calculationContext); dynamic dynObject = ToDynamic(product, ctx, productContext.SeName, price.FinalPrice); dynObject._IsParent = isParent; dynObject._CategoryName = null; dynObject._CategoryPath = null; dynObject._AttributeCombinationValues = null; dynObject._AttributeCombinationId = 0; dynObject.Price = price.FinalPrice.Amount; if (combination != null) { dynObject._AttributeCombinationId = combination.Id; dynObject._UniqueId = product.Id + "-" + combination.Id; if (ctx.Supports(ExportFeatures.UsesAttributeCombination)) { dynObject._AttributeCombinationValues = variantAttributeValues; } if (ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) { var valueNames = variantAttributeValues .Select(x => ctx.GetTranslation(x, nameof(x.Name), x.Name)) .ToList(); dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); } } else { dynObject._UniqueId = product.Id.ToString(); } if (selectedAttributes?.AttributesMap?.Any() ?? false) { var query = new ProductVariantQuery(); await _productUrlHelper.AddAttributesToQueryAsync(query, selectedAttributes, product.Id, 0, productAttributes); dynObject._DetailUrl = productContext.AbsoluteProductUrl + _productUrlHelper.ToQueryString(query); } else { dynObject._DetailUrl = productContext.AbsoluteProductUrl; } // Category path. { var categoryPath = string.Empty; var pc = productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault(); if (pc != null) { var node = await _categoryService.GetCategoryTreeAsync(pc.CategoryId, true, ctx.Store.Id); if (node != null) { categoryPath = _categoryService.GetCategoryPath(node, ctx.Projection.LanguageId, null, " > "); } } dynObject._CategoryPath = categoryPath; } dynObject.CountryOfOrigin = ctx.Countries.TryGetValue(product.CountryOfOriginId ?? 0, out var countryOfOrigin) ? ToDynamic(countryOfOrigin, ctx) : null; dynObject.ProductManufacturers = productManufacturers .OrderBy(x => x.DisplayOrder) .Select(x => { dynamic dyn = new DynamicEntity(x); dyn.Manufacturer = ToDynamic(x.Manufacturer, ctx); dyn.Manufacturer.Picture = x.Manufacturer != null && x.Manufacturer.MediaFileId.HasValue ? ToDynamic(x.Manufacturer.MediaFile, _mediaSettings.ManufacturerThumbPictureSize, _mediaSettings.ManufacturerThumbPictureSize, ctx) : null; return(dyn); }) .ToList(); dynObject.ProductCategories = productCategories .OrderBy(x => x.DisplayOrder) .Select(x => { dynamic dyn = new DynamicEntity(x); dyn.Category = ToDynamic(x.Category, ctx); dyn.Category.Picture = x.Category != null && x.Category.MediaFileId.HasValue ? ToDynamic(x.Category.MediaFile, _mediaSettings.CategoryThumbPictureSize, _mediaSettings.CategoryThumbPictureSize, ctx) : null; if (dynObject._CategoryName == null) { dynObject._CategoryName = (string)dyn.Category.Name; } return(dyn); }) .ToList(); dynObject.ProductAttributes = productAttributes .OrderBy(x => x.DisplayOrder) .Select(x => ToDynamic(x, ctx)) .ToList(); // Do not export combinations if a combination is exported as a product. if (productContext.Combinations != null && productContext.Combination == null) { var pictureSize = ctx.Projection.PictureSize > 0 ? ctx.Projection.PictureSize : _mediaSettings.ProductDetailsPictureSize; var productMediaFiles = (await ctx.ProductBatchContext.ProductMediaFiles.GetOrLoadAsync(product.Id)) .ToDictionarySafe(x => x.MediaFileId, x => x); dynObject.ProductAttributeCombinations = productContext.Combinations .Select(x => { dynamic dyn = DataExporter.ToDynamic(x, ctx); var assignedFiles = new List <dynamic>(); foreach (var fileId in x.GetAssignedMediaIds().Take(ctx.Projection.NumberOfMediaFiles ?? int.MaxValue)) { if (productMediaFiles.TryGetValue(fileId, out var assignedFile) && assignedFile.MediaFile != null) { assignedFiles.Add(ToDynamic(assignedFile.MediaFile, _mediaSettings.ProductThumbPictureSize, pictureSize, ctx)); } } dyn.Pictures = assignedFiles; return(dyn); }) .ToList(); } else { dynObject.ProductAttributeCombinations = Enumerable.Empty <ProductVariantAttributeCombination>(); } if (product.HasTierPrices) { var tierPrices = await ctx.ProductBatchContext.TierPrices.GetOrLoadAsync(product.Id); dynObject.TierPrices = tierPrices .RemoveDuplicatedQuantities() .Select(x => { dynamic dyn = new DynamicEntity(x); return(dyn); }) .ToList(); } if (product.HasDiscountsApplied) { var appliedDiscounts = await ctx.ProductBatchContext.AppliedDiscounts.GetOrLoadAsync(product.Id); dynObject.AppliedDiscounts = appliedDiscounts .Select(x => CreateDynamic(x)) .ToList(); } if (product.IsDownload) { var downloads = await ctx.ProductBatchContext.Downloads.GetOrLoadAsync(product.Id); dynObject.Downloads = downloads .Select(x => CreateDynamic(x)) .ToList(); } dynObject.ProductTags = productTags .Select(x => { var localizedName = ctx.GetTranslation(x, nameof(x.Name), x.Name); dynamic dyn = new DynamicEntity(x); dyn.Name = localizedName; dyn.SeName = SeoHelper.BuildSlug(localizedName, _seoSettings); dyn._Localized = GetLocalized(ctx, x, y => y.Name); return(dyn); }) .ToList(); dynObject.ProductSpecificationAttributes = specificationAttributes .Select(x => ToDynamic(x, ctx)) .ToList(); if (product.ProductType == ProductType.BundledProduct) { var bundleItems = await ctx.ProductBatchContext.ProductBundleItems.GetOrLoadAsync(product.Id); dynObject.ProductBundleItems = bundleItems .Select(x => { dynamic dyn = new DynamicEntity(x); dyn.Name = ctx.GetTranslation(x, nameof(x.Name), x.Name); dyn.ShortDescription = ctx.GetTranslation(x, nameof(x.ShortDescription), x.ShortDescription); dyn._Localized = GetLocalized(ctx, x, y => y.Name, y => y.ShortDescription); return(dyn); }) .ToList(); } var mediaFiles = await ApplyMediaFiles(dynObject, product, ctx, productContext); await ApplyExportFeatures(dynObject, product, price, mediaFiles, ctx, productContext); return(dynObject); }