From fef5b44519ab16961c0167bd23f44c4959bd9fa3 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Fri, 9 Nov 2018 11:07:51 +0100 Subject: [PATCH 001/657] Updated changelog --- changelog.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 916d3f7f28..db52cc3d08 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# Release Notes +# Release Notes ## SmartStore.NET 3.2 @@ -42,7 +42,7 @@ * Viveum: Supports payment via "Virtual Account Brands" (e.g. PayPal). * Added options for alternating price display (in badges). * #1515 Poll: Add result tab with a list of answers and customers for a poll -* Added export and import of product tags. +* BMEcat: Added export and import of product tags. ### Improvements * (Perf) Significantly increased query performance for products with a lot of category assignments (> 10). @@ -55,6 +55,7 @@ * Refactored download section * Enhanced EntityPicker to pick from customers, manufacturers & categories * #1510 Breadcrumb of an associated product should include the grouped product if it has no assigned categories. +* OpenTrans: added customer number to parties ### Bugfixes * In a multi-store environment, multiple topics with the same system name cannot be resolved reliably. @@ -90,6 +91,7 @@ * Fixed redirection of bots when several languages were active * Region cannot be selected in checkout when entering a billing or shipping address * Fixed invalid conversion of "System.Int32" to "SmartStore.Core.Domain.Tax.VatNumberStatus" when placing an order +* MegaMenu: Improved item rendering for third tier elements ## SmartStore.NET 3.1.5 From 8022e9593265005f8c943854685f84b949b3e906 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 9 Nov 2018 12:27:23 +0100 Subject: [PATCH 002/657] Added some moved customer properties to customer XML export --- .../DataExchange/Export/ExportXmlHelper.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index f224098270..df53b4cb3f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -941,7 +941,15 @@ public void WriteCustomer(dynamic customer, string node) _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); _writer.Write("LastLoginDateUtc", entity.LastLoginDateUtc.HasValue ? entity.LastLoginDateUtc.Value.ToString(_culture) : ""); _writer.Write("LastActivityDateUtc", entity.LastActivityDateUtc.ToString(_culture)); - _writer.Write("RewardPointsBalance", ((int)customer._RewardPointsBalance).ToString()); + //_writer.Write("Salutation", entity.Salutation); // TODO: marked as "for future use" + _writer.Write("Title", entity.Title); + _writer.Write("FirstName", entity.FirstName); + _writer.Write("LastName", entity.LastName); + _writer.Write("FullName", entity.FullName); + _writer.Write("Company", entity.Company); + _writer.Write("CustomerNumber", entity.CustomerNumber); + _writer.Write("BirthDate", entity.BirthDate.HasValue ? entity.BirthDate.Value.ToString(_culture) : ""); + _writer.Write("RewardPointsBalance", ((int)customer._RewardPointsBalance).ToString()); if (customer.CustomerRoles != null) { From 54eca0ebd607ba5752eccd0ff2a55f18da85f3ab Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 9 Nov 2018 15:41:43 +0100 Subject: [PATCH 003/657] Perf: Export. Bulk loading of associated products. Speeds up price calculation of grouped products. --- .../Catalog/IProductService.cs | 3 +- .../Catalog/PriceCalculationService.cs | 2 +- .../Catalog/ProductService.cs | 8 ++- .../DataExchange/Export/DataExporter.cs | 68 +++++++++++-------- .../Export/DynamicEntityHelper.cs | 63 +++++++++++------ .../Export/Internal/DataExporterContext.cs | 16 ++--- 6 files changed, 101 insertions(+), 59 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs index 1cc2baa1d0..89cc3363ae 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs @@ -157,8 +157,9 @@ public partial interface IProductService /// Gets products that are assigned to group products. /// /// Grouped product identifiers. + /// A value indicating whether to show hidden records. /// Map of associated products. - Multimap GetAssociatedProducts(int[] productIds); + Multimap GetAssociatedProductsByProductIds(int[] productIds, bool showHidden = false); /// /// Get applied discounts by product identifiers diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 9f0c8c7c48..c91b3f533f 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -432,7 +432,7 @@ public virtual PriceCalculationContext CreatePriceCalculationContext( x => _manufacturerService.GetProductManufacturersByProductIds(x), x => _productService.GetAppliedDiscountsByProductIds(x), x => _productService.GetBundleItemsByProductIds(x, true), - x => _productService.GetAssociatedProducts(x) + x => _productService.GetAssociatedProductsByProductIds(x) ); return context; diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index 733432fb31..bd20bc65e9 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -624,7 +624,7 @@ public virtual Multimap GetProductTagsByProductIds(int[] produc return map; } - public virtual Multimap GetAssociatedProducts(int[] productIds) + public virtual Multimap GetAssociatedProductsByProductIds(int[] productIds, bool showHidden = false) { Guard.NotNull(productIds, nameof(productIds)); @@ -633,9 +633,11 @@ public virtual Multimap GetAssociatedProducts(int[] productIds) return new Multimap(); } + // Ignore multistore. Expect multistore setting for associated products is the same as for parent grouped product. var query = _productRepository.TableUntracked - .Where(x => productIds.Contains(x.ParentGroupedProductId) && !x.Deleted && x.Published) - .OrderBy(x => x.DisplayOrder); + .Where(x => productIds.Contains(x.ParentGroupedProductId) && !x.Deleted && (showHidden || x.Published)) + .OrderBy(x => x.ParentGroupedProductId) + .ThenBy(x => x.DisplayOrder); var associatedProducts = query.ToList(); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index c0549dfafe..f2bfd14a9e 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -42,6 +42,7 @@ using SmartStore.Services.Tax; using SmartStore.Utilities; using SmartStore.Utilities.Threading; +using SmartStore.Collections; namespace SmartStore.Services.DataExchange.Export { @@ -324,6 +325,8 @@ x is Picture || x is ProductBundleItem || x is ProductCategory || x is ProductMa ctx.ProductExportContext.Clear(); } + ctx.AssociatedProductContext?.Clear(); + if (ctx.OrderExportContext != null) { _dbContext.DetachEntities(x => @@ -394,9 +397,16 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in skip => GetProducts(ctx, skip), entities => { - // load data behind navigation properties for current queue in one go + // Load data behind navigation properties for current queue in one go. ctx.ProductExportContext = CreateProductExportContext(entities, ctx.ContextCustomer, ctx.Store.Id); - }, + ctx.AssociatedProductContext = null; + if (!ctx.Projection.NoGroupedProducts && entities.Where(x => x.ProductType == ProductType.GroupedProduct).Any()) + { + ctx.ProductExportContext.AssociatedProducts.LoadAll(); + var associatedProducts = ctx.ProductExportContext.AssociatedProducts.SelectMany(x => x.Value); + ctx.AssociatedProductContext = CreateProductExportContext(associatedProducts, ctx.ContextCustomer, ctx.Store.Id); + } + }, entity => Convert(ctx, entity), offset, PageSize, limit, recordsPerSegment, totalCount ); @@ -763,7 +773,7 @@ public virtual ProductExportContext CreateProductExportContext( x => _manufacturerService.Value.GetProductManufacturersByProductIds(x), x => _productService.Value.GetAppliedDiscountsByProductIds(x), x => _productService.Value.GetBundleItemsByProductIds(x, showHidden), - x => _productService.Value.GetAssociatedProducts(x), + x => _productService.Value.GetAssociatedProductsByProductIds(x), x => _pictureService.Value.GetPicturesByProductIds(x, maxPicturesPerProduct, true), x => _productService.Value.GetProductPicturesByProductIds(x), x => _productService.Value.GetProductTagsByProductIds(x), @@ -835,11 +845,16 @@ private IQueryable GetProductQuery(DataExporterContext ctx, int skip, i private List GetProducts(DataExporterContext ctx, int skip) { - // we use ctx.EntityIdsPerSegment to avoid exporting products multiple times per segment\file (cause of associated products). - + // We use ctx.EntityIdsPerSegment to avoid exporting products multiple times per segment\file (cause of associated products). var result = new List(); - var products = GetProductQuery(ctx, skip, PageSize).ToList(); + Multimap associatedProducts = null; + + if (ctx.Projection.NoGroupedProducts) + { + var groupedProductIds = products.Where(x => x.ProductType == ProductType.GroupedProduct).Select(x => x.Id).ToArray(); + associatedProducts = _productService.Value.GetAssociatedProductsByProductIds(groupedProductIds, true); + } foreach (var product in products) { @@ -855,27 +870,26 @@ private List GetProducts(DataExporterContext ctx, int skip) { if (ctx.Projection.NoGroupedProducts) { - var searchQuery = new CatalogSearchQuery() - .HasParentGroupedProduct(product.Id) - .HasStoreId(ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); - - if (ctx.Projection.OnlyIndividuallyVisibleAssociated) - searchQuery = searchQuery.VisibleIndividuallyOnly(true); - - if (ctx.Filter.IsPublished.HasValue) - searchQuery = searchQuery.PublishedOnly(ctx.Filter.IsPublished.Value); - - var query = _catalogSearchService.Value.PrepareQuery(searchQuery); - var associatedProducts = query.OrderBy(p => p.DisplayOrder).ToList(); - - foreach (var associatedProduct in associatedProducts) - { - if (!ctx.EntityIdsPerSegment.Contains(associatedProduct.Id)) - { - result.Add(associatedProduct); - ctx.EntityIdsPerSegment.Add(associatedProduct.Id); - } - } + if (associatedProducts.ContainsKey(product.Id)) + { + foreach (var associatedProduct in associatedProducts[product.Id]) + { + if (ctx.Projection.OnlyIndividuallyVisibleAssociated && !associatedProduct.VisibleIndividually) + { + continue; + } + if (ctx.Filter.IsPublished.HasValue && ctx.Filter.IsPublished.Value != associatedProduct.Published) + { + continue; + } + + if (!ctx.EntityIdsPerSegment.Contains(associatedProduct.Id)) + { + result.Add(associatedProduct); + ctx.EntityIdsPerSegment.Add(associatedProduct.Id); + } + } + } } else { diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 8bc039898c..24c7250399 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -159,36 +159,61 @@ private decimal CalculatePrice( ICollection attributeValues) { var price = product.Price; - var priceCalculationContext = ctx.ProductExportContext as PriceCalculationContext; + var productContext = ctx.ProductExportContext as PriceCalculationContext; + var associatedProductContext = ctx.AssociatedProductContext as PriceCalculationContext; - if (combination != null) + if (combination != null) { // price for attribute combination var attributesTotalPriceBase = decimal.Zero; if (attributeValues != null) { - attributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.Value.GetProductVariantAttributeValuePriceAdjustment(x, product, ctx.ContextCustomer, priceCalculationContext)); + attributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.Value.GetProductVariantAttributeValuePriceAdjustment(x, product, ctx.ContextCustomer, productContext)); } - price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, attributesTotalPriceBase, true, 1, null, priceCalculationContext); + price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, attributesTotalPriceBase, true, 1, null, productContext); } else if (ctx.Projection.PriceType.HasValue) - { - // price for product - if (ctx.Projection.PriceType.Value == PriceDisplayType.LowestPrice) - { - bool displayFromMessage; - price = _priceCalculationService.Value.GetLowestPrice(product, ctx.ContextCustomer, priceCalculationContext, out displayFromMessage); - } - else if (ctx.Projection.PriceType.Value == PriceDisplayType.PreSelectedPrice) - { - price = _priceCalculationService.Value.GetPreselectedPrice(product, ctx.ContextCustomer, ctx.ContextCurrency, priceCalculationContext); - } - else if (ctx.Projection.PriceType.Value == PriceDisplayType.PriceWithoutDiscountsAndAttributes) - { - price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, decimal.Zero, false, 1, null, priceCalculationContext); - } + { + var priceType = ctx.Projection.PriceType.Value; + + if (product.ProductType == ProductType.GroupedProduct) + { + var associatedProducts = productContext.AssociatedProducts.GetOrLoad(product.Id); + if (associatedProducts.Any()) + { + var firstAssociatedProduct = associatedProducts.First(); + + if (priceType == PriceDisplayType.PreSelectedPrice) + { + price = _priceCalculationService.Value.GetPreselectedPrice(firstAssociatedProduct, ctx.ContextCustomer, ctx.ContextCurrency, associatedProductContext); + } + else if (priceType == PriceDisplayType.PriceWithoutDiscountsAndAttributes) + { + price = _priceCalculationService.Value.GetFinalPrice(firstAssociatedProduct, null, ctx.ContextCustomer, decimal.Zero, false, 1, null, associatedProductContext); + } + else if (priceType == PriceDisplayType.LowestPrice) + { + price = _priceCalculationService.Value.GetLowestPrice(product, ctx.ContextCustomer, associatedProductContext, associatedProducts, out _) ?? decimal.Zero; + } + } + } + else + { + if (priceType == PriceDisplayType.PreSelectedPrice) + { + price = _priceCalculationService.Value.GetPreselectedPrice(product, ctx.ContextCustomer, ctx.ContextCurrency, productContext); + } + else if (priceType == PriceDisplayType.PriceWithoutDiscountsAndAttributes) + { + price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, decimal.Zero, false, 1, null, productContext); + } + else if (priceType == PriceDisplayType.LowestPrice) + { + price = _priceCalculationService.Value.GetLowestPrice(product, ctx.ContextCustomer, productContext, out _); + } + } } return ConvertPrice(ctx, product, price) ?? price; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index c0e56b6175..b3384c5807 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Threading; using SmartStore.Core; -using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Directory; @@ -12,7 +11,7 @@ namespace SmartStore.Services.DataExchange.Export.Internal { - internal class DataExporterContext + internal class DataExporterContext { public DataExporterContext( DataExportRequest request, @@ -62,7 +61,7 @@ public DataExporterContext( } /// - /// All entity identifiers per export + /// All entity identifiers per export. /// public List EntityIdsLoaded { get; set; } public void SetLoadedEntityIds(IEnumerable ids) @@ -74,7 +73,7 @@ public void SetLoadedEntityIds(IEnumerable ids) } /// - /// All entity identifiers per segment (to avoid exporting products multiple times) + /// All entity identifiers per segment (to avoid exporting products multiple times). /// public List EntityIdsPerSegment { get; set; } @@ -88,7 +87,7 @@ public void SetLoadedEntityIds(IEnumerable ids) public bool Supports(ExportFeatures feature) { - return (!IsPreview && Request.Provider.Metadata.ExportFeatures.HasFlag(feature)); + return !IsPreview && Request.Provider.Metadata.ExportFeatures.HasFlag(feature); } public ExportFilter Filter { get; private set; } @@ -107,7 +106,7 @@ public bool IsFileBasedExport get { return Request.Provider == null || Request.Provider.Value == null || Request.Provider.Value.FileExtension.HasValue(); } } - // data loaded once per export + // Data loaded once per export. public Dictionary DeliveryTimes { get; set; } public Dictionary QuantityUnits { get; set; } public Dictionary Stores { get; set; } @@ -117,9 +116,10 @@ public bool IsFileBasedExport public Dictionary CategoryTemplates { get; set; } public HashSet NewsletterSubscriptions { get; set; } - // data loaded once per page + // Data loaded once per page. public ProductExportContext ProductExportContext { get; set; } - public OrderExportContext OrderExportContext { get; set; } + public ProductExportContext AssociatedProductContext { get; set; } + public OrderExportContext OrderExportContext { get; set; } public ManufacturerExportContext ManufacturerExportContext { get; set; } public CategoryExportContext CategoryExportContext { get; set; } public CustomerExportContext CustomerExportContext { get; set; } From 15597b4a71f8238ff646a8ce4c738631381e12b6 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 10 Nov 2018 00:20:24 +0100 Subject: [PATCH 004/657] New data fetching strategy for translations and url slugs during product list rendering: Prefetch instead of segmented caching. Perfect for REALLY LARGE catalogs (> 1M). Must be turned on per advanced setting 'PerformanceSetting.Prefetch...' --- .../Domain/Common/PerformanceSettings.cs | 25 ++- .../LocalizedPropertyCollection.cs | 53 ++++-- .../Domain/Seo/UrlRecordCollection.cs | 89 ++++++++++ .../SmartStore.Core/SmartStore.Core.csproj | 1 + .../Migrations/MigrationsConfiguration.cs | 8 +- .../Localization/LocalizedEntityService.cs | 66 ++++--- .../SmartStore.Services/ScopedServiceBase.cs | 13 +- .../Seo/IUrlRecordService.cs | 33 ++++ .../Seo/UrlRecordService.cs | 162 ++++++++++++++---- .../Controllers/CatalogHelper.MapProduct.cs | 125 ++++++++++---- .../Controllers/CatalogHelper.cs | 12 +- .../Controllers/SearchController.cs | 12 +- 12 files changed, 485 insertions(+), 114 deletions(-) create mode 100644 src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs diff --git a/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs index 2fddb9c454..8acd3065db 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs @@ -7,11 +7,28 @@ public class PerformanceSettings : ISettings { /// /// The number of entries in a single cache segment - /// when greedy loading is disabled. + /// when greedy loading is disabled. The larger the catalog, + /// the smaller this value should be. We recommend segment + /// size 500 for catalogs smaller than 100.000 items. /// - /// - /// The cache has to be cleared after changing this setting. - /// public int CacheSegmentSize { get; set; } = 500; + + /// + /// By default only instant search prefetches translations. + /// All other product listings work against the segmented cache. + /// In very large multilingual catalogs (> 500.000) setting this to true + /// can result in higher request performance as well as less + /// resource usage. + /// + public bool AlwaysPrefetchTranslations { get; set; } + + /// + /// By default only instant search prefetches url slugs. + /// All other product listings work against the segmented cache. + /// In very large catalogs (> 500.000) setting this to true + /// can result in higher request performance as well as less + /// resource usage. + /// + public bool AlwaysPrefetchUrlSlugs { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs index 076d2c0d94..9cd5549e0f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Globalization; namespace SmartStore.Core.Domain.Localization @@ -9,14 +10,20 @@ public class LocalizedPropertyCollection : IReadOnlyCollection _dict; + private HashSet _requestedSet; - public LocalizedPropertyCollection(string keyGroup, IEnumerable properties) + public LocalizedPropertyCollection(string keyGroup, int[] requestedSet, IEnumerable items) { Guard.NotEmpty(keyGroup, nameof(keyGroup)); - Guard.NotNull(properties, nameof(properties)); + Guard.NotNull(items, nameof(items)); _keyGroup = keyGroup; - _dict = properties.ToDictionarySafe(x => CreateKey(x.LocaleKey, x.EntityId, x.LanguageId), StringComparer.OrdinalIgnoreCase); + _dict = items.ToDictionarySafe(x => CreateKey(x.LocaleKey, x.EntityId, x.LanguageId), StringComparer.OrdinalIgnoreCase); + + if (requestedSet != null && requestedSet.Length > 0) + { + _requestedSet = new HashSet(requestedSet); + } } public void MergeWith(LocalizedPropertyCollection other) @@ -28,22 +35,47 @@ public void MergeWith(LocalizedPropertyCollection other) throw new InvalidOperationException("Expected keygroup '{0}', but was '{1}'".FormatInvariant(this._keyGroup, other._keyGroup)); } + // Merge dictionary other._dict.Merge(this._dict, true); + + // Merge requested set (entity ids) + if (this._requestedSet != null) + { + if (other._requestedSet == null) + { + other._requestedSet = new HashSet(this._requestedSet); + } + else + { + other._requestedSet.AddRange(this._requestedSet); + } + } } - public LocalizedProperty Find(int languageId, int entityId, string localeKey) + public string GetValue(int languageId, int entityId, string localeKey) { - return _dict.Get(CreateKey(localeKey, entityId, languageId)); + return Find(languageId, entityId, localeKey)?.LocaleValue; } - public string GetValue(int languageId, int entityId, string localeKey) + public LocalizedProperty Find(int languageId, int entityId, string localeKey) { - if (_dict.TryGetValue(CreateKey(localeKey, entityId, languageId), out var prop)) + var item = _dict.Get(CreateKey(localeKey, entityId, languageId)); + + if (item == null && (_requestedSet == null || _requestedSet.Contains(entityId))) { - return prop.LocaleValue; + // Although the item does not exist in the local dictionary it has been requested + // from the database, which means it does not exist in the db either. + // Avoid the upcoming roundtrip. + return new LocalizedProperty + { + LocaleKeyGroup = _keyGroup, + EntityId = entityId, + LanguageId = languageId, + LocaleKey = localeKey + }; } - return null; + return item; } private string CreateKey(string localeKey, int entityId, int languageId) @@ -52,9 +84,6 @@ private string CreateKey(string localeKey, int entityId, int languageId) } public int Count => _dict.Values.Count; - - public bool IsReadOnly => throw new NotImplementedException(); - public IEnumerator GetEnumerator() => _dict.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _dict.Values.GetEnumerator(); } diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs new file mode 100644 index 0000000000..1863ef4bb5 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; + +namespace SmartStore.Core.Domain.Seo +{ + public class UrlRecordCollection : IReadOnlyCollection + { + private readonly string _entityName; + private readonly IDictionary _dict; + private HashSet _requestedSet; + + public UrlRecordCollection(string entityName, int[] requestedSet, IEnumerable items) + { + Guard.NotEmpty(entityName, nameof(entityName)); + Guard.NotNull(items, nameof(items)); + + _entityName = entityName; + _dict = items.ToDictionarySafe(x => CreateKey(x.EntityId, x.LanguageId)); + + if (requestedSet != null && requestedSet.Length > 0) + { + _requestedSet = new HashSet(requestedSet); + } + } + + public void MergeWith(UrlRecordCollection other) + { + Guard.NotNull(other, nameof(other)); + + if (!this._entityName.IsCaseInsensitiveEqual(other._entityName)) + { + throw new InvalidOperationException("Expected group '{0}', but was '{1}'".FormatInvariant(this._entityName, other._entityName)); + } + + // Merge dictionary + other._dict.Merge(this._dict, true); + + // Merge requested set (entity ids) + if (this._requestedSet != null) + { + if (other._requestedSet == null) + { + other._requestedSet = new HashSet(this._requestedSet); + } + else + { + other._requestedSet.AddRange(this._requestedSet); + } + } + } + + public string GetSlug(int languageId, int entityId) + { + return Find(languageId, entityId)?.Slug; + } + + public UrlRecord Find(int languageId, int entityId) + { + var item = _dict.Get(CreateKey(entityId, languageId)); + + if (item == null && (_requestedSet == null || _requestedSet.Contains(entityId))) + { + // Although the item does not exist in the local dictionary it has been requested + // from the database, which means it does not exist in the db either. + // Avoid the upcoming roundtrip. + return new UrlRecord + { + EntityName = _entityName, + EntityId = entityId, + LanguageId = languageId + }; + } + + return item; + } + + private string CreateKey(int entityId, int languageId) + { + return string.Concat(entityId.ToString(CultureInfo.InvariantCulture), "-", languageId); + } + + public int Count => _dict.Values.Count; + public IEnumerator GetEnumerator() => _dict.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _dict.Values.GetEnumerator(); + } +} diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 3d971c06b7..4a2d409068 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -247,6 +247,7 @@ + diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 3db5bc46b2..b47d2e61fe 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -30,8 +30,12 @@ protected override void Seed(SmartObjectContext context) public void MigrateSettings(SmartObjectContext context) { - var name = TypeHelper.NameOf(y => y.CacheSegmentSize, true); - context.MigrateSettings(x => x.Add(name, 500)); + context.MigrateSettings(x => + { + x.Add(TypeHelper.NameOf(y => y.CacheSegmentSize, true), 500); + x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchTranslations, true), false); + x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchUrlSlugs, true), false); + }); } public void MigrateLocaleResources(LocaleResourcesBuilder builder) diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index f09a74a593..d6fc49be18 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Threading; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -28,6 +29,7 @@ public partial class LocalizedEntityService : ScopedServiceBase, ILocalizedEntit private readonly PerformanceSettings _performanceSettings; private readonly IDictionary _prefetchedCollections; + private static int _lastCacheSegmentSize = -1; public LocalizedEntityService( ICacheManager cacheManager, @@ -39,6 +41,31 @@ public LocalizedEntityService( _performanceSettings = performanceSettings; _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ValidateCacheState(); + } + + private void ValidateCacheState() + { + // Ensure that after a segment size change the cache segments are invalidated. + var size = _performanceSettings.CacheSegmentSize; + var changed = _lastCacheSegmentSize == -1; + + if (size <= 0) + { + _performanceSettings.CacheSegmentSize = size = 1; + } + + if (_lastCacheSegmentSize > 0 && _lastCacheSegmentSize != size) + { + OnClearCache(); + changed = true; + } + + if (changed) + { + Interlocked.Exchange(ref _lastCacheSegmentSize, size); + } } protected override void OnClearCache() @@ -100,6 +127,15 @@ protected virtual void ClearCacheSegment(string localeKeyGroup, string localeKey public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) { + if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var collection)) + { + var cachedItem = collection.Find(languageId, entityId, localeKey); + if (cachedItem != null) + { + return cachedItem.LocaleValue; + } + } + if (IsInScope) { return GetLocalizedValueUncached(languageId, entityId, localeKeyGroup, localeKey); @@ -123,15 +159,6 @@ protected string GetLocalizedValueUncached(int languageId, int entityId, string if (languageId <= 0) return string.Empty; - if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var collection)) - { - var cachedItem = collection.Find(languageId, entityId, localeKey); - if (cachedItem != null) - { - return cachedItem.LocaleValue; - } - } - var query = from lp in _localizedPropertyRepository.TableUntracked where lp.EntityId == entityId && @@ -163,17 +190,14 @@ public virtual void PrefetchLocalizedProperties(string localeKeyGroup, int langu return; var collection = GetLocalizedPropertyCollectionInternal(localeKeyGroup, languageId, entityIds, isRange, isSorted); - - if (collection.Count > 0) + + if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var existing)) { - if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var existing)) - { - collection.MergeWith(existing); - } - else - { - _prefetchedCollections[localeKeyGroup] = collection; - } + collection.MergeWith(existing); + } + else + { + _prefetchedCollections[localeKeyGroup] = collection; } } @@ -186,7 +210,7 @@ public virtual LocalizedPropertyCollection GetLocalizedPropertyCollectionInterna { Guard.NotEmpty(localeKeyGroup, nameof(localeKeyGroup)); - using (var scope = new DbContextScope(ctx: _localizedPropertyRepository.Context, proxyCreation: false, lazyLoading: false)) + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) { var query = from x in _localizedPropertyRepository.TableUntracked where x.LocaleKeyGroup == localeKeyGroup @@ -212,7 +236,7 @@ public virtual LocalizedPropertyCollection GetLocalizedPropertyCollectionInterna query = query.Where(x => x.LanguageId == languageId); } - return new LocalizedPropertyCollection(localeKeyGroup, query.ToList()); + return new LocalizedPropertyCollection(localeKeyGroup, entityIds, query.ToList()); } } diff --git a/src/Libraries/SmartStore.Services/ScopedServiceBase.cs b/src/Libraries/SmartStore.Services/ScopedServiceBase.cs index 3bd839061b..961fa5cd6f 100644 --- a/src/Libraries/SmartStore.Services/ScopedServiceBase.cs +++ b/src/Libraries/SmartStore.Services/ScopedServiceBase.cs @@ -5,8 +5,6 @@ namespace SmartStore.Services { public abstract class ScopedServiceBase : IScopedService { - private bool _isInScope; - /// /// Creates a long running unit of work in which cache eviction is suppressed /// @@ -14,18 +12,18 @@ public abstract class ScopedServiceBase : IScopedService /// A disposable unit of work public IDisposable BeginScope(bool clearCache = true) { - if (_isInScope) + if (IsInScope) { // nested batches are not supported return ActionDisposable.Empty; } OnBeginScope(); - _isInScope = true; + IsInScope = true; return new ActionDisposable(() => { - _isInScope = false; + IsInScope = false; OnEndScope(); if (clearCache && HasChanges) { @@ -48,12 +46,13 @@ public bool HasChanges protected bool IsInScope { - get { return _isInScope; } + get; + private set; } public void ClearCache() { - if (!_isInScope) + if (!IsInScope) { OnClearCache(); HasChanges = false; diff --git a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs index 15d0d8bb35..732a73fad1 100644 --- a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs @@ -63,6 +63,39 @@ public partial interface IUrlRecordService : IScopedService /// Customer collection IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive); + /// + /// Prefetches a collection of url records properties for a range of entities in one go + /// and caches them for the duration of the current request. + /// + /// Entity name + /// + /// The entity ids to prefetch url records for. Can be null, + /// in which case all records for the requested entity name are loaded. + /// + /// Whether represents a range of ids (perf). + /// Whether is already sorted (perf). + /// Url record collection + /// + /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). + /// + void PrefetchUrlRecords(string entityName, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false); + + /// + /// Prefetches a collection of url records properties for a range of entities in one go. + /// + /// Entity name + /// + /// The entity ids to prefetch url records for. Can be null, + /// in which case all records for the requested entity name are loaded. + /// + /// Whether represents a range of ids (perf). + /// Whether is already sorted (perf). + /// Url record collection + /// + /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). + /// + UrlRecordCollection GetUrlRecordCollection(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false); + /// /// Gets all URL records for the specified entity /// diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index c7ab2c3dec..0d13b0bdff 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -15,8 +16,8 @@ public partial class UrlRecordService : ScopedServiceBase, IUrlRecordService /// /// 0 = segment (EntityName.IdRange), 1 = language id /// - const string URLRECORD_SEGMENT_KEY = "urlrecord:{0}-lang-{1}"; - const string URLRECORD_SEGMENT_PATTERN = "urlrecord:{0}*"; + const string URLRECORD_SEGMENT_KEY = "urlrecord:segment:{0}-lang-{1}"; + const string URLRECORD_SEGMENT_PATTERN = "urlrecord:segment:{0}*"; const string URLRECORD_ALL_PATTERN = "urlrecord:*"; const string URLRECORD_ALL_ACTIVESLUGS_KEY = "urlrecord:all-active-slugs"; @@ -25,6 +26,9 @@ public partial class UrlRecordService : ScopedServiceBase, IUrlRecordService private readonly SeoSettings _seoSettings; private readonly PerformanceSettings _performanceSettings; + private readonly IDictionary _prefetchedCollections; + private static int _lastCacheSegmentSize = -1; + public UrlRecordService( ICacheManager cacheManager, IRepository urlRecordRepository, @@ -35,8 +39,35 @@ public UrlRecordService( _urlRecordRepository = urlRecordRepository; _seoSettings = seoSettings; _performanceSettings = performanceSettings; + + _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ValidateCacheState(); } + private void ValidateCacheState() + { + // Ensure that after a segment size change the cache segments are invalidated. + var size = _performanceSettings.CacheSegmentSize; + var changed = _lastCacheSegmentSize == -1; + + if (size <= 0) + { + _performanceSettings.CacheSegmentSize = size = 1; + } + + if (_lastCacheSegmentSize > 0 && _lastCacheSegmentSize != size) + { + _cacheManager.RemoveByPattern(URLRECORD_SEGMENT_PATTERN); + changed = true; + } + + if (changed) + { + Interlocked.Exchange(ref _lastCacheSegmentSize, size); + } + } + protected override void OnClearCache() { _cacheManager.Remove(URLRECORD_ALL_PATTERN); @@ -153,6 +184,60 @@ public virtual IList GetUrlRecordsFor(string entityName, int entityId return query.ToList(); } + public virtual void PrefetchUrlRecords(string entityName, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false) + { + var collection = GetUrlRecordCollectionInternal(entityName, languageId, entityIds, isRange, isSorted); + + if (_prefetchedCollections.TryGetValue(entityName, out var existing)) + { + collection.MergeWith(existing); + } + else + { + _prefetchedCollections[entityName] = collection; + } + } + + public virtual UrlRecordCollection GetUrlRecordCollection(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false) + { + return GetUrlRecordCollectionInternal(entityName, 0, entityIds, isRange, isSorted); + } + + public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityName, int? languageId, int[] entityIds, bool isRange = false, bool isSorted = false) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) + { + var query = from x in _urlRecordRepository.TableUntracked + where x.EntityName == entityName && x.IsActive + select x; + + if (entityIds != null && entityIds.Length > 0) + { + if (isRange) + { + var min = isSorted ? entityIds[0] : entityIds.Min(); + var max = isSorted ? entityIds[entityIds.Length - 1] : entityIds.Max(); + + query = query.Where(x => x.EntityId >= min && x.EntityId <= max); + } + else + { + query = query.Where(x => entityIds.Contains(x.EntityId)); + } + } + + if (languageId.HasValue) + { + query = query.Where(x => x.LanguageId == languageId.Value); + } + + // Don't sort DESC, because latter items overwrite exisiting ones (it's the same as sorting DESC and taking the first) + return new UrlRecordCollection(entityName, entityIds, query.OrderBy(x => x.Id).ToList()); + } + } + public virtual UrlRecord GetBySlug(string slug) { // INFO: (mc) Caching unnecessary here. This is not a 'bottleneck' function. @@ -169,6 +254,15 @@ public virtual UrlRecord GetBySlug(string slug) public virtual string GetActiveSlug(int entityId, string entityName, int languageId) { + if (_prefetchedCollections.TryGetValue(entityName, out var collection)) + { + var cachedItem = collection.Find(languageId, entityId); + if (cachedItem != null) + { + return cachedItem.Slug.EmptyNull(); + } + } + if (IsInScope) { return GetActiveSlugUncached(entityId, entityName, languageId); @@ -180,17 +274,20 @@ public virtual string GetActiveSlug(int entityId, string entityName, int languag { var allActiveSlugs = _cacheManager.Get(URLRECORD_ALL_ACTIVESLUGS_KEY, () => { - var query = from x in _urlRecordRepository.TableUntracked - where x.IsActive - orderby x.Id descending - select x; - - var result = query.ToDictionarySafe( - x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), - x => x.Slug, - StringComparer.OrdinalIgnoreCase); - - return result; + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) + { + var query = from x in _urlRecordRepository.TableUntracked + where x.IsActive + orderby x.Id descending + select x; + + var result = query.ToDictionarySafe( + x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), + x => x.Slug, + StringComparer.OrdinalIgnoreCase); + + return result; + } }); var key = GenerateKey(entityId, entityName, languageId); @@ -396,26 +493,29 @@ protected virtual IDictionary GetCacheSegment(string entityName, in return _cacheManager.Get(cacheKey, () => { - var query = from ur in _urlRecordRepository.TableUntracked - where - ur.EntityId >= minEntityId && - ur.EntityId <= maxEntityId && - ur.EntityName == entityName && - ur.LanguageId == languageId && - ur.IsActive - orderby ur.Id descending - select ur; - - var urlRecords = query.ToList(); - - var dict = new Dictionary(urlRecords.Count); - - foreach (var ur in urlRecords) + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) { - dict[ur.EntityId] = ur.Slug.EmptyNull(); + var query = from ur in _urlRecordRepository.TableUntracked + where + ur.EntityId >= minEntityId && + ur.EntityId <= maxEntityId && + ur.EntityName == entityName && + ur.LanguageId == languageId && + ur.IsActive + orderby ur.Id descending + select ur; + + var urlRecords = query.ToList(); + + var dict = new Dictionary(urlRecords.Count); + + foreach (var ur in urlRecords) + { + dict[ur.EntityId] = ur.Slug.EmptyNull(); + } + + return dict; } - - return dict; }); } diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs index e40bbc131c..123410c6b3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs @@ -22,6 +22,7 @@ using SmartStore.Services.Search; using SmartStore.Services.Security; using SmartStore.Services.Seo; +using SmartStore.Utilities; using SmartStore.Web.Framework; using SmartStore.Web.Framework.UI; using SmartStore.Web.Infrastructure.Cache; @@ -159,18 +160,56 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr { settings = new ProductSummaryMappingSettings(); } - + using (_services.Chronometer.Step("MapProductSummaryModel")) { + var model = new ProductSummaryModel(products) + { + ViewMode = settings.ViewMode, + GridColumnSpan = _catalogSettings.GridStyleListColumnSpan, + ShowSku = _catalogSettings.ShowProductSku, + ShowWeight = _catalogSettings.ShowWeight, + ShowDimensions = settings.MapDimensions, + ShowLegalInfo = settings.MapLegalInfo, + ShowDescription = settings.MapShortDescription, + ShowFullDescription = settings.MapFullDescription, + ShowRatings = settings.MapReviews, + ShowDeliveryTimes = settings.MapDeliveryTimes, + ShowPrice = settings.MapPrices, + ShowBasePrice = settings.MapPrices && _catalogSettings.ShowBasePriceInProductLists && settings.ViewMode != ProductSummaryViewMode.Mini, + ShowShippingSurcharge = settings.MapPrices && settings.ViewMode != ProductSummaryViewMode.Mini, + ShowButtons = settings.ViewMode != ProductSummaryViewMode.Mini, + ShowBrand = settings.MapManufacturers, + ForceRedirectionAfterAddingToCart = settings.ForceRedirectionAfterAddingToCart, + CompareEnabled = _catalogSettings.CompareProductsEnabled, + WishlistEnabled = _permissionService.Value.Authorize(StandardPermissionProvider.EnableWishlist), + BuyEnabled = !_catalogSettings.HideBuyButtonInLists, + ThumbSize = settings.ThumbnailSize, + ShowDiscountBadge = _catalogSettings.ShowDiscountSign, + ShowNewBadge = _catalogSettings.LabelAsNewForMaxDays.HasValue + }; + + if (products.Count == 0) + { + // No products, stop here. + return model; + } + // PERF!! var store = _services.StoreContext.CurrentStore; var customer = _services.WorkContext.CurrentCustomer; var currency = _services.WorkContext.WorkingCurrency; + var language = _services.WorkContext.WorkingLanguage; var allowPrices = _services.Permissions.Authorize(StandardPermissionProvider.DisplayPrices); var sllowShoppingCart = _services.Permissions.Authorize(StandardPermissionProvider.EnableShoppingCart); var allowWishlist = _services.Permissions.Authorize(StandardPermissionProvider.EnableWishlist); var taxDisplayType = _services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id); var cachedManufacturerModels = new Dictionary(); + var prefetchTranslations = settings.PrefetchTranslations == true || (settings.PrefetchTranslations == null && _performanceSettings.AlwaysPrefetchTranslations); + var prefetchSlugs = settings.PrefetchUrlSlugs == true || (settings.PrefetchUrlSlugs == null && _performanceSettings.AlwaysPrefetchUrlSlugs); + var allProductIds = prefetchSlugs || prefetchTranslations ? products.Select(x => x.Id).ToArray() : new int[0]; + + //var productIds = products.Select(x => x.Id).ToArray(); string taxInfo = T(taxDisplayType == TaxDisplayType.IncludingTax ? "Tax.InclVAT" : "Tax.ExclVAT"); var legalInfo = ""; @@ -198,6 +237,17 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr } } + if (prefetchSlugs) + { + _urlRecordService.PrefetchUrlRecords(nameof(Product), language.Id, allProductIds); + } + + if (prefetchTranslations) + { + // Prefetch all delivery time translations + _localizedEntityService.PrefetchLocalizedProperties(nameof(DeliveryTime), language.Id, null); + } + using (var scope = new DbContextScope(ctx: _services.DbContext, autoCommit: false, validateOnSave: false)) { // Run in uncommitting scope, because pictures could be updated (IsNew property) @@ -212,6 +262,21 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr if (settings.MapAttributes || settings.MapColorAttributes) { batchContext.Attributes.LoadAll(); + + if (prefetchTranslations) + { + // Prefetch all product attribute translations + PrefetchTranslations( + nameof(ProductAttribute), + language.Id, + batchContext.Attributes.SelectMany(x => x.Value).Select(x => x.ProductAttribute)); + + // Prefetch all variant attribute value translations + PrefetchTranslations( + nameof(ProductVariantAttributeValue), + language.Id, + batchContext.Attributes.SelectMany(x => x.Value).SelectMany(x => x.ProductVariantAttributeValues)); + } } if (settings.MapManufacturers) @@ -222,42 +287,25 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr if (settings.MapSpecificationAttributes) { batchContext.SpecificationAttributes.LoadAll(); - } - if (settings.MapPictures) - { - //var pids = products.Where(x => x.MainPictureId.HasValue).Select(x => x.MainPictureId.Value); - //var pis = ((Services.Media.PictureService)_pictureService).GetPictureInfos(pids, model.ThumbSize ?? 250); + if (prefetchTranslations) + { + // Prefetch all spec attribute option translations + PrefetchTranslations( + nameof(SpecificationAttributeOption), + language.Id, + batchContext.SpecificationAttributes.SelectMany(x => x.Value).Select(x => x.SpecificationAttributeOption)); + + // Prefetch all spec attribute translations + PrefetchTranslations( + nameof(SpecificationAttribute), + language.Id, + batchContext.SpecificationAttributes.SelectMany(x => x.Value).Select(x => x.SpecificationAttributeOption.SpecificationAttribute)); + } } - var model = new ProductSummaryModel(products) - { - ViewMode = settings.ViewMode, - GridColumnSpan = _catalogSettings.GridStyleListColumnSpan, - ShowSku = _catalogSettings.ShowProductSku, - ShowWeight = _catalogSettings.ShowWeight, - ShowDimensions = settings.MapDimensions, - ShowLegalInfo = settings.MapLegalInfo, - ShowDescription = settings.MapShortDescription, - ShowFullDescription = settings.MapFullDescription, - ShowRatings = settings.MapReviews, - ShowDeliveryTimes = settings.MapDeliveryTimes, - ShowPrice = settings.MapPrices, - ShowBasePrice = settings.MapPrices && _catalogSettings.ShowBasePriceInProductLists && settings.ViewMode != ProductSummaryViewMode.Mini, - ShowShippingSurcharge = settings.MapPrices && settings.ViewMode != ProductSummaryViewMode.Mini, - ShowButtons = settings.ViewMode != ProductSummaryViewMode.Mini, - ShowBrand = settings.MapManufacturers, - ForceRedirectionAfterAddingToCart = settings.ForceRedirectionAfterAddingToCart, - CompareEnabled = _catalogSettings.CompareProductsEnabled, - WishlistEnabled = _permissionService.Value.Authorize(StandardPermissionProvider.EnableWishlist), - BuyEnabled = !_catalogSettings.HideBuyButtonInLists, - ThumbSize = settings.ThumbnailSize, - ShowDiscountBadge = _catalogSettings.ShowDiscountSign, - ShowNewBadge = _catalogSettings.LabelAsNewForMaxDays.HasValue - }; - // If a size has been set in the view, we use it in priority - int thumbSize = model.ThumbSize.HasValue ? model.ThumbSize.Value : _mediaSettings.ProductThumbPictureSize; + int thumbSize = model.ThumbSize ?? _mediaSettings.ProductThumbPictureSize; var mapItemContext = new MapProductSummaryItemContext { @@ -297,6 +345,14 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr } } + private void PrefetchTranslations(string keyGroup, int languageId, IEnumerable entities) + { + if (entities.Any()) + { + _localizedEntityService.PrefetchLocalizedProperties(keyGroup, languageId, entities.Select(x => x.Id).Distinct().ToArray()); + } + } + private void MapProductSummaryItem(Product product, MapProductSummaryItemContext ctx) { var contextProduct = product; @@ -787,5 +843,8 @@ public ProductSummaryMappingSettings() public bool ForceRedirectionAfterAddingToCart { get; set; } public int? ThumbnailSize { get; set; } + + public bool? PrefetchTranslations { get; set; } + public bool? PrefetchUrlSlugs { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs index fa2de199fc..4fffd68631 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs @@ -39,6 +39,7 @@ using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Media; +using SmartStore.Core.Domain.Common; namespace SmartStore.Web.Controllers { @@ -67,6 +68,7 @@ public partial class CatalogHelper private readonly CustomerSettings _customerSettings; private readonly CaptchaSettings _captchaSettings; private readonly TaxSettings _taxSettings; + private readonly PerformanceSettings _performanceSettings; private readonly IMeasureService _measureService; private readonly IQuantityUnitService _quantityUnitService; private readonly MeasureSettings _measureSettings; @@ -81,6 +83,8 @@ public partial class CatalogHelper private readonly HttpRequestBase _httpRequest; private readonly UrlHelper _urlHelper; private readonly ProductUrlHelper _productUrlHelper; + private readonly ILocalizedEntityService _localizedEntityService; + private readonly IUrlRecordService _urlRecordService; public CatalogHelper( ICommonServices services, @@ -108,6 +112,7 @@ public CatalogHelper( IQuantityUnitService quantityUnitService, MeasureSettings measureSettings, TaxSettings taxSettings, + PerformanceSettings performanceSettings, IDeliveryTimeService deliveryTimeService, ISettingService settingService, Lazy _menuPublisher, @@ -119,7 +124,9 @@ public CatalogHelper( ISiteMapService siteMapService, HttpRequestBase httpRequest, UrlHelper urlHelper, - ProductUrlHelper productUrlHelper) + ProductUrlHelper productUrlHelper, + ILocalizedEntityService localizedEntityService, + IUrlRecordService urlRecordService) { this._services = services; this._categoryService = categoryService; @@ -143,6 +150,7 @@ public CatalogHelper( this._quantityUnitService = quantityUnitService; this._measureSettings = measureSettings; this._taxSettings = taxSettings; + this._performanceSettings = performanceSettings; this._deliveryTimeService = deliveryTimeService; this._settingService = settingService; this._mediaSettings = mediaSettings; @@ -158,6 +166,8 @@ public CatalogHelper( this._httpRequest = httpRequest; this._urlHelper = urlHelper; this._productUrlHelper = productUrlHelper; + this._localizedEntityService = localizedEntityService; + this._urlRecordService = urlRecordService; T = NullLocalizer.Instance; } diff --git a/src/Presentation/SmartStore.Web/Controllers/SearchController.cs b/src/Presentation/SmartStore.Web/Controllers/SearchController.cs index 777ca26594..0323a4cc22 100644 --- a/src/Presentation/SmartStore.Web/Controllers/SearchController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/SearchController.cs @@ -11,6 +11,7 @@ using SmartStore.Services.Search; using SmartStore.Services.Search.Modelling; using SmartStore.Services.Search.Rendering; +using SmartStore.Services.Seo; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Models.Catalog; @@ -28,6 +29,7 @@ public partial class SearchController : PublicControllerBase private readonly CatalogHelper _catalogHelper; private readonly ICatalogSearchQueryFactory _queryFactory; private readonly ILocalizedEntityService _localizedEntityService; + private readonly IUrlRecordService _urlRecordService; private readonly Lazy _templateProvider; public SearchController( @@ -39,6 +41,7 @@ public SearchController( IGenericAttributeService genericAttributeService, CatalogHelper catalogHelper, ILocalizedEntityService localizedEntityService, + IUrlRecordService urlRecordService, Lazy templateProvider) { _queryFactory = queryFactory; @@ -49,6 +52,7 @@ public SearchController( _genericAttributeService = genericAttributeService; _catalogHelper = catalogHelper; _localizedEntityService = localizedEntityService; + _urlRecordService = urlRecordService; _templateProvider = templateProvider; } @@ -98,11 +102,13 @@ public ActionResult InstantSearch(CatalogSearchQuery query) { x.MapPrices = false; x.MapShortDescription = true; + x.MapPictures = _searchSettings.ShowProductImagesInInstantSearch; + x.ThumbnailSize = _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage; + x.PrefetchTranslations = true; + x.PrefetchUrlSlugs = true; }); - - mappingSettings.MapPictures = _searchSettings.ShowProductImagesInInstantSearch; - mappingSettings.ThumbnailSize = _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage; + using (_urlRecordService.BeginScope(false)) using (_localizedEntityService.BeginScope(false)) { // InstantSearch should be REALLY very fast! No time for smart caching stuff. From baf13c93c5a99258b5d582fc2b51d55809b52303 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 10 Nov 2018 00:40:15 +0100 Subject: [PATCH 005/657] Changed search index should reset all sitemap element counts --- .../Catalog/CategoryTreeChangeHandler.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs index 8e0741a6c8..ebbe3e7632 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs @@ -10,6 +10,8 @@ using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Security; using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Events; +using SmartStore.Core.Search; namespace SmartStore.Services.Catalog { @@ -33,7 +35,7 @@ public CategoryTreeChangedEvent(CategoryTreeChangeReason reason) public CategoryTreeChangeReason Reason { get; private set; } } - public class CategoryTreeChangeHook : IDbSaveHook + public class CategoryTreeChangeHook : IDbSaveHook, IConsumer { private readonly ICommonServices _services; private readonly ICategoryService _categoryService; @@ -256,6 +258,14 @@ public void OnAfterSave(IHookedEntity entry) } } + void IConsumer.HandleEvent(IndexingCompletedEvent message) + { + if (message.IndexInfo.IsModified) + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + private void PublishEvent(CategoryTreeChangeReason reason) { if (_handledReasons[(int)reason] == false) From c793566e573eb27902e69548efabb2bd9c289f0a Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 10 Nov 2018 01:18:22 +0100 Subject: [PATCH 006/657] Void zone content due to duplicate key or ajax request was still rendered --- .../Extensions/HtmlZoneExtensions.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs index 472a99d9c8..fd9b6f3717 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs @@ -28,6 +28,7 @@ private class DocumentZone : IDisposable private readonly WebViewPage _page; private readonly string _targetZone; private readonly ZoneInjectMode _injectMode; + private readonly bool _isVoid; public DocumentZone(HtmlHelper html, string targetZone, ZoneInjectMode injectMode, string key) { @@ -46,7 +47,19 @@ public DocumentZone(HtmlHelper html, string targetZone, ZoneInjectMode injectMod if (key.HasValue()) { - UniqueKeys.Add(key); + if (HasUniqueKey(key)) + { + _isVoid = true; + } + else + { + UniqueKeys.Add(key); + } + } + + if (_page.Request.IsAjaxRequest()) + { + _isVoid = true; } } @@ -86,7 +99,7 @@ public void Dispose() return; var writer = ((StringWriter)_page.OutputStack.Pop()); - var content = writer.ToString(); + var content = _isVoid ? string.Empty : writer.ToString(); _viewContext.Writer = _originalViewContextWriter; @@ -114,11 +127,6 @@ public static IDisposable BeginZoneContent(this HtmlHelper helper, ZoneInjectMode injectMode = ZoneInjectMode.Append, string key = null) { - if ((key.HasValue() && DocumentZone.HasUniqueKey(key)) || helper.ViewContext.HttpContext.Request.IsAjaxRequest()) - { - return ActionDisposable.Empty; - } - return new DocumentZone(helper, targetZone, injectMode, key); } From 8d7ce917a05c9d1f6531b434df8cf5226a9d97f6 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 10 Nov 2018 02:37:56 +0100 Subject: [PATCH 007/657] Fix Chrome mobile check/radio alignment and padding issues --- .../SmartStore.Web/Content/shared/_forms.scss | 16 ++++++ .../Product/Partials/Product.Picture.cshtml | 52 +++++++++---------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Content/shared/_forms.scss b/src/Presentation/SmartStore.Web/Content/shared/_forms.scss index b0c6aa5b85..eb48ebb516 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_forms.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_forms.scss @@ -132,6 +132,22 @@ fieldset.content-group { margin-right: 0.25rem; } +.wkit.touchevents .form-check { + @if $font-size-base < 1 rem { + // Fix Chrome mobile check/radio alignment and padding issues: + // increase spacing from 1.25rem to 1.5rem + padding-left: 1.5rem; + + .form-check-input { + margin-left: -1.5rem; + } + + .form-check-label { + padding-top: 0.2rem; + } + } +} + // Custom range // ------------------------------------------------------ diff --git a/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Picture.cshtml b/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Picture.cshtml index 663efe9ae4..9284f9f703 100644 --- a/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Picture.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Picture.cshtml @@ -3,22 +3,22 @@ @using SmartStore.Web.Models.Catalog; @{ - Html.AddScriptParts("~/bundles/smart-gallery"); - var defaultPicture = Model.DefaultPictureModel; + Html.AddScriptParts("~/bundles/smart-gallery"); + var defaultPicture = Model.DefaultPictureModel; - // FB OpenGraph og:image & Twitter meta - var oImg = Model.PictureModels?.FirstOrDefault(); - if (oImg != null && oImg.FullSizeImageUrl.HasValue()) - { - var url = WebHelper.GetAbsoluteUrl(oImg.FullSizeImageUrl, this.Request, true); - Html.AddCustomHeadParts("".FormatInvariant(url)); - Html.AddCustomHeadParts("".FormatInvariant(url)); - if (oImg.FullSizeImageWidth > 0 && oImg.FullSizeImageHeight > 0) - { - Html.AddCustomHeadParts("".FormatInvariant(oImg.FullSizeImageWidth.Value)); - Html.AddCustomHeadParts("".FormatInvariant(oImg.FullSizeImageHeight.Value)); - } - } + // FB OpenGraph og:image & Twitter meta + var oImg = Model.PictureModels?.FirstOrDefault(); + if (oImg != null && oImg.FullSizeImageUrl.HasValue()) + { + var url = WebHelper.GetAbsoluteUrl(oImg.FullSizeImageUrl, this.Request, true); + Html.AddCustomHeadParts("".FormatInvariant(url)); + Html.AddCustomHeadParts("".FormatInvariant(url)); + if (oImg.FullSizeImageWidth > 0 && oImg.FullSizeImageHeight > 0) + { + Html.AddCustomHeadParts("".FormatInvariant(oImg.FullSizeImageWidth.Value)); + Html.AddCustomHeadParts("".FormatInvariant(oImg.FullSizeImageHeight.Value)); + } + } } bool Exists { get; } + /// + /// Gets the size of the index in bytes. + /// + long IndexSize { get; } + /// /// The identifier of the last added document. /// diff --git a/src/Libraries/SmartStore.Core/Search/IndexInfo.cs b/src/Libraries/SmartStore.Core/Search/IndexInfo.cs index cb48f828c2..7389b7a5cf 100644 --- a/src/Libraries/SmartStore.Core/Search/IndexInfo.cs +++ b/src/Libraries/SmartStore.Core/Search/IndexInfo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using SmartStore.Utilities; namespace SmartStore.Core.Search { @@ -25,6 +26,7 @@ public IndexInfo(string scope) public string Scope { get; private set; } public int DocumentCount { get; set; } + public long IndexSize { get; set; } public int LastAddedDocumentId { get; set; } public IEnumerable Fields { get; set; } @@ -52,6 +54,7 @@ public string ToXml() new XElement("error", Error), new XElement("should-rebuild", ShouldRebuild ? "true" : "false"), new XElement("document-count", DocumentCount), + new XElement("index-size", IndexSize), new XElement("last-added-document-id", LastAddedDocumentId), new XElement("fields", string.Join(", ", Fields ?? Enumerable.Empty())) )).ToString(); @@ -101,6 +104,12 @@ public static IndexInfo FromXml(string xml, string scope) info.DocumentCount = documentCount.ToInt(); } + var indexSize = doc.Descendants("index-size").FirstOrDefault()?.Value; + if (indexSize.HasValue() && CommonHelper.TryConvert(indexSize, out long size)) + { + info.IndexSize = size; + } + var lastAddedDocumentId = doc.Descendants("last-added-document-id").FirstOrDefault()?.Value; if (lastAddedDocumentId.HasValue()) { diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index b47d2e61fe..318b070e42 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -574,6 +574,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Common.Voting", "Voting", "Abstimmung"); builder.AddOrUpdate("Common.Answer", "Answer", "Antwort"); + builder.AddOrUpdate("Common.Size", "Size", "Größe"); builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerFormFields.Description", "Manage form fields that are displayed during registration.", From f69f6e13325578f9fb7c15d6962c3f281a427cb2 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 13 Nov 2018 02:20:11 +0100 Subject: [PATCH 010/657] GetMetadata() util method for WebViewPage --- .../Theming/WebViewPage.cs | 41 +++++++++++++++++++ .../SmartStore.Web/Content/shared/_utils.scss | 21 ++++++++++ .../Views/Shared/EditorTemplates/Byte.cshtml | 22 ++++------ .../Shared/EditorTemplates/Decimal.cshtml | 22 ++++------ .../Shared/EditorTemplates/Double.cshtml | 22 ++++------ .../Views/Shared/EditorTemplates/Int32.cshtml | 22 ++++------ .../Views/Shared/EditorTemplates/Range.cshtml | 12 +++--- 7 files changed, 95 insertions(+), 67 deletions(-) diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs index b0404c4afe..6593d8fad7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs @@ -304,6 +304,47 @@ public LocalizationFileResolveResult ResolveLocalizationFile( { return _helper.LocalizationFileResolver.Resolve(culture, virtualPath, pattern, true, fallbackCulture); } + + public bool HasMetadata(string name) + { + return TryGetMetadata(name, out _); + } + + /// + /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary + /// + /// Actual type of value + /// Name of entry + /// Result + public T GetMetadata(string name) + { + TryGetMetadata(name, out var value); + return value; + } + + /// + /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary + /// + /// Actual type of value + /// Name of entry + /// true if the entry exists in any of the dictionaries, false otherwise + public bool TryGetMetadata(string name, out T value) + { + value = default(T); + + var exists = ViewData.TryGetValue(name, out var raw); + if (!exists) + { + exists = ViewData.ModelMetadata?.AdditionalValues?.TryGetValue(name, out raw) == true; + } + + if (raw != null) + { + value = raw.Convert(); + } + + return exists; + } } public abstract class WebViewPage : WebViewPage diff --git a/src/Presentation/SmartStore.Web/Content/shared/_utils.scss b/src/Presentation/SmartStore.Web/Content/shared/_utils.scss index f427e6919d..58ea82127d 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_utils.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_utils.scss @@ -240,6 +240,27 @@ table th { } +// +// Extra border utils +// ------------------------------------------------------ + +.border-translucent { + border-color: rgba(#000, 0.12) !important; +} + +.rounded-sm { + border-radius: $border-radius-sm !important; +} + +.rounded-lg { + border-radius: $border-radius-lg !important; +} + +.rounded-xl { + border-radius: $border-radius-lg * 2 !important; +} + + // // Some shims // ------------------------------------------------------ diff --git a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Byte.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Byte.cshtml index c7360886a3..e2c1ba5f6d 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Byte.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Byte.cshtml @@ -14,23 +14,15 @@ } } - private string Postfix - { - get - { - return ViewData["postfix"] as string; - } - } - private string CssClass { get { var cls = "numerictextbox-group flex-grow-1"; - if (ViewData.ContainsKey("size")) + if (TryGetMetadata("size", out var size)) { - cls += " numerictextbox-group-" + ViewData["size"].Convert(); + cls += " numerictextbox-group-" + size; } return cls; @@ -42,14 +34,14 @@ @(Html.Telerik().NumericTextBox() .Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)) .EmptyMessage(T("Common.EnterValue")) - .MinValue(0) - .MaxValue(255) - .IncrementStep(ViewData["step"].Convert() ?? 1) + .MinValue(GetMetadata("min") ?? 0) + .MaxValue(GetMetadata("max") ?? 255) + .IncrementStep(GetMetadata("step") ?? 1) .Value(Value) ) - @if (Postfix.HasValue()) + @if (TryGetMetadata("postfix", out var postfix)) { - @Postfix + @postfix } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Decimal.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Decimal.cshtml index efce4fc607..09129ae333 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Decimal.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Decimal.cshtml @@ -14,23 +14,15 @@ } } - private string Postfix - { - get - { - return ViewData["postfix"] as string; - } - } - private string CssClass { get { var cls = "numerictextbox-group flex-grow-1"; - if (ViewData.ContainsKey("size")) + if (TryGetMetadata("size", out var size)) { - cls += " numerictextbox-group-" + ViewData["size"].Convert(); + cls += " numerictextbox-group-" + size; } return cls; @@ -43,15 +35,15 @@ .Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)) .EmptyMessage(T("Common.EnterValue")) .Value(Value) - .MinValue(ViewData["min"].Convert()) - .MaxValue(ViewData["max"].Convert()) - .IncrementStep(ViewData["step"].Convert() ?? 1) + .MinValue(GetMetadata("min")) + .MaxValue(GetMetadata("max")) + .IncrementStep(GetMetadata("step") ?? 1) .DecimalDigits(4) //always display 4 digits ) - @if (Postfix.HasValue()) + @if (TryGetMetadata("postfix", out var postfix)) { - @Postfix + @postfix } diff --git a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Double.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Double.cshtml index 2396030733..e82db60a99 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Double.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Double.cshtml @@ -14,23 +14,15 @@ } } - private string Postfix - { - get - { - return ViewData["postfix"] as string; - } - } - private string CssClass { get { var cls = "numerictextbox-group flex-grow-1"; - if (ViewData.ContainsKey("size")) + if (TryGetMetadata("size", out var size)) { - cls += " numerictextbox-group-" + ViewData["size"].Convert(); + cls += " numerictextbox-group-" + size; } return cls; @@ -43,15 +35,15 @@ .Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)) .EmptyMessage(T("Common.EnterValue")) .Value(Value) - .MinValue(ViewData["min"].Convert()) - .MaxValue(ViewData["max"].Convert()) - .IncrementStep(ViewData["step"].Convert() ?? 1) + .MinValue(GetMetadata("min")) + .MaxValue(GetMetadata("max")) + .IncrementStep(GetMetadata("step") ?? 1) .DecimalDigits(4) //always display 4 digits ) - @if (Postfix.HasValue()) + @if (TryGetMetadata("postfix", out var postfix)) { - @Postfix + @postfix } diff --git a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Int32.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Int32.cshtml index 166826c1c8..091cb0dc19 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Int32.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Int32.cshtml @@ -14,23 +14,15 @@ } } - private string Postfix - { - get - { - return ViewData["postfix"] as string; - } - } - private string CssClass { get { var cls = "numerictextbox-group flex-grow-1"; - if (ViewData.ContainsKey("size")) + if (TryGetMetadata("size", out var size)) { - cls += " numerictextbox-group-" + ViewData["size"].Convert(); + cls += " numerictextbox-group-" + size; } return cls; @@ -42,14 +34,14 @@ @(Html.Telerik().IntegerTextBox() .Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)) .EmptyMessage(T("Common.EnterValue")) - .MinValue(ViewData["min"].Convert()) - .MaxValue(ViewData["max"].Convert()) - .IncrementStep(ViewData["step"].Convert() ?? 1) + .MinValue(GetMetadata("min")) + .MaxValue(GetMetadata("max")) + .IncrementStep(GetMetadata("step") ?? 1) .Value(Value) ) - @if (Postfix.HasValue()) + @if (TryGetMetadata("postfix", out var postfix)) { - @Postfix + @postfix } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Range.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Range.cshtml index ee7ccb2199..dd3630406f 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Range.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Range.cshtml @@ -23,13 +23,11 @@ var id = ViewData.TemplateInfo.GetFullHtmlFieldId(string.Empty); var name = ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty); - var metadataValues = ViewData.ModelMetadata.AdditionalValues; - - var min = (ViewData["min"].Convert() ?? metadataValues.Get("min").Convert() ?? 0m).ToString(CultureInfo.InvariantCulture); - var max = (ViewData["max"].Convert() ?? metadataValues.Get("max").Convert() ?? 100m).ToString(CultureInfo.InvariantCulture); - var step = (ViewData["step"].Convert() ?? metadataValues.Get("step").Convert() ?? 1m).ToString(CultureInfo.InvariantCulture); - var ticks = (ViewData["ticks"].Convert() ?? metadataValues.Get("ticks").Convert()).SplitSafe(",").Select(x => x.Trim()).ToArray(); - var format = ViewData["format"].Convert() ?? metadataValues.Get("format").Convert() ?? "{0}"; + var min = (GetMetadata("min") ?? 0m).ToString(CultureInfo.InvariantCulture); + var max = (GetMetadata("max") ?? 100m).ToString(CultureInfo.InvariantCulture); + var step = (GetMetadata("step") ?? 1m).ToString(CultureInfo.InvariantCulture); + var ticks = GetMetadata("ticks").SplitSafe(",").Select(x => x.Trim()).ToArray(); + var format = GetMetadata("format") ?? "{0}"; var invariantValue = ViewData.Model.Convert().ToString(CultureInfo.InvariantCulture); } From f90e4e031ed058fe5e4257f640c3bcc5bc595dad Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 13 Nov 2018 20:28:07 +0100 Subject: [PATCH 011/657] Fixed AOS css specificity --- .../Search/Catalog/CatalogSearchService.cs | 2 +- .../SmartStore.Web/Content/vendors/aos/scss/_core.scss | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs index 51ed0e2dbd..b790813c4a 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs @@ -165,7 +165,7 @@ public CatalogSearchResult Search( if (searchQuery.Take > 0) { - using (_services.Chronometer.Step(stepPrefix + "Count")) + using (_services.Chronometer.Step(stepPrefix + "Search")) { totalCount = searchEngine.Count(); // Fix paging boundaries diff --git a/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_core.scss b/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_core.scss index fc28ff789d..7094130ae6 100644 --- a/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_core.scss +++ b/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_core.scss @@ -3,6 +3,7 @@ // ---------------------------------- // Edit info (mc): // - Removed root [data-aos] +// - prepended 'body' to second selector because of correct specifity // - body[data-aos-duration='#{$i * 50}'] & >> body[data-aos-duration='#{$i * 50}'] [data-aos] // - 50ms steps > 100ms steps // - from 1 through 60 > 30 @@ -10,12 +11,12 @@ @for $i from 1 through 30 { body[data-aos-duration='#{$i * 100}'] [data-aos], - [data-aos][data-aos-duration='#{$i * 100}'] { + body [data-aos][data-aos-duration='#{$i * 100}'] { transition-duration: #{$i * 100}ms; } body[data-aos-delay='#{$i * 100}'] [data-aos], - [data-aos][data-aos-delay='#{$i * 100}'] { + body [data-aos][data-aos-delay='#{$i * 100}'] { transition-delay: 0s; &.aos-animate { From 838cf79ec1d500476b988f3ba53c262b8eb5bf81 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 13 Nov 2018 21:40:45 +0100 Subject: [PATCH 012/657] PrefetchUrlRecords should preload entries for more than one language (for faster fallback handling) --- .../Seo/IUrlRecordService.cs | 2 +- .../Seo/UrlRecordService.cs | 19 +++++++++++++------ .../Controllers/CatalogHelper.MapProduct.cs | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs index 732a73fad1..bc79cb22fc 100644 --- a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs @@ -78,7 +78,7 @@ public partial interface IUrlRecordService : IScopedService /// /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). /// - void PrefetchUrlRecords(string entityName, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false); + void PrefetchUrlRecords(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false); /// /// Prefetches a collection of url records properties for a range of entities in one go. diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index 0d13b0bdff..ea7d446964 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -184,9 +184,9 @@ public virtual IList GetUrlRecordsFor(string entityName, int entityId return query.ToList(); } - public virtual void PrefetchUrlRecords(string entityName, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false) + public virtual void PrefetchUrlRecords(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) { - var collection = GetUrlRecordCollectionInternal(entityName, languageId, entityIds, isRange, isSorted); + var collection = GetUrlRecordCollectionInternal(entityName, languageIds, entityIds, isRange, isSorted); if (_prefetchedCollections.TryGetValue(entityName, out var existing)) { @@ -200,10 +200,10 @@ public virtual void PrefetchUrlRecords(string entityName, int languageId, int[] public virtual UrlRecordCollection GetUrlRecordCollection(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false) { - return GetUrlRecordCollectionInternal(entityName, 0, entityIds, isRange, isSorted); + return GetUrlRecordCollectionInternal(entityName, null, entityIds, isRange, isSorted); } - public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityName, int? languageId, int[] entityIds, bool isRange = false, bool isSorted = false) + public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) { Guard.NotEmpty(entityName, nameof(entityName)); @@ -228,9 +228,16 @@ public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityN } } - if (languageId.HasValue) + if (languageIds != null && languageIds.Length > 0) { - query = query.Where(x => x.LanguageId == languageId.Value); + if (languageIds.Length == 1) + { + query = query.Where(x => x.LanguageId == languageIds[0]); + } + else + { + query = query.Where(x => languageIds.Contains(x.LanguageId)); + } } // Don't sort DESC, because latter items overwrite exisiting ones (it's the same as sorting DESC and taking the first) diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs index 123410c6b3..54c8b446cc 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs @@ -239,7 +239,7 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr if (prefetchSlugs) { - _urlRecordService.PrefetchUrlRecords(nameof(Product), language.Id, allProductIds); + _urlRecordService.PrefetchUrlRecords(nameof(Product), new[] { language.Id, 0 }, allProductIds); } if (prefetchTranslations) From 413f5a0eaf40b9580998a3f3ad87351140d73a2c Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 14 Nov 2018 15:55:31 +0100 Subject: [PATCH 013/657] Export: Using new property collection for translations (part 1) --- .../DataExchange/Export/DataExporter.cs | 101 +++++-- .../Export/DynamicEntityHelper.cs | 256 ++++++++++++------ .../Export/Internal/DataExporterContext.cs | 18 +- 3 files changed, 264 insertions(+), 111 deletions(-) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index f2bfd14a9e..0a86a82b1f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -43,6 +43,7 @@ using SmartStore.Utilities; using SmartStore.Utilities.Threading; using SmartStore.Collections; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.DataExchange.Export { @@ -57,7 +58,7 @@ public partial class DataExporter : IDataExporter private readonly HttpContextBase _httpContext; private readonly Lazy _priceFormatter; private readonly Lazy _exportProfileService; - private readonly Lazy _localizedEntityService; + private readonly ILocalizedEntityService _localizedEntityService; private readonly Lazy _languageService; private readonly Lazy _urlRecordService; private readonly Lazy _pictureService; @@ -105,7 +106,7 @@ public DataExporter( HttpContextBase httpContext, Lazy priceFormatter, Lazy exportProfileService, - Lazy localizedEntityService, + ILocalizedEntityService localizedEntityService, Lazy languageService, Lazy urlRecordService, Lazy pictureService, @@ -197,11 +198,22 @@ public DataExporter( public Localizer T { get; set; } - #endregion + #endregion + + #region Utilities + + private LocalizedPropertyCollection CreateTranslationCollection(string keyGroup, IEnumerable entities) + { + if (entities == null || !entities.Any()) + { + return new LocalizedPropertyCollection(keyGroup, null, Enumerable.Empty()); + } - #region Utilities + var collection = _localizedEntityService.GetLocalizedPropertyCollection(keyGroup, entities.Select(x => x.Id).Distinct().ToArray()); + return collection; + } - private void SetProgress(DataExporterContext ctx, int loadedRecords) + private void SetProgress(DataExporterContext ctx, int loadedRecords) { try { @@ -313,7 +325,10 @@ private void DetachAllEntitiesAndClear(DataExporterContext ctx) { try { - if (ctx.ProductExportContext != null) + ctx.AssociatedProductContext?.Clear(); + ctx.TranslationsPerPage?.Clear(); + + if (ctx.ProductExportContext != null) { _dbContext.DetachEntities(x => { @@ -325,8 +340,6 @@ x is Picture || x is ProductBundleItem || x is ProductCategory || x is ProductMa ctx.ProductExportContext.Clear(); } - ctx.AssociatedProductContext?.Clear(); - if (ctx.OrderExportContext != null) { _dbContext.DetachEntities(x => @@ -386,7 +399,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in { var offset = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); var limit = Math.Max(ctx.Request.Profile.Limit, 0); - var recordsPerSegment = (ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0)); + var recordsPerSegment = ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0); var totalCount = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; switch (ctx.Request.Provider.Value.EntityType) @@ -397,15 +410,30 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in skip => GetProducts(ctx, skip), entities => { - // Load data behind navigation properties for current queue in one go. - ctx.ProductExportContext = CreateProductExportContext(entities, ctx.ContextCustomer, ctx.Store.Id); + // Load data behind navigation properties for current queue in one go. + ctx.ProductExportContext = CreateProductExportContext(entities, ctx.ContextCustomer, ctx.Store.Id); ctx.AssociatedProductContext = null; + + var context = ctx.ProductExportContext; if (!ctx.Projection.NoGroupedProducts && entities.Where(x => x.ProductType == ProductType.GroupedProduct).Any()) { - ctx.ProductExportContext.AssociatedProducts.LoadAll(); - var associatedProducts = ctx.ProductExportContext.AssociatedProducts.SelectMany(x => x.Value); + context.AssociatedProducts.LoadAll(); + var associatedProducts = context.AssociatedProducts.SelectMany(x => x.Value); ctx.AssociatedProductContext = CreateProductExportContext(associatedProducts, ctx.ContextCustomer, ctx.Store.Id); + + ctx.Translations[nameof(Product)] = CreateTranslationCollection(nameof(Product), entities.Where(x => x.ProductType != ProductType.GroupedProduct).Concat(associatedProducts)); + } + else + { + ctx.Translations[nameof(Product)] = CreateTranslationCollection(nameof(Product), entities); } + + context.ProductTags.LoadAll(); + context.ProductBundleItems.LoadAll(); + + ctx.TranslationsPerPage[nameof(ProductTag)] = CreateTranslationCollection(nameof(ProductTag), context.ProductTags.SelectMany(x => x.Value)); + ctx.TranslationsPerPage[nameof(ProductBundleItem)] = CreateTranslationCollection(nameof(ProductBundleItem), context.ProductBundleItems.SelectMany(x => x.Value)); + }, entity => Convert(ctx, entity), offset, PageSize, limit, recordsPerSegment, totalCount @@ -1257,7 +1285,8 @@ private List GetShoppingCartItems(DataExporterContext ctx, int private List Init(DataExporterContext ctx, int? totalRecords = null) { - // Init base things that are even required for preview. Init all other things (regular export) in ExportCoreOuter. + // Init things that are required for export and for preview. + // Init things that are only required for export in ExportCoreOuter. List result = null; ctx.ContextCurrency = _currencyService.Value.GetCurrencyById(ctx.Projection.CurrencyId ?? 0) ?? _services.WorkContext.WorkingCurrency; @@ -1267,20 +1296,40 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) ctx.Stores = _services.StoreService.GetAllStores().ToDictionary(x => x.Id, x => x); ctx.Languages = _languageService.Value.GetAllLanguages(true).ToDictionary(x => x.Id, x => x); - if (!ctx.IsPreview && ctx.Request.Profile.PerStore) + if (ctx.IsPreview) + { + ctx.Translations[nameof(Currency)] = new LocalizedPropertyCollection(nameof(Currency), null, Enumerable.Empty()); + ctx.Translations[nameof(Country)] = new LocalizedPropertyCollection(nameof(Country), null, Enumerable.Empty()); + ctx.Translations[nameof(StateProvince)] = new LocalizedPropertyCollection(nameof(StateProvince), null, Enumerable.Empty()); + ctx.Translations[nameof(DeliveryTime)] = new LocalizedPropertyCollection(nameof(DeliveryTime), null, Enumerable.Empty()); + ctx.Translations[nameof(QuantityUnit)] = new LocalizedPropertyCollection(nameof(QuantityUnit), null, Enumerable.Empty()); + ctx.Translations[nameof(Manufacturer)] = new LocalizedPropertyCollection(nameof(Manufacturer), null, Enumerable.Empty()); + ctx.Translations[nameof(Category)] = new LocalizedPropertyCollection(nameof(Category), null, Enumerable.Empty()); + } + else + { + ctx.Translations[nameof(Currency)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Currency), null); + ctx.Translations[nameof(Country)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Country), null); + ctx.Translations[nameof(StateProvince)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(StateProvince), null); + ctx.Translations[nameof(DeliveryTime)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(DeliveryTime), null); + ctx.Translations[nameof(QuantityUnit)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(QuantityUnit), null); + ctx.Translations[nameof(Manufacturer)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Manufacturer), null); + ctx.Translations[nameof(Category)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Category), null); + } + + if (!ctx.IsPreview && ctx.Request.Profile.PerStore) { result = new List(ctx.Stores.Values.Where(x => x.Id == ctx.Filter.StoreId || ctx.Filter.StoreId == 0)); } else { - int? storeId = (ctx.Filter.StoreId == 0 ? ctx.Projection.StoreId : ctx.Filter.StoreId); - + int? storeId = ctx.Filter.StoreId == 0 ? ctx.Projection.StoreId : ctx.Filter.StoreId; ctx.Store = ctx.Stores.Values.FirstOrDefault(x => x.Id == (storeId ?? _services.StoreContext.CurrentStore.Id)); result = new List { ctx.Store }; } - // get total records for progress + // Get total records for progress. foreach (var store in result) { ctx.Store = store; @@ -1289,7 +1338,8 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) if (totalRecords.HasValue) { - totalCount = totalRecords.Value; // speed up preview by not counting total at each page + // Speed up preview by not counting total at each page. + totalCount = totalRecords.Value; } else { @@ -1429,7 +1479,7 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) ctx.EntityIdsPerSegment.Clear(); DetachAllEntitiesAndClear(ctx); - _localizedEntityService.Value.ClearCache(); + _localizedEntityService.ClearCache(); if (context.IsMaxFailures) { @@ -1524,16 +1574,16 @@ private void ExportCoreOuter(DataExporterContext ctx) } } - // lazyLoading: false, proxyCreation: false impossible. how to identify all properties of all data levels of all entities - // that require manual resolving for now and for future? fragile, susceptible to faults (e.g. price calculation)... - using (var scope = new DbContextScope(_dbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) + // lazyLoading: false, proxyCreation: false impossible due to price calculation. + using (var scope = new DbContextScope(_dbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) { + // Init things required for export and not required for preview. ctx.DeliveryTimes = _deliveryTimeService.Value.GetAllDeliveryTimes().ToDictionary(x => x.Id); ctx.QuantityUnits = _quantityUnitService.Value.GetAllQuantityUnits().ToDictionary(x => x.Id); ctx.ProductTemplates = _productTemplateService.Value.GetAllProductTemplates().ToDictionary(x => x.Id, x => x.ViewPath); ctx.CategoryTemplates = _categoryTemplateService.Value.GetAllCategoryTemplates().ToDictionary(x => x.Id, x => x.ViewPath); - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product || + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product || ctx.Request.Provider.Value.EntityType == ExportEntityType.Order) { ctx.Countries = _countryService.Value.GetAllCountries(true).ToDictionary(x => x.Id, x => x); @@ -1612,7 +1662,7 @@ private void ExportCoreOuter(DataExporterContext ctx) } DetachAllEntitiesAndClear(ctx); - _localizedEntityService.Value.ClearCache(); + _localizedEntityService.ClearCache(); try { @@ -1624,6 +1674,7 @@ private void ExportCoreOuter(DataExporterContext ctx) ctx.QuantityUnits.Clear(); ctx.DeliveryTimes.Clear(); ctx.Stores.Clear(); + ctx.Translations.Clear(); ctx.Request.CustomData.Clear(); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 24c7250399..611abc4099 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -254,7 +254,7 @@ private List GetLocalized(DataExporterContext ctx, T entity, params var member = keySelector.Body as MemberExpression; var propInfo = member.Member as PropertyInfo; string localeKey = propInfo.Name; - var value = _localizedEntityService.Value.GetLocalizedValue(language.Value.Id, entity.Id, localeKeyGroup, localeKey); + var value = _localizedEntityService.GetLocalizedValue(language.Value.Id, entity.Id, localeKeyGroup, localeKey); // we better not export empty values. the risk is to high that they are imported and unnecessary fill databases. if (value.HasValue()) @@ -272,7 +272,68 @@ private List GetLocalized(DataExporterContext ctx, T entity, params return (localized.Count == 0 ? null : localized); } - private dynamic ToDynamic(DataExporterContext ctx, ExportProfile profile) + private List GetLocalized( + DataExporterContext ctx, + LocalizedPropertyCollection values, + T entity, + params Expression>[] keySelectors) + where T : BaseEntity, ILocalizedEntity + { + Guard.NotNull(values, nameof(values)); + + if (ctx.Languages.Count <= 1) + { + return null; + } + + var localized = new List(); + var localeKeyGroup = typeof(T).Name; + var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); + + foreach (var language in ctx.Languages) + { + var languageCulture = language.Value.LanguageCulture.EmptyNull().ToLower(); + + // Add SEO name. + if (isSlugSupported) + { + var value = _urlRecordService.Value.GetActiveSlug(entity.Id, localeKeyGroup, language.Value.Id); + if (value.HasValue()) + { + dynamic exp = new HybridExpando(); + exp.Culture = languageCulture; + exp.LocaleKey = "SeName"; + exp.LocaleValue = value; + + localized.Add(exp); + } + } + + // Add localized property value. + foreach (var keySelector in keySelectors) + { + var member = keySelector.Body as MemberExpression; + var propInfo = member.Member as PropertyInfo; + string localeKey = propInfo.Name; + var value = values.GetValue(language.Value.Id, entity.Id, localeKey); + + // We do not export empty values to not fill databases with it. + if (value.HasValue()) + { + dynamic exp = new HybridExpando(); + exp.Culture = languageCulture; + exp.LocaleKey = localeKey; + exp.LocaleValue = value; + + localized.Add(exp); + } + } + } + + return localized.Any() ? localized : null; + } + + private dynamic ToDynamic(DataExporterContext ctx, ExportProfile profile) { if (profile == null) return null; @@ -283,21 +344,26 @@ private dynamic ToDynamic(DataExporterContext ctx, ExportProfile profile) private dynamic ToDynamic(DataExporterContext ctx, Currency currency) { - if (currency == null) - return null; + if (currency == null) + { + return null; + } dynamic result = new DynamicEntity(currency); + var translations = ctx.Translations[nameof(Currency)]; - result.Name = currency.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result._Localized = GetLocalized(ctx, currency, x => x.Name); + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, currency.Id, nameof(currency.Name)) ?? currency.Name; + result._Localized = GetLocalized(ctx, translations, currency, x => x.Name); return result; } private dynamic ToDynamic(DataExporterContext ctx, Language language) { - if (language == null) - return null; + if (language == null) + { + return null; + } dynamic result = new DynamicEntity(language); return result; @@ -305,32 +371,38 @@ private dynamic ToDynamic(DataExporterContext ctx, Language language) private dynamic ToDynamic(DataExporterContext ctx, Country country) { - if (country == null) - return null; + if (country == null) + { + return null; + } dynamic result = new DynamicEntity(country); + var translations = ctx.Translations[nameof(Country)]; - result.Name = country.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result._Localized = GetLocalized(ctx, country, x => x.Name); + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, country.Id, nameof(country.Name)) ?? country.Name; + result._Localized = GetLocalized(ctx, translations, country, x => x.Name); return result; } private dynamic ToDynamic(DataExporterContext ctx, Address address) { - if (address == null) - return null; + if (address == null) + { + return null; + } dynamic result = new DynamicEntity(address); - result.Country = ToDynamic(ctx, address.Country); + result.Country = ToDynamic(ctx, address.Country); if (address.StateProvinceId.GetValueOrDefault() > 0) { dynamic sp = new DynamicEntity(address.StateProvince); + var translations = ctx.Translations[nameof(StateProvince)]; - sp.Name = address.StateProvince.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - sp._Localized = GetLocalized(ctx, address.StateProvince, x => x.Name); + sp.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, address.StateProvince.Id, nameof(StateProvince)) ?? address.StateProvince.Name; + sp._Localized = GetLocalized(ctx, translations, address.StateProvince, x => x.Name); result.StateProvince = sp; } @@ -344,18 +416,21 @@ private dynamic ToDynamic(DataExporterContext ctx, Address address) private dynamic ToDynamic(DataExporterContext ctx, RewardPointsHistory points) { - if (points == null) - return null; + if (points == null) + { + return null; + } dynamic result = new DynamicEntity(points); - return result; } private dynamic ToDynamic(DataExporterContext ctx, Customer customer) { - if (customer == null) - return null; + if (customer == null) + { + return null; + } dynamic result = new DynamicEntity(customer); @@ -385,8 +460,10 @@ private dynamic ToDynamic(DataExporterContext ctx, Customer customer) private dynamic ToDynamic(DataExporterContext ctx, Store store) { - if (store == null) - return null; + if (store == null) + { + return null; + } dynamic result = new DynamicEntity(store); @@ -398,13 +475,16 @@ private dynamic ToDynamic(DataExporterContext ctx, Store store) private dynamic ToDynamic(DataExporterContext ctx, DeliveryTime deliveryTime) { - if (deliveryTime == null) - return null; + if (deliveryTime == null) + { + return null; + } dynamic result = new DynamicEntity(deliveryTime); + var translations = ctx.Translations[nameof(DeliveryTime)]; - result.Name = deliveryTime.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result._Localized = GetLocalized(ctx, deliveryTime, x => x.Name); + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, deliveryTime.Id, nameof(deliveryTime.Name)) ?? deliveryTime.Name; + result._Localized = GetLocalized(ctx, translations, deliveryTime, x => x.Name); return result; } @@ -413,24 +493,26 @@ private void ToDeliveryTime(DataExporterContext ctx, dynamic parent, int? delive { if (ctx.DeliveryTimes != null) { - if (deliveryTimeId.HasValue && ctx.DeliveryTimes.ContainsKey(deliveryTimeId.Value)) - parent.DeliveryTime = ToDynamic(ctx, ctx.DeliveryTimes[deliveryTimeId.Value]); - else - parent.DeliveryTime = null; + parent.DeliveryTime = deliveryTimeId.HasValue && ctx.DeliveryTimes.ContainsKey(deliveryTimeId.Value) + ? ToDynamic(ctx, ctx.DeliveryTimes[deliveryTimeId.Value]) + : null; } } private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) { - if (quantityUnit == null) - return null; + if (quantityUnit == null) + { + return null; + } dynamic result = new DynamicEntity(quantityUnit); + var translations = ctx.Translations[nameof(QuantityUnit)]; - result.Name = quantityUnit.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result.Description = quantityUnit.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, quantityUnit.Id, nameof(quantityUnit.Name)) ?? quantityUnit.Name; + result.Description = translations.GetValue(ctx.Projection.LanguageId ?? 0, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; - result._Localized = GetLocalized(ctx, quantityUnit, + result._Localized = GetLocalized(ctx, translations, quantityUnit, x => x.Name, x => x.Description); @@ -441,20 +523,20 @@ private void ToQuantityUnit(DataExporterContext ctx, dynamic parent, int? quanti { if (ctx.QuantityUnits != null) { - if (quantityUnitId.HasValue && ctx.QuantityUnits.ContainsKey(quantityUnitId.Value)) - parent.QuantityUnit = ToDynamic(ctx, ctx.QuantityUnits[quantityUnitId.Value]); - else - parent.QuantityUnit = null; + parent.QuantityUnit = quantityUnitId.HasValue && ctx.QuantityUnits.ContainsKey(quantityUnitId.Value) + ? ToDynamic(ctx, ctx.QuantityUnits[quantityUnitId.Value]) + : null; } } private dynamic ToDynamic(DataExporterContext ctx, Picture picture, int thumbPictureSize, int detailsPictureSize) { - if (picture == null) - return null; + if (picture == null) + { + return null; + } // TODO: (mc) Refactor > GetPictureInfo - dynamic result = new DynamicEntity(picture); var pictureInfo = _pictureService.Value.GetPictureInfo(picture); var host = _services.StoreService.GetHost(ctx.Store); @@ -466,8 +548,6 @@ private dynamic ToDynamic(DataExporterContext ctx, Picture picture, int thumbPic result._ThumbImageUrl = _pictureService.Value.GetUrl(pictureInfo, thumbPictureSize, FallbackPictureType.NoFallback, host); result._ImageUrl = _pictureService.Value.GetUrl(pictureInfo, detailsPictureSize, FallbackPictureType.NoFallback, host); result._FullSizeImageUrl = _pictureService.Value.GetUrl(pictureInfo, 0, FallbackPictureType.NoFallback, host); - - //result._ThumbLocalPath = _pictureService.Value.GetThumbLocalPath(picture); } return result; @@ -522,23 +602,26 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttributeCombin private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) { - if (manufacturer == null) - return null; + if (manufacturer == null) + { + return null; + } dynamic result = new DynamicEntity(manufacturer); + var translations = ctx.Translations[nameof(Manufacturer)]; - result.Picture = null; - result.Name = manufacturer.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result.Picture = null; + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; if (!ctx.IsPreview) { result.SeName = manufacturer.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - result.Description = manufacturer.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaKeywords = manufacturer.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaDescription = manufacturer.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaTitle = manufacturer.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); + result.Description = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.Description)) ?? manufacturer.Description; + result.MetaKeywords = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.MetaKeywords)) ?? manufacturer.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.MetaDescription)) ?? manufacturer.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.MetaTitle)) ?? manufacturer.MetaTitle; - result._Localized = GetLocalized(ctx, manufacturer, + result._Localized = GetLocalized(ctx, translations, manufacturer, x => x.Name, x => x.Description, x => x.MetaKeywords, @@ -551,29 +634,32 @@ private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) private dynamic ToDynamic(DataExporterContext ctx, Category category) { - if (category == null) - return null; + if (category == null) + { + return null; + } dynamic result = new DynamicEntity(category); + var translations = ctx.Translations[nameof(Category)]; result.Picture = null; - result.Name = category.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result.FullName = category.GetLocalized(x => x.FullName, ctx.Projection.LanguageId ?? 0, true, false); + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.Name)) ?? category.Name; + result.FullName = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.FullName)) ?? category.FullName; if (!ctx.IsPreview) { result.SeName = category.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - result.Description = category.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); - result.BottomDescription = category.GetLocalized(x => x.BottomDescription, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaKeywords = category.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaDescription = category.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaTitle = category.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); + result.Description = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.Description)) ?? category.Description; + result.BottomDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.BottomDescription)) ?? category.BottomDescription; + result.MetaKeywords = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.MetaKeywords)) ?? category.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.MetaDescription)) ?? category.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.MetaTitle)) ?? category.MetaTitle; result._CategoryTemplateViewPath = ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId) ? ctx.CategoryTemplates[category.CategoryTemplateId] : ""; - result._Localized = GetLocalized(ctx, category, + result._Localized = GetLocalized(ctx, translations, category, x => x.Name, x => x.FullName, x => x.Description, @@ -588,12 +674,15 @@ private dynamic ToDynamic(DataExporterContext ctx, Category category) private dynamic ToDynamic(DataExporterContext ctx, Product product, string seName = null) { - if (product == null) - return null; + if (product == null) + { + return null; + } dynamic result = new DynamicEntity(product); + var translations = ctx.Translations[nameof(Product)]; - result.AppliedDiscounts = null; + result.AppliedDiscounts = null; result.Downloads = null; result.TierPrices = null; result.ProductAttributes = null; @@ -605,17 +694,17 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam result.ProductSpecificationAttributes = null; result.ProductBundleItems = null; - result.Name = product.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.Name)) ?? product.Name; if (!ctx.IsPreview) { result.SeName = seName ?? product.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - result.ShortDescription = product.GetLocalized(x => x.ShortDescription, ctx.Projection.LanguageId ?? 0, true, false); - result.FullDescription = product.GetLocalized(x => x.FullDescription, ctx.Projection.LanguageId ?? 0, true, false, true); - result.MetaKeywords = product.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaDescription = product.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); - result.MetaTitle = product.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); - result.BundleTitleText = product.GetLocalized(x => x.BundleTitleText, ctx.Projection.LanguageId ?? 0, true, false); + result.ShortDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.ShortDescription)) ?? product.ShortDescription; + result.FullDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.FullDescription)) ?? product.FullDescription; + result.MetaKeywords = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.MetaKeywords)) ?? product.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.MetaDescription)) ?? product.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.MetaTitle)) ?? product.MetaTitle; + result.BundleTitleText = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.BundleTitleText)) ?? product.BundleTitleText; result._ProductTemplateViewPath = ctx.ProductTemplates.ContainsKey(product.ProductTemplateId) ? ctx.ProductTemplates[product.ProductTemplateId] @@ -627,7 +716,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam ToDeliveryTime(ctx, result, product.DeliveryTimeId); ToQuantityUnit(ctx, result, product.QuantityUnitId); - result._Localized = GetLocalized(ctx, product, + result._Localized = GetLocalized(ctx, translations, product, x => x.Name, x => x.ShortDescription, x => x.FullDescription, @@ -832,7 +921,6 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen .Select(x => { dynamic dyn = new DynamicEntity(x); - return dyn; }) .ToList(); @@ -860,10 +948,11 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen .Select(x => { dynamic dyn = new DynamicEntity(x); + var translations = ctx.TranslationsPerPage[nameof(ProductTag)]; - dyn.Name = x.GetLocalized(y => y.Name, languageId, true, false); + dyn.Name = translations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; dyn.SeName = x.GetSeName(languageId); - dyn._Localized = GetLocalized(ctx, x, y => y.Name); + dyn._Localized = GetLocalized(ctx, translations, x, y => y.Name); return dyn; }) @@ -881,10 +970,11 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen .Select(x => { dynamic dyn = new DynamicEntity(x); + var translations = ctx.TranslationsPerPage[nameof(ProductBundleItem)]; - dyn.Name = x.GetLocalized(y => y.Name, languageId, true, false); - dyn.ShortDescription = x.GetLocalized(y => y.ShortDescription, languageId, true, false); - dyn._Localized = GetLocalized(ctx, x, y => y.Name, y => y.ShortDescription); + dyn.Name = translations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; + dyn.ShortDescription = translations.GetValue(languageId, x.Id, nameof(x.ShortDescription)) ?? x.ShortDescription; + dyn._Localized = GetLocalized(ctx, translations, x, y => y.Name, y => y.ShortDescription); return dyn; }) @@ -942,7 +1032,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen if (ctx.Supports(ExportFeatures.OffersShippingTimeFallback)) { dynamic deliveryTime = dynObject.DeliveryTime; - dynObject._ShippingTime = (deliveryTime == null ? ctx.Projection.ShippingTime : deliveryTime.Name); + dynObject._ShippingTime = deliveryTime == null ? ctx.Projection.ShippingTime : deliveryTime.Name; } if (ctx.Supports(ExportFeatures.OffersShippingCostsFallback)) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index b3384c5807..0c351245eb 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -39,6 +39,8 @@ public DataExporterContext( ProductTemplates = new Dictionary(); CategoryTemplates = new Dictionary(); NewsletterSubscriptions = new HashSet(); + Translations = new Dictionary(); + TranslationsPerPage = new Dictionary(); RecordsPerStore = new Dictionary(); EntityIdsLoaded = new List(); @@ -116,15 +118,25 @@ public bool IsFileBasedExport public Dictionary CategoryTemplates { get; set; } public HashSet NewsletterSubscriptions { get; set; } - // Data loaded once per page. - public ProductExportContext ProductExportContext { get; set; } + /// + /// All translations for global scopes (like Category, Manufacturer etc.) + /// + public Dictionary Translations { get; set; } + + // Data loaded once per page. + public ProductExportContext ProductExportContext { get; set; } public ProductExportContext AssociatedProductContext { get; set; } public OrderExportContext OrderExportContext { get; set; } public ManufacturerExportContext ManufacturerExportContext { get; set; } public CategoryExportContext CategoryExportContext { get; set; } public CustomerExportContext CustomerExportContext { get; set; } - public ExportExecuteContext ExecuteContext { get; set; } + /// + /// All per page translations (like ProductVariantAttributeValue etc.) + /// + public Dictionary TranslationsPerPage { get; set; } + + public ExportExecuteContext ExecuteContext { get; set; } public DataExportResult Result { get; set; } } } From 2c57fa5574e9f3d926364ba499bf226b26945223 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 14 Nov 2018 17:11:59 +0100 Subject: [PATCH 014/657] Fixes "Count must be a DbConstantExpression or a DbParameterReferenceExpression. Parameter name: count" --- .../Search/Catalog/LinqCatalogSearchService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs index cdd2cac9ec..aee829e7f3 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs @@ -766,8 +766,10 @@ public CatalogSearchResult Search(CatalogSearchQuery searchQuery, ProductLoadFla if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) { - query = query - .Skip(() => searchQuery.PageIndex * searchQuery.Take) + var skip = searchQuery.PageIndex * searchQuery.Take; + + query = query + .Skip(() => skip) .Take(() => searchQuery.Take); var ids = query.Select(x => x.Id).ToArray(); From 9cdaf590846d7f27e61548d7a9947278be29e8c4 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 14 Nov 2018 18:22:19 +0100 Subject: [PATCH 015/657] More fixes for "Count must be a DbConstantExpression or a DbParameterReferenceExpression. Parameter name: count" --- .../Search/Forum/LinqForumSearchService.cs | 6 ++++-- .../SmartStore.Web/Controllers/EntityController.cs | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs b/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs index 2e4badca21..cce742613e 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs @@ -328,7 +328,8 @@ protected virtual IDictionary GetFacets(ForumSearchQuery sea // Limit the result. Do not allow to get all customers. var maxChoices = descriptor.MaxChoicesCount > 0 ? descriptor.MaxChoicesCount : 20; - var customers = customerQuery.Take(() => maxChoices * 3).ToList(); + var take = maxChoices * 3; + var customers = customerQuery.Take(() => take).ToList(); foreach (var customer in customers) { @@ -400,8 +401,9 @@ public ForumSearchResult Search(ForumSearchQuery searchQuery, bool direct = fals if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) { + var skip = searchQuery.PageIndex * searchQuery.Take; query = query - .Skip(() => searchQuery.PageIndex * searchQuery.Take) + .Skip(() => skip) .Take(() => searchQuery.Take); var ids = query.Select(x => x.Id).ToArray(); diff --git a/src/Presentation/SmartStore.Web/Controllers/EntityController.cs b/src/Presentation/SmartStore.Web/Controllers/EntityController.cs index 5c21b84b7a..539ca5080e 100644 --- a/src/Presentation/SmartStore.Web/Controllers/EntityController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/EntityController.cs @@ -157,9 +157,10 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) { searchQuery = searchQuery.WithCategoryIds(null, node.Flatten(true).Select(x => x.Id).ToArray()); } - } + } - var query = _catalogSearchService.PrepareQuery(searchQuery); + var skip = model.PageIndex * model.PageSize; + var query = _catalogSearchService.PrepareQuery(searchQuery); var products = query .Select(x => new @@ -172,7 +173,7 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) x.MainPictureId }) .OrderBy(x => x.Name) - .Skip(() => model.PageIndex * model.PageSize) + .Skip(() => skip) .Take(() => model.PageSize) .ToList(); From 71ffa6cdc0abd05dc3dbd531228fbb5324617b36 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 14 Nov 2018 19:18:15 +0100 Subject: [PATCH 016/657] Export: Using new property collection for translations (part 2) --- .../DataExchange/Export/DataExporter.cs | 22 +- .../Export/DynamicEntityHelper.cs | 220 ++++++++---------- 2 files changed, 115 insertions(+), 127 deletions(-) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 0a86a82b1f..cfa5efd919 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -421,19 +421,33 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in var associatedProducts = context.AssociatedProducts.SelectMany(x => x.Value); ctx.AssociatedProductContext = CreateProductExportContext(associatedProducts, ctx.ContextCustomer, ctx.Store.Id); - ctx.Translations[nameof(Product)] = CreateTranslationCollection(nameof(Product), entities.Where(x => x.ProductType != ProductType.GroupedProduct).Concat(associatedProducts)); + var translationEntities = entities.Where(x => x.ProductType != ProductType.GroupedProduct).Concat(associatedProducts); + ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), translationEntities); } else { - ctx.Translations[nameof(Product)] = CreateTranslationCollection(nameof(Product), entities); + ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), entities); } context.ProductTags.LoadAll(); context.ProductBundleItems.LoadAll(); + context.SpecificationAttributes.LoadAll(); + context.Attributes.LoadAll(); + + var psa = context.SpecificationAttributes.SelectMany(x => x.Value); + var sao = psa.Select(x => x.SpecificationAttributeOption); + var sa = psa.Select(x => x.SpecificationAttributeOption.SpecificationAttribute); + + var pva = context.Attributes.SelectMany(x => x.Value); + var pvav = pva.SelectMany(x => x.ProductVariantAttributeValues); + var pa = pva.Select(x => x.ProductAttribute); ctx.TranslationsPerPage[nameof(ProductTag)] = CreateTranslationCollection(nameof(ProductTag), context.ProductTags.SelectMany(x => x.Value)); ctx.TranslationsPerPage[nameof(ProductBundleItem)] = CreateTranslationCollection(nameof(ProductBundleItem), context.ProductBundleItems.SelectMany(x => x.Value)); - + ctx.TranslationsPerPage[nameof(SpecificationAttribute)] = CreateTranslationCollection(nameof(SpecificationAttribute), sa); + ctx.TranslationsPerPage[nameof(SpecificationAttributeOption)] = CreateTranslationCollection(nameof(SpecificationAttributeOption), sao); + ctx.TranslationsPerPage[nameof(ProductAttribute)] = CreateTranslationCollection(nameof(ProductAttribute), pa); + ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)] = CreateTranslationCollection(nameof(ProductVariantAttributeValue), pvav); }, entity => Convert(ctx, entity), offset, PageSize, limit, recordsPerSegment, totalCount @@ -1479,7 +1493,6 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) ctx.EntityIdsPerSegment.Clear(); DetachAllEntitiesAndClear(ctx); - _localizedEntityService.ClearCache(); if (context.IsMaxFailures) { @@ -1662,7 +1675,6 @@ private void ExportCoreOuter(DataExporterContext ctx) } DetachAllEntitiesAndClear(ctx); - _localizedEntityService.ClearCache(); try { diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 611abc4099..201819554e 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -25,16 +25,16 @@ using SmartStore.Core.Html; using SmartStore.Services.Catalog; using SmartStore.Services.Catalog.Modelling; +using SmartStore.Services.Customers; using SmartStore.Services.DataExchange.Export.Events; using SmartStore.Services.DataExchange.Export.Internal; using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Seo; -using SmartStore.Services.Customers; namespace SmartStore.Services.DataExchange.Export { - public partial class DataExporter + public partial class DataExporter { private readonly string[] _orderCustomerAttributes = new string[] { @@ -49,13 +49,13 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec { try { - var languageId = (ctx.Projection.LanguageId ?? 0); + var languageId = ctx.Projection.LanguageId ?? 0; string description = ""; - // description merging + // Description merging. if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.None) { - // export empty description + // Export empty description. } else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty) { @@ -87,37 +87,38 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec { var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); - if (productManus != null && productManus.Any()) - description = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); + if (productManus != null && productManus.Any()) + { + var translations = ctx.Translations[nameof(Manufacturer)]; + var manufacturer = productManus.First().Manufacturer; + description = translations.GetValue(languageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; + } description = description.Grow((string)dynObject.Name, " "); - - if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription) - description = description.Grow((string)dynObject.ShortDescription, " "); - else - description = description.Grow((string)dynObject.FullDescription, " "); + description = ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription + ? description.Grow((string)dynObject.ShortDescription, " ") + : description.Grow((string)dynObject.FullDescription, " "); } - // append text + // Append text. if (ctx.Projection.AppendDescriptionText.HasValue() && ((string)dynObject.ShortDescription).IsEmpty() && ((string)dynObject.FullDescription).IsEmpty()) { string[] appendText = ctx.Projection.AppendDescriptionText.SplitSafe(","); if (appendText.Length > 0) { - var rnd = (new Random()).Next(0, appendText.Length - 1); - + var rnd = new Random().Next(0, appendText.Length - 1); description = description.Grow(appendText.SafeGet(rnd), " "); } } - // remove critical characters + // Remove critical characters. if (description.HasValue() && ctx.Projection.RemoveCriticalCharacters) { foreach (var str in ctx.Projection.CriticalCharacters.SplitSafe(",")) description = description.Replace(str, ""); } - // convert to plain text + // Convert to plain text. if (description.HasValue() && ctx.Projection.DescriptionToPlainText) { //Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); @@ -219,59 +220,6 @@ private decimal CalculatePrice( return ConvertPrice(ctx, product, price) ?? price; } - private List GetLocalized(DataExporterContext ctx, T entity, params Expression>[] keySelectors) - where T : BaseEntity, ILocalizedEntity - { - if (ctx.Languages.Count <= 1) - return null; - - var localized = new List(); - - var localeKeyGroup = typeof(T).Name; - var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); - - foreach (var language in ctx.Languages) - { - var languageCulture = language.Value.LanguageCulture.EmptyNull().ToLower(); - - // add SeName - if (isSlugSupported) - { - var value = _urlRecordService.Value.GetActiveSlug(entity.Id, localeKeyGroup, language.Value.Id); - if (value.HasValue()) - { - dynamic exp = new HybridExpando(); - exp.Culture = languageCulture; - exp.LocaleKey = "SeName"; - exp.LocaleValue = value; - - localized.Add(exp); - } - } - - foreach (var keySelector in keySelectors) - { - var member = keySelector.Body as MemberExpression; - var propInfo = member.Member as PropertyInfo; - string localeKey = propInfo.Name; - var value = _localizedEntityService.GetLocalizedValue(language.Value.Id, entity.Id, localeKeyGroup, localeKey); - - // we better not export empty values. the risk is to high that they are imported and unnecessary fill databases. - if (value.HasValue()) - { - dynamic exp = new HybridExpando(); - exp.Culture = languageCulture; - exp.LocaleKey = localeKey; - exp.LocaleValue = value; - - localized.Add(exp); - } - } - } - - return (localized.Count == 0 ? null : localized); - } - private List GetLocalized( DataExporterContext ctx, LocalizedPropertyCollection values, @@ -555,34 +503,40 @@ private dynamic ToDynamic(DataExporterContext ctx, Picture picture, int thumbPic private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttribute pva) { - if (pva == null) - return null; + if (pva == null) + { + return null; + } - dynamic result = new DynamicEntity(pva); + var languageId = ctx.Projection.LanguageId ?? 0; + var attribute = pva.ProductAttribute; - dynamic attribute = new DynamicEntity(pva.ProductAttribute); + dynamic result = new DynamicEntity(pva); + dynamic dynAttribute = new DynamicEntity(attribute); + var paTranslations = ctx.TranslationsPerPage[nameof(ProductAttribute)]; + var pvavTranslations = ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)]; - attribute.Name = pva.ProductAttribute.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - attribute.Description = pva.ProductAttribute.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); + dynAttribute.Name = paTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Name)) ?? attribute.Name; + dynAttribute.Description = paTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Description)) ?? attribute.Description; - attribute.Values = pva.ProductVariantAttributeValues + dynAttribute.Values = pva.ProductVariantAttributeValues .OrderBy(x => x.DisplayOrder) .Select(x => { dynamic dyn = new DynamicEntity(x); - dyn.Name = x.GetLocalized(y => y.Name, ctx.Projection.LanguageId ?? 0, true, false); - dyn._Localized = GetLocalized(ctx, x, y => y.Name); + dyn.Name = pvavTranslations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; + dyn._Localized = GetLocalized(ctx, pvavTranslations, x, y => y.Name); return dyn; }) .ToList(); - attribute._Localized = GetLocalized(ctx, pva.ProductAttribute, + dynAttribute._Localized = GetLocalized(ctx, paTranslations, attribute, x => x.Name, x => x.Description); - result.Attribute = attribute; + result.Attribute = dynAttribute; return result; } @@ -680,7 +634,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam } dynamic result = new DynamicEntity(product); - var translations = ctx.Translations[nameof(Product)]; + var translations = ctx.TranslationsPerPage[nameof(Product)]; result.AppliedDiscounts = null; result.Downloads = null; @@ -778,8 +732,9 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen if (ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) { + var translations = ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)]; var valueNames = variantAttributeValues - .Select(x => x.GetLocalized(y => y.Name, languageId, true, false)) + .Select(x => translations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name) .ToList(); dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); @@ -995,11 +950,16 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen string brand = null; var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); - if (productManus != null && productManus.Any()) - brand = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); - - if (brand.IsEmpty()) - brand = ctx.Projection.Brand; + if (productManus != null && productManus.Any()) + { + var translations = ctx.Translations[nameof(Manufacturer)]; + var manufacturer = productManus.First().Manufacturer; + brand = translations.GetValue(languageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; + } + if (brand.IsEmpty()) + { + brand = ctx.Projection.Brand; + } dynObject._Brand = brand; } @@ -1106,8 +1066,10 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen private dynamic ToDynamic(DataExporterContext ctx, Order order) { - if (order == null) - return null; + if (order == null) + { + return null; + } dynamic result = new DynamicEntity(order); @@ -1135,13 +1097,14 @@ private dynamic ToDynamic(DataExporterContext ctx, Order order) private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) { - if (orderItem == null) - return null; + if (orderItem == null) + { + return null; + } dynamic result = new DynamicEntity(orderItem); orderItem.Product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser.Value); - result.Product = ToDynamic(ctx, orderItem.Product); return result; @@ -1149,8 +1112,10 @@ private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) private dynamic ToDynamic(DataExporterContext ctx, Shipment shipment) { - if (shipment == null) - return null; + if (shipment == null) + { + return null; + } dynamic result = new DynamicEntity(shipment); @@ -1158,7 +1123,6 @@ private dynamic ToDynamic(DataExporterContext ctx, Shipment shipment) .Select(x => { dynamic exp = new DynamicEntity(x); - return exp; }) .ToList(); @@ -1168,18 +1132,21 @@ private dynamic ToDynamic(DataExporterContext ctx, Shipment shipment) private dynamic ToDynamic(DataExporterContext ctx, Discount discount) { - if (discount == null) - return null; + if (discount == null) + { + return null; + } dynamic result = new DynamicEntity(discount); - return result; } private dynamic ToDynamic(DataExporterContext ctx, Download download) { if (download == null) + { return null; + } dynamic result = new DynamicEntity(download); return result; @@ -1187,31 +1154,35 @@ private dynamic ToDynamic(DataExporterContext ctx, Download download) private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute psa) { - if (psa == null) - return null; - - var option = psa.SpecificationAttributeOption; + if (psa == null) + { + return null; + } - dynamic result = new DynamicEntity(psa); + var languageId = ctx.Projection.LanguageId ?? 0; + var option = psa.SpecificationAttributeOption; + var attribute = option.SpecificationAttribute; - dynamic dynAttribute = new DynamicEntity(option.SpecificationAttribute); + dynamic result = new DynamicEntity(psa); + dynamic dynAttribute = new DynamicEntity(attribute); + var saTranslations = ctx.TranslationsPerPage[nameof(SpecificationAttribute)]; + var saoTranslations = ctx.TranslationsPerPage[nameof(SpecificationAttributeOption)]; - dynAttribute.Name = option.SpecificationAttribute.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - dynAttribute._Localized = GetLocalized(ctx, option.SpecificationAttribute, x => x.Name); + dynAttribute.Name = saTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Name)) ?? attribute.Name; + dynAttribute._Localized = GetLocalized(ctx, saTranslations, attribute, x => x.Name); - dynAttribute.Alias = option.SpecificationAttribute.GetLocalized(x => x.Alias, ctx.Projection.LanguageId ?? 0, true, false); - dynAttribute._Localized = GetLocalized(ctx, option.SpecificationAttribute, x => x.Alias); + dynAttribute.Alias = saTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Alias)) ?? attribute.Alias; + dynAttribute._Localized = GetLocalized(ctx, saTranslations, attribute, x => x.Alias); - dynamic dynOption = new DynamicEntity(option); + dynamic dynOption = new DynamicEntity(option); - dynOption.Name = option.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - dynOption._Localized = GetLocalized(ctx, option, x => x.Name); + dynOption.Name = saoTranslations.GetValue(languageId, option.Id, nameof(option.Name)) ?? option.Name; + dynOption._Localized = GetLocalized(ctx, saoTranslations, option, x => x.Name); - dynOption.Alias = option.GetLocalized(x => x.Alias, ctx.Projection.LanguageId ?? 0, true, false); - dynOption._Localized = GetLocalized(ctx, option, x => x.Alias); + dynOption.Alias = saoTranslations.GetValue(languageId, option.Id, nameof(option.Alias)) ?? option.Alias; + dynOption._Localized = GetLocalized(ctx, saoTranslations, option, x => x.Alias); dynOption.SpecificationAttribute = dynAttribute; - result.SpecificationAttributeOption = dynOption; return result; @@ -1219,18 +1190,21 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute private dynamic ToDynamic(DataExporterContext ctx, GenericAttribute genericAttribute) { - if (genericAttribute == null) - return null; + if (genericAttribute == null) + { + return null; + } dynamic result = new DynamicEntity(genericAttribute); - return result; } private dynamic ToDynamic(DataExporterContext ctx, NewsLetterSubscription subscription) { - if (subscription == null) - return null; + if (subscription == null) + { + return null; + } dynamic result = new DynamicEntity(subscription); @@ -1243,8 +1217,10 @@ private dynamic ToDynamic(DataExporterContext ctx, NewsLetterSubscription subscr private dynamic ToDynamic(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) { - if (shoppingCartItem == null) - return null; + if (shoppingCartItem == null) + { + return null; + } dynamic result = new DynamicEntity(shoppingCartItem); From d5f5697e67de73f21296ccf5777c38fcd6642f16 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Wed, 14 Nov 2018 22:59:49 +0100 Subject: [PATCH 017/657] New storefront catalog options: ShowSubCategoriesInSubPages, ShowDescriptionInSubPages & IncludeFeaturedProductsInSubPages (Subpage: List index > 1 or any active filter). --- .../Domain/Catalog/CatalogSettings.cs | 35 +- .../Migrations/MigrationsConfiguration.cs | 26 +- .../Localization/LocalizedValue.cs | 7 +- .../Search/Catalog/CatalogSearchQuery.cs | 14 + .../Search/Catalog/CatalogSearchResult.cs | 2 + .../Modelling/CatalogSearchQueryFactory.cs | 16 +- .../Models/Settings/CatalogSettingsModel.cs | 9 + .../Views/Setting/Catalog.cshtml | 320 ++++++++++-------- .../Controllers/CatalogController.cs | 114 ++++--- ...egoryTemplate.ProductsInGridOrLines.cshtml | 2 +- ...turerTemplate.ProductsInGridOrLines.cshtml | 2 +- 11 files changed, 328 insertions(+), 219 deletions(-) diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index 0be6f2d778..c6d208659f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -188,11 +188,21 @@ public CatalogSettings() /// Gets or sets a value indicating whether and where to display a list of subcategories /// public SubCategoryDisplayType SubCategoryDisplayType { get; set; } - - /// - /// Gets or sets a value indicating whether a 'Share button' is enabled - /// - public bool ShowShareButton { get; set; } + + /// + /// An option indicating whether sub pages should display the subcategories + /// + public bool ShowSubCategoriesInSubPages { get; set; } + + /// + /// An option indicating whether sub pages should display the category/manufacturer description + /// + public bool ShowDescriptionInSubPages { get; set; } + + /// + /// Gets or sets a value indicating whether a 'Share button' is enabled + /// + public bool ShowShareButton { get; set; } /// /// Gets or sets a share code (e.g. AddThis button code) @@ -440,11 +450,16 @@ public CatalogSettings() /// An option indicating whether products on category and manufacturer pages should include featured products as well /// public bool IncludeFeaturedProductsInNormalLists { get; set; } - - /// - /// Gets or sets a value indicating whether tier prices should be displayed with applied discounts (if available) - /// - public bool DisplayTierPricesWithDiscounts { get; set; } + + /// + /// An option indicating whether products on category and manufacturer pages should include featured products in sub pages as well + /// + public bool IncludeFeaturedProductsInSubPages { get; set; } + + /// + /// Gets or sets a value indicating whether tier prices should be displayed with applied discounts (if available) + /// + public bool DisplayTierPricesWithDiscounts { get; set; } /// /// Gets or sets a value indicating whether to ignore discounts (side-wide) diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 318b070e42..75366a9612 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -3,6 +3,7 @@ using System; using System.Data.Entity.Migrations; using Setup; + using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Utilities; @@ -35,6 +36,11 @@ public void MigrateSettings(SmartObjectContext context) x.Add(TypeHelper.NameOf(y => y.CacheSegmentSize, true), 500); x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchTranslations, true), false); x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchUrlSlugs, true), false); + + // New CatalogSettings properties + x.Add(TypeHelper.NameOf(y => y.ShowSubCategoriesInSubPages, true), false); + x.Add(TypeHelper.NameOf(y => y.ShowDescriptionInSubPages, true), false); + x.Add(TypeHelper.NameOf(y => y.IncludeFeaturedProductsInSubPages, true), false); }); } @@ -595,6 +601,24 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Common.ProcessingInfo", "{0}: {1} of {2} processed", "{0}: {1} von {2} verarbeitet"); - } + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowSubCategoriesInSubPages", + "Show subcategories also in subpages", + "Unterwarengruppen auch in Unterseiten anzeigen", + "Subpage: List index greater than 1 or any active filter.", + "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowDescriptionInSubPages", + "Show page description also in subpages", + "Seitenbeschreibungen auch in Unterseiten anzeigen", + "Subpage: List index greater than 1 or any active filter.", + "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.IncludeFeaturedProductsInSubPages", + "Show featured products also in subpages", + "Top-Produkte auch in Unterseiten anzeigen", + "Subpage: List index greater than 1 or any active filter.", + "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); + } } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs index ed3c57dcf4..625f53f986 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs @@ -37,7 +37,7 @@ public static string FixBrackets(string str, Language currentLanguage) [Serializable] public class LocalizedValue : IHtmlString, IEquatable>, IComparable, IComparable> { - private readonly T _value; + private T _value; private readonly Language _requestLanguage; private readonly Language _currentLanguage; @@ -75,6 +75,11 @@ public bool BidiOverride get { return _requestLanguage != _currentLanguage && _requestLanguage.Rtl != _currentLanguage.Rtl; } } + public void ChangeValue(T value) + { + _value = value; + } + public static implicit operator T(LocalizedValue obj) { if (obj == null) diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs index 00be3a01bc..ca01fd89cd 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs @@ -37,6 +37,20 @@ object ICloneable.Clone() return this.MemberwiseClone(); } + public bool IsSubPage + { + get + { + if (PageIndex > 0) + { + return true; + } + + var hasActiveFilter = FacetDescriptors.Values.Any(x => x.Values.Any(y => y.IsSelected)); + return hasActiveFilter; + } + } + #region Fluent builder public CatalogSearchQuery SortBy(ProductSortingEnum sort) diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs index 87fe6940dc..1a215fcf79 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Search; @@ -12,6 +13,7 @@ public partial class CatalogSearchResult private readonly int _totalHitsCount; private readonly Func> _hitsFactory; private IPagedList _hits; + private bool? _isSubPage; public CatalogSearchResult( ISearchEngine engine, diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs index 4f4674d168..88bb3da613 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs @@ -330,13 +330,15 @@ private void AddFacet( throw new SmartException($"Unknown field name for facet group '{kind.ToString()}'"); } - var descriptor = new FacetDescriptor(fieldName); - descriptor.Label = _services.Localization.GetResource(FacetUtility.GetLabelResourceKey(kind) ?? kind.ToString()); - descriptor.IsMultiSelect = isMultiSelect; - descriptor.DisplayOrder = displayOrder; - descriptor.OrderBy = sorting; - descriptor.MinHitCount = _searchSettings.FilterMinHitCount; - descriptor.MaxChoicesCount = _searchSettings.FilterMaxChoicesCount; + var descriptor = new FacetDescriptor(fieldName) + { + Label = _services.Localization.GetResource(FacetUtility.GetLabelResourceKey(kind) ?? kind.ToString()), + IsMultiSelect = isMultiSelect, + DisplayOrder = displayOrder, + OrderBy = sorting, + MinHitCount = _searchSettings.FilterMinHitCount, + MaxChoicesCount = _searchSettings.FilterMaxChoicesCount + }; addValues(descriptor); query.WithFacet(descriptor); diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs index d7f56e0c9b..8c2e1acf84 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs @@ -167,6 +167,15 @@ public CatalogSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.GridStyleListColumnSpan")] public GridColumnSpan GridStyleListColumnSpan { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowSubCategoriesInSubPages")] + public bool ShowSubCategoriesInSubPages { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowDescriptionInSubPages")] + public bool ShowDescriptionInSubPages { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.IncludeFeaturedProductsInSubPages")] + public bool IncludeFeaturedProductsInSubPages { get; set; } + #endregion #region Products diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml index efcb02b519..44f9f4db7f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml @@ -304,160 +304,143 @@ @helper TabProductListSettings() { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +
-
-
@T("Common.Navigation")
-
-
- @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) - - @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) - @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories) -
- @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) - - @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists) - @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists) -
- @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) - - @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) - @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures) -
- @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumber, Html.CheckBoxFor(model => model.ShowCategoryProductNumber, new { data_toggler_for = "#pnlShowCategoryProductNumberIncludingSubcategories" })) - @Html.ValidationMessageFor(model => model.ShowCategoryProductNumber) -
- @Html.SmartLabelFor(model => model.ShowCategoryProductNumberIncludingSubcategories) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumberIncludingSubcategories) - @Html.ValidationMessageFor(model => model.ShowCategoryProductNumberIncludingSubcategories) -
- @Html.SmartLabelFor(model => model.CategoryBreadcrumbEnabled) - - @Html.SettingEditorFor(model => model.CategoryBreadcrumbEnabled) - @Html.ValidationMessageFor(model => model.CategoryBreadcrumbEnabled) -
- @Html.SmartLabelFor(model => model.SubCategoryDisplayType) - + + + + + + + + + + + + + + + + - -
+ @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) + + @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) + @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories) +
+ @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) + + @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists, Html.CheckBoxFor(model => model.IncludeFeaturedProductsInNormalLists, new { data_toggler_for = "#pnlIncludeFeaturedProductsInSubPages" })) + @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists) +
+ @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInSubPages) + + @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInSubPages) + @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInSubPages) +
+ @Html.SmartLabelFor(model => model.SubCategoryDisplayType) + @Html.SettingEditorFor(model => model.SubCategoryDisplayType, @Html.DropDownListFor(model => model.SubCategoryDisplayType, Model.AvailableSubCategoryDisplayTypes)) - @Html.ValidationMessageFor(model => model.SubCategoryDisplayType) -
- - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - -
-
-
@T("Common.List")
-
-
- @Html.SmartLabelFor(model => model.AllowProductSorting) - - @Html.SettingEditorFor(model => model.AllowProductSorting) - @Html.ValidationMessageFor(model => model.AllowProductSorting) -
- @Html.SmartLabelFor(model => model.DefaultViewMode) - + @Html.ValidationMessageFor(model => model.SubCategoryDisplayType) +
+ @Html.SmartLabelFor(model => model.ShowSubCategoriesInSubPages) + + @Html.SettingEditorFor(model => model.ShowSubCategoriesInSubPages) + @Html.ValidationMessageFor(model => model.ShowSubCategoriesInSubPages) +
+ @Html.SmartLabelFor(model => model.ShowDescriptionInSubPages) + + @Html.SettingEditorFor(model => model.ShowDescriptionInSubPages) + @Html.ValidationMessageFor(model => model.ShowDescriptionInSubPages) +
+ @Html.SmartLabelFor(model => model.AllowProductSorting) + + @Html.SettingEditorFor(model => model.AllowProductSorting) + @Html.ValidationMessageFor(model => model.AllowProductSorting) +
+ @Html.SmartLabelFor(model => model.DefaultViewMode) + @Html.SettingEditorFor(model => model.DefaultViewMode, @Html.DropDownListFor(model => model.DefaultViewMode, Model.AvailableDefaultViewModes)) - @Html.ValidationMessageFor(model => model.DefaultViewMode) -
- @Html.SmartLabelFor(model => model.GridStyleListColumnSpan) - - @Html.EnumSettingEditorFor(model => model.GridStyleListColumnSpan) - @Html.ValidationMessageFor(model => model.GridStyleListColumnSpan) -
- @Html.SmartLabelFor(model => model.DefaultSortOrder) - + @Html.ValidationMessageFor(model => model.DefaultViewMode) +
+ @Html.SmartLabelFor(model => model.GridStyleListColumnSpan) + + @Html.EnumSettingEditorFor(model => model.GridStyleListColumnSpan) + @Html.ValidationMessageFor(model => model.GridStyleListColumnSpan) +
+ @Html.SmartLabelFor(model => model.DefaultSortOrder) + @Html.SettingEditorFor(model => model.DefaultSortOrder, @Html.DropDownListFor(model => model.DefaultSortOrder, Model.AvailableSortOrderModes)) - @Html.ValidationMessageFor(model => model.DefaultSortOrder) -
- @Html.SmartLabelFor(model => model.AllowProductViewModeChanging) - - @Html.SettingEditorFor(model => model.AllowProductViewModeChanging) - @Html.ValidationMessageFor(model => model.AllowProductViewModeChanging) -
- @Html.SmartLabelFor(model => model.DefaultProductListPageSize) - - @Html.SettingEditorFor(model => model.DefaultProductListPageSize) - @Html.ValidationMessageFor(model => model.DefaultProductListPageSize) -
- @Html.SmartLabelFor(model => model.DefaultPageSizeOptions) - - @Html.SettingEditorFor(model => model.DefaultPageSizeOptions) - @Html.ValidationMessageFor(model => model.DefaultPageSizeOptions) -
- @Html.SmartLabelFor(model => model.PriceDisplayType) - + @Html.ValidationMessageFor(model => model.DefaultSortOrder) +
+ @Html.SmartLabelFor(model => model.AllowProductViewModeChanging) + + @Html.SettingEditorFor(model => model.AllowProductViewModeChanging) + @Html.ValidationMessageFor(model => model.AllowProductViewModeChanging) +
+ @Html.SmartLabelFor(model => model.DefaultProductListPageSize) + + @Html.SettingEditorFor(model => model.DefaultProductListPageSize) + @Html.ValidationMessageFor(model => model.DefaultProductListPageSize) +
+ @Html.SmartLabelFor(model => model.DefaultPageSizeOptions) + + @Html.SettingEditorFor(model => model.DefaultPageSizeOptions) + @Html.ValidationMessageFor(model => model.DefaultPageSizeOptions) +
+ @Html.SmartLabelFor(model => model.PriceDisplayType) + @Html.SettingEditorFor(model => model.PriceDisplayType, @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes)) - @Html.ValidationMessageFor(model => model.PriceDisplayType) -
+ @Html.ValidationMessageFor(model => model.PriceDisplayType) +
+ @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) + + @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) + @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures) +
@@ -550,6 +533,43 @@
+ + + + + + + + + + + + + + + + +
+
+
@T("Common.Navigation")
+
+
+ @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) + + @Html.SettingEditorFor(model => model.ShowCategoryProductNumber, Html.CheckBoxFor(model => model.ShowCategoryProductNumber, new { data_toggler_for = "#pnlShowCategoryProductNumberIncludingSubcategories" })) + @Html.ValidationMessageFor(model => model.ShowCategoryProductNumber) +
+ @Html.SmartLabelFor(model => model.ShowCategoryProductNumberIncludingSubcategories) + + @Html.SettingEditorFor(model => model.ShowCategoryProductNumberIncludingSubcategories) + @Html.ValidationMessageFor(model => model.ShowCategoryProductNumberIncludingSubcategories) +
+ @Html.SmartLabelFor(model => model.CategoryBreadcrumbEnabled) + + @Html.SettingEditorFor(model => model.CategoryBreadcrumbEnabled) + @Html.ValidationMessageFor(model => model.CategoryBreadcrumbEnabled) +
+ - @@ -234,7 +234,7 @@ columns.Bound(x => x.Succeeded) .ClientTemplate( "<# if(Succeeded){ #>" + @Html.SymbolForBool("Succeeded") + "" + T("Common.Succeeded") + "<# } #>" + - "<# if(!Succeeded){ #>
" + T("Common.Error") + ": <#= Error #>
<# } #>"); + "<# if(!Succeeded){ #>
" + T("Common.Error") + ":
" + T("Common.Error") + ":  <#= Error #>
<# } #>"); columns.Bound(x => x.MachineName); columns.Command(commands => { From 8e17f697a94ef01c418b549e8ba0c43f20c4e239 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Fri, 16 Nov 2018 04:28:00 +0100 Subject: [PATCH 024/657] (Perf) New XML sitemap generation strategy (wip) --- .../Seo/IXmlSitemapGenerator.cs | 15 + .../Seo/Tasks/RebuildXmlSitemapTask.cs | 17 +- .../Seo/XmlSitemapGenerator.cs | 340 ++++++++++++++++-- 3 files changed, 343 insertions(+), 29 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs index 0861c8fe92..cc1850fb99 100644 --- a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs @@ -1,4 +1,6 @@ +using SmartStore.Services.Tasks; using System.Collections.Generic; +using System.Threading; namespace SmartStore.Services.Seo { @@ -18,6 +20,19 @@ public partial interface IXmlSitemapGenerator /// Sitemap XML string GetSitemap(int? index = null); + /// + /// Rebuilds the collection of XML sitemap documents for the current site. If there are less than 1.000 sitemap + /// nodes, only one sitemap document will exist in the collection, otherwise a sitemap index document will be + /// the first entry in the collection and all other entries will be sitemap XML documents. + /// + /// Optional callback for progress change + /// Cancellation token + /// + /// During rebuilding, requests are being served from the existing cache. + /// Once rebuild is completed, the cache is updated. + /// + void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null); + /// /// Removes the sitemap from the cache for a rebuild. /// diff --git a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs index ff719bbe05..c7090b1996 100644 --- a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs +++ b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs @@ -21,13 +21,20 @@ public RebuildXmlSitemapTask(IXmlSitemapGenerator generator, SeoSettings seoSett public void Execute(TaskExecutionContext ctx) { - if (_generator.IsGenerated) + //if (_generator.IsGenerated) + //{ + // _generator.Invalidate(); + //} + + //// enforces refresh + //_generator.GetSitemap(0); + + _generator.Rebuild(ctx.CancellationToken, OnProgress); + + void OnProgress(int value, int max, string msg) { - _generator.Invalidate(); + ctx.SetProgress(value, max, msg, true); } - - // enforces refresh - _generator.GetSitemap(0); } } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index b68b641ce6..a734d7786f 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; +using System.Data.Entity; using System.Web.Mvc; using System.Xml.Linq; using SmartStore.Core; @@ -10,17 +12,51 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Security; using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Topics; using SmartStore.Core.Logging; using SmartStore.Services.Catalog; using SmartStore.Services.Customers; using SmartStore.Services.Localization; using SmartStore.Services.Search; +using SmartStore.Services.Tasks; using SmartStore.Services.Topics; +using System.Diagnostics; +using SmartStore.Core.IO; namespace SmartStore.Services.Seo { + internal class XmlSitemapEntity : BaseEntity, ISlugSupported + { + public string EntityName { get; set; } + public DateTime LastMod { get; set; } + + public override string GetEntityName() + { + return EntityName; + } + } + public partial class XmlSitemapGenerator : IXmlSitemapGenerator { + class QueryHolder + { + public IQueryable Categories { get; set; } + public IQueryable Manufacturers { get; set; } + public IQueryable Topics { get; set; } + public IQueryable Products { get; set; } + + public int GetTotalRecordCount() + { + int num = 0; + if (Categories != null) num += Categories.Count(); + if (Manufacturers != null) num += Manufacturers.Count(); + if (Topics != null) num += Topics.Count(); + if (Products != null) num += Products.Count(); + + return num; + } + } + /// /// Key for seo sitemap /// @@ -52,16 +88,18 @@ public partial class XmlSitemapGenerator : IXmlSitemapGenerator /// private const int MaximumSiteMapSizeInBytes = 10485760; - private readonly ICategoryService _categoryService; + private readonly ICategoryService _categoryService; private readonly IProductService _productService; private readonly IManufacturerService _manufacturerService; private readonly ITopicService _topicService; private readonly ILanguageService _languageService; private readonly ICustomerService _customerService; private readonly ICatalogSearchService _catalogSearchService; + private readonly IUrlRecordService _urlRecordService; private readonly SeoSettings _seoSettings; private readonly SecuritySettings _securitySettings; private readonly ICommonServices _services; + private readonly ILockFileManager _lockFileManager; private readonly UrlHelper _urlHelper; public XmlSitemapGenerator( @@ -72,9 +110,11 @@ public XmlSitemapGenerator( ILanguageService languageService, ICustomerService customerService, ICatalogSearchService catalogSearchService, + IUrlRecordService urlRecordService, SeoSettings commonSettings, SecuritySettings securitySettings, ICommonServices services, + ILockFileManager lockFileManager, UrlHelper urlHelper) { _categoryService = categoryService; @@ -84,26 +124,36 @@ public XmlSitemapGenerator( _languageService = languageService; _customerService = customerService; _catalogSearchService = catalogSearchService; + _urlRecordService = urlRecordService; _seoSettings = commonSettings; _securitySettings = securitySettings; _services = services; + _lockFileManager = lockFileManager; _urlHelper = urlHelper; Logger = NullLogger.Instance; - } + BasePath = _services.ApplicationEnvironment.TenantFolder.Combine("Sitemaps"); + } - public ILogger Logger + private void EnsureBaseDirectoryExists() { - get; - set; + // create base directory if it doesn't exist yet + if (!_services.ApplicationEnvironment.TenantFolder.DirectoryExists(BasePath)) + { + _services.ApplicationEnvironment.TenantFolder.CreateDirectory(BasePath); + } } - public void Invalidate() + public string BasePath { get; private set; } + + public ILogger Logger { get; set; } + + public virtual void Invalidate() { _services.Cache.RemoveByPattern(XMLSITEMAP_PATTERN_KEY); } - public bool IsGenerated + public virtual bool IsGenerated { get { @@ -115,7 +165,7 @@ public bool IsGenerated } } - public string GetSitemap(int? index = null) + public virtual string GetSitemap(int? index = null) { var storeId = _services.StoreContext.CurrentStore.Id; var langId = _services.WorkContext.WorkingLanguage.Id; @@ -166,13 +216,223 @@ public string GetSitemap(int? index = null) return null; } + public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null) + { + var storeId = _services.StoreContext.CurrentStore.Id; + var langId = _services.WorkContext.WorkingLanguage.Id; + + var siteMapPath = GetSitemapPath(storeId, langId); + var lockFilePath = GetLockFilePath(storeId, langId); + + if (!_lockFileManager.TryAcquireLock(lockFilePath, out var lockFile)) + { + Logger.Warn("XML Sitemap rebuild already in process."); + return; + } + + using (lockFile) + { + // Impersonate + var prevCustomer = _services.WorkContext.CurrentCustomer; + var bot = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); + + try + { + var protocol = _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http"; + var nodes = new List(); + var queries = CreateQueries(); + var total = queries.GetTotalRecordCount(); + + using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) + { + var entities = EnumerateEntities(queries); + + var segment = 0; + var numProcessed = 0; + foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + numProcessed = ++segment * MaximumSiteMapNodeCount; + callback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); + + var firstEntityName = batch.First().EntityName; + var lastEntityName = batch.Last().EntityName; + + var slugs = GetUrlRecordCollectionsForBatch(batch, langId); + + nodes.AddRange(batch.Select(x => new XmlSitemapNode + { + LastMod = x.LastMod, + Loc = _urlHelper.RouteUrl(x.EntityName, new { SeName = slugs[x.EntityName].GetSlug(langId, x.Id, true) }, protocol) + })); + } + + if (!cancellationToken.IsCancellationRequested) + { + callback?.Invoke(numProcessed, total, "Processing custom nodes".FormatCurrent(numProcessed, total)); + var customNodes = GetCustomNodes(protocol); + if (customNodes != null) + { + nodes.AddRange(customNodes); + } + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + callback?.Invoke(total, total, "Finalizing"); + var documents = GetSiteMapDocuments(nodes.AsReadOnly(), protocol); + + cancellationToken.ThrowIfCancellationRequested(); + + SaveToDisk(siteMapPath, documents, langId, storeId); + } + finally + { + // Undo impersonation + _services.WorkContext.CurrentCustomer = prevCustomer; + } + } + } + + private void SaveToDisk(string path, List documents, int languageId, int storeId) + { + EnsureBaseDirectoryExists(); + + var folder = _services.ApplicationEnvironment.TenantFolder; + + if (folder.DirectoryExists(path)) + { + folder.DeleteDirectory(path); + } + + folder.CreateDirectory(path); + + for (int i = 0; i < documents.Count; i++) + { + // Save segment to disk + var fileName = "sitemap-" + i + ".xml"; + var filePath = folder.Combine(path, fileName); + + folder.CreateTextFile(filePath, documents[i]); + } + } + + private string GetSitemapPath(int storeId, int languageId) + { + return _services.ApplicationEnvironment.TenantFolder.Combine(BasePath + "/" + storeId + "/" + languageId); + } + + private string GetLockFilePath(int storeId, int languageId) + { + return _services.ApplicationEnvironment.TenantFolder.Combine(GetSitemapPath(storeId, languageId), "_sitemap.lock"); + } + + private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int languageId) + { + var result = new Dictionary(); + var languageIds = new[] { languageId, 0 }; + + if (batch.First().EntityName == "Product") + { + // nothing comes after product + int min = batch.Last().Id; + int max = batch.First().Id; + + result["Product"] = _urlRecordService.GetUrlRecordCollection("Product", languageIds, new[] { min, max }, true, true); + } + + var entityGroups = batch.ToMultimap(x => x.EntityName, x => x.Id); + foreach (var group in entityGroups) + { + var isRange = group.Key == "Product"; + var entityIds = isRange ? new[] { group.Value.Last(), group.Value.First() } : group.Value.ToArray(); + + result[group.Key] = _urlRecordService.GetUrlRecordCollection(group.Key, languageIds, entityIds, isRange, isRange); + } + + return result; + } + + private IEnumerable EnumerateEntities(QueryHolder queries) + { + var entities = Enumerable.Empty(); + + if (queries.Categories != null) + { + var categories = queries.Categories.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); + foreach (var x in categories) + { + yield return new XmlSitemapEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } + } + + if (queries.Manufacturers != null) + { + var manufacturers = queries.Manufacturers.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); + foreach (var x in manufacturers) + { + yield return new XmlSitemapEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } + } + + if (queries.Topics != null) + { + var topics = queries.Topics.Select(x => new { x.Id }).ToList(); + foreach (var x in topics) + { + yield return new XmlSitemapEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }; + } + } + + if (queries.Products != null) + { + var query = queries.Products.AsNoTracking(); + + var maxId = int.MaxValue; + int xxx = 0; + while (maxId > 1) + { + xxx++; + if (xxx >= 100) + { + break; + } + + var products = queries.Products.AsNoTracking() + .Where(x => x.Id < maxId) + .OrderByDescending(x => x.Id) + .Take(() => 1000) + .Select(x => new { x.Id, x.UpdatedOnUtc }) + .ToList(); + + if (products.Count == 0) + { + break; + } + + maxId = products.Last().Id; + + foreach (var x in products) + { + yield return new XmlSitemapEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } + } + } + } + /// - /// Gets the collection of XML sitemap documents for the current site. If there are less than 1.000 sitemap + /// Generates the collection of XML sitemap documents for the current site. If there are less than 1.000 sitemap /// nodes, only one sitemap document will exist in the collection, otherwise a sitemap index document will be /// the first entry in the collection and all other entries will be sitemap XML documents. /// /// A collection of XML sitemap documents. - protected IList Generate() + /// This method operates uncached and always rebuilds the sitemap when called. + protected virtual IList Generate() { var protocol = _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http"; @@ -180,10 +440,9 @@ protected IList Generate() using (var scope = new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) { - if (_seoSettings.XmlSitemapIncludesCategories) { - nodes.AddRange(GetCategoryNodes(0, protocol)); + nodes.AddRange(GetCategoryNodes(protocol)); } if (_seoSettings.XmlSitemapIncludesManufacturers) @@ -200,23 +459,21 @@ protected IList Generate() { nodes.AddRange(GetProductNodes(protocol)); } - } - var customNodes = GetCustomNodes(protocol); - if (customNodes != null) - { - nodes.AddRange(customNodes); + var customNodes = GetCustomNodes(protocol); + if (customNodes != null) + { + nodes.AddRange(customNodes); + } } - var documents = GetSiteMapDocuments(nodes.AsReadOnly()); + var documents = GetSiteMapDocuments(nodes.AsReadOnly(), protocol); return documents; } - protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes) + protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes, string protocol) { - var protocol = _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http"; - int siteMapCount = (int)Math.Ceiling(nodes.Count / (double)MaximumSiteMapNodeCount); CheckSitemapCount(siteMapCount); @@ -287,7 +544,7 @@ private string GetSitemapIndexDocument(IEnumerable nodes) return xml; } - protected virtual IEnumerable GetCategoryNodes(int parentCategoryId, string protocol) + private QueryHolder CreateQueries() + { + var holder = new QueryHolder(); + + if (_seoSettings.XmlSitemapIncludesCategories) + { + holder.Categories = _categoryService.GetAllCategories(showHidden: false, storeId: _services.StoreContext.CurrentStore.Id).SourceQuery; + } + + if (_seoSettings.XmlSitemapIncludesManufacturers) + { + holder.Manufacturers = _manufacturerService.GetManufacturers(false).OrderBy(x => x.DisplayOrder).ThenBy(x => x.Name); + } + + if (_seoSettings.XmlSitemapIncludesTopics) + { + holder.Topics = _topicService.GetAllTopics(_services.StoreContext.CurrentStore.Id).AlterQuery(q => + { + return q.Where(t => t.IncludeInSitemap && !t.RenderAsWidget); + }).SourceQuery; + } + + if (_seoSettings.XmlSitemapIncludesProducts) + { + var searchQuery = new CatalogSearchQuery() + .VisibleOnly() + .VisibleIndividuallyOnly(true) + .HasStoreId(_services.StoreContext.CurrentStoreIdIfMultiStoreMode); + + holder.Products = _catalogSearchService.PrepareQuery(searchQuery); + } + + return holder; + } + + protected virtual IEnumerable GetCategoryNodes(string protocol) { var categories = _categoryService.GetAllCategories(showHidden: false, storeId: _services.StoreContext.CurrentStore.Id); @@ -433,7 +725,7 @@ protected virtual IEnumerable GetProductNodes(string protocol) .HasStoreId(_services.StoreContext.CurrentStoreIdIfMultiStoreMode); var query = _catalogSearchService.PrepareQuery(searchQuery); - query = query.OrderByDescending(x => x.CreatedOnUtc); + query = query.OrderByDescending(x => x.Id); for (var pageIndex = 0; pageIndex < 9999999; ++pageIndex) { From 4aa38d43dca8d80b931713a57086caa506a8be8e Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 17 Nov 2018 02:48:29 +0100 Subject: [PATCH 025/657] (Perf) New XML sitemap generation strategy (wip) --- .../IO/LockFile/LockFileManager.cs | 5 +- .../SmartStore.Core/Infrastructure/Guard.cs | 2 +- .../Seo/IXmlSitemapGenerator.cs | 17 +- .../Seo/XmlSitemapBuildContext.cs | 25 + .../Seo/XmlSitemapGenerator.cs | 511 +++++++----------- .../Seo/XmlSitemapPartition.cs | 15 + .../SmartStore.Services.csproj | 2 + .../Filters/CompressAttribute.cs | 4 +- .../Controllers/CommonController.cs | 4 +- .../Controllers/CommonController.cs | 2 +- .../Controllers/HomeController.cs | 24 +- .../Controllers/MediaController.cs | 39 +- .../Infrastructure/Routes/1_StoreRoutes.cs | 4 +- .../Themes/Flex/Content/_variables.scss | 5 + 14 files changed, 305 insertions(+), 354 deletions(-) create mode 100644 src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs create mode 100644 src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs diff --git a/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs b/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs index ab48a467b8..20d00e3441 100644 --- a/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs +++ b/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs @@ -42,7 +42,7 @@ public bool TryAcquireLock(string path, out ILockFile lockFile) { return false; } - + lockFile = new LockFile(_env.TenantFolder, path, DateTime.UtcNow.ToString("u"), _rwLock); return true; } @@ -80,8 +80,7 @@ private bool IsLockedInternal(string path) { var content = _env.TenantFolder.ReadFile(path); - DateTime creationUtc; - if (DateTime.TryParse(content, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out creationUtc)) + if (DateTime.TryParse(content, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var creationUtc)) { // if expired the file is not removed // it should be automatically as there is a finalizer in LockFile diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs b/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs index d49aa6c074..a1d8f6c73a 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs @@ -43,7 +43,7 @@ public static void NotEmpty(string arg, string argName) public static void NotEmpty(ICollection arg, string argName) { if (arg == null || !arg.Any()) - throw Error.Argument(argName, "Collection cannot be null and must have at least one item."); + throw Error.Argument(argName, "Collection cannot be null and must contain at least one item."); } [DebuggerStepThrough] diff --git a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs index cc1850fb99..f5764b8ffc 100644 --- a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs @@ -10,15 +10,15 @@ namespace SmartStore.Services.Seo public partial interface IXmlSitemapGenerator { /// - /// Gets the sitemap XML + /// Gets the sitemap partition /// /// - /// The page index. 0 or null retrieves the first document, which can be a sitemap INDEX document + /// The page index. 0 retrieves the first document, which can be a sitemap INDEX document /// (when the sitemap size exceeded the limits). An index greater 0 retrieves the /// sitemap XML document at this index, but only when the sitemap is actually indexed (otherwise null is returned) /// - /// Sitemap XML - string GetSitemap(int? index = null); + /// Sitemap partition + XmlSitemapPartition GetSitemapPart(int index = 0); /// /// Rebuilds the collection of XML sitemap documents for the current site. If there are less than 1.000 sitemap @@ -34,13 +34,18 @@ public partial interface IXmlSitemapGenerator void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null); /// - /// Removes the sitemap from the cache for a rebuild. + /// Determines whether a rebuild is already running. /// - void Invalidate(); + bool IsRebuilding { get; } /// /// Indicates whether the sitemap has been generated and cached. /// bool IsGenerated { get; } + + /// + /// Removes the sitemap from the cache for a rebuild. + /// + void Invalidate(); } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs new file mode 100644 index 0000000000..d67af75f96 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; +using SmartStore.Services.Tasks; + +namespace SmartStore.Services.Seo +{ + public class XmlSitemapBuildContext + { + public XmlSitemapBuildContext(Store store, Language[] languages) + { + Guard.NotNull(store, nameof(store)); + Guard.NotEmpty(languages, nameof(languages)); + + Store = store; + Languages = languages; + } + + public CancellationToken CancellationToken { get; set; } + public ProgressCallback ProgressCallback { get; set; } + public Store Store { get; set; } + public Language[] Languages { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index a734d7786f..f31e4b873d 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -20,56 +20,20 @@ using SmartStore.Services.Search; using SmartStore.Services.Tasks; using SmartStore.Services.Topics; -using System.Diagnostics; using SmartStore.Core.IO; +using SmartStore.Utilities.Threading; +using SmartStore.Utilities; +using System.IO; +using System.Threading.Tasks; namespace SmartStore.Services.Seo { - internal class XmlSitemapEntity : BaseEntity, ISlugSupported - { - public string EntityName { get; set; } - public DateTime LastMod { get; set; } - - public override string GetEntityName() - { - return EntityName; - } - } - public partial class XmlSitemapGenerator : IXmlSitemapGenerator { - class QueryHolder - { - public IQueryable Categories { get; set; } - public IQueryable Manufacturers { get; set; } - public IQueryable Topics { get; set; } - public IQueryable Products { get; set; } - - public int GetTotalRecordCount() - { - int num = 0; - if (Categories != null) num += Categories.Count(); - if (Manufacturers != null) num += Manufacturers.Count(); - if (Topics != null) num += Topics.Count(); - if (Products != null) num += Products.Count(); - - return num; - } - } - - /// - /// Key for seo sitemap - /// - /// - /// {0} : sitemap index - /// {1} : current store id - /// {2} : current language id - /// - public const string XMLSITEMAP_DOCUMENT_KEY = "sitemap:xml-idx{0}-{1}-{2}"; - public const string XMLSITEMAP_PATTERN_KEY = "sitemap:xml*"; - private const string SiteMapsNamespace = "http://www.sitemaps.org/schemas/sitemap/0.9"; private const string XhtmlNamespace = "http://www.w3.org/1999/xhtml"; + private const string SiteMapFileNamePattern = "sitemap-{0}.xml"; + private const string LockFileNamePattern = "sitemap-{0}-{1}.lock"; /// /// The maximum number of sitemaps a sitemap index file can contain. @@ -102,6 +66,12 @@ public int GetTotalRecordCount() private readonly ILockFileManager _lockFileManager; private readonly UrlHelper _urlHelper; + private readonly int _storeId; + private readonly int _langId; + private readonly IVirtualFolder _tenantFolder; + private readonly string _baseDir; + private readonly string _siteMapDir; + public XmlSitemapGenerator( ICategoryService categoryService, IProductService productService, @@ -131,100 +101,121 @@ public XmlSitemapGenerator( _lockFileManager = lockFileManager; _urlHelper = urlHelper; - Logger = NullLogger.Instance; - BasePath = _services.ApplicationEnvironment.TenantFolder.Combine("Sitemaps"); - } + _storeId = _services.StoreContext.CurrentStore.Id; + _langId = _services.WorkContext.WorkingLanguage.Id; + _tenantFolder = _services.ApplicationEnvironment.TenantFolder; + _baseDir = _tenantFolder.Combine("Sitemaps"); + _siteMapDir = _tenantFolder.Combine(_baseDir, _storeId + "/" + _langId); - private void EnsureBaseDirectoryExists() - { - // create base directory if it doesn't exist yet - if (!_services.ApplicationEnvironment.TenantFolder.DirectoryExists(BasePath)) - { - _services.ApplicationEnvironment.TenantFolder.CreateDirectory(BasePath); - } + Logger = NullLogger.Instance; + } - public string BasePath { get; private set; } - public ILogger Logger { get; set; } - public virtual void Invalidate() + public virtual XmlSitemapPartition GetSitemapPart(int index = 0) { - _services.Cache.RemoveByPattern(XMLSITEMAP_PATTERN_KEY); + return GetSitemapPart(index, false); } - public virtual bool IsGenerated + private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) { - get + Guard.NotNegative(index, nameof(index)); + + var exists = SitemapFileExists(index, out var path, out var name); + + if (exists) { - string cacheKey = XMLSITEMAP_DOCUMENT_KEY.FormatInvariant(0, - _services.StoreContext.CurrentStore.Id, - _services.WorkContext.WorkingLanguage.Id); + return new XmlSitemapPartition + { + Index = index, + Name = name, + LanguageId = _langId, + StoreId = _storeId, + ModifiedOnUtc = _tenantFolder.GetFileLastWriteTimeUtc(path), + Stream = _tenantFolder.OpenFile(path) + }; + } - return _services.Cache.Contains(cacheKey); + if (isRetry) + { + var msg = "Could not generate XML sitemap. Index: {0}, Date: {1}".FormatInvariant(index, DateTime.UtcNow); + Logger.Error(msg); + throw new SmartException(msg); } - } - public virtual string GetSitemap(int? index = null) - { - var storeId = _services.StoreContext.CurrentStore.Id; - var langId = _services.WorkContext.WorkingLanguage.Id; - var cache = _services.Cache; + if (index > 0) + { + // File with index greater 0 has been requested, but it does not exist. + // Now we have to determine whether just the passed index is out of range + // or the files has never been created before. + // If the main file (index 0) exists, the action should return NotFoundResult, + // otherwise the rebuild process should be started or waited for. + + if (SitemapFileExists(0, out path, out name)) + { + throw new IndexOutOfRangeException("The sitemap file '{0}' does not exist.".FormatInvariant(name)); + } + } - string cacheKey = XMLSITEMAP_DOCUMENT_KEY.FormatInvariant(0, storeId, langId); + // The main sitemap document with index 0 does not exist, meaning: the whole sitemap + // needs to be created and cached by partitions. - if (!cache.Contains(cacheKey)) + if (IsRebuilding) { - // The main sitemap document with index 0 does not exist, meaning: the whole sitemap - // needs to be created and cached by partitions. - lock (String.Intern(cacheKey)) - { - var prevCustomer = _services.WorkContext.CurrentCustomer; - var bot = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); + // The rebuild process is already running, either started + // by the task scheduler or another HTTP request. + // We should wait for completion. + + //while (IsRebuilding) + //{ + // //Thread.Sleep(500); + // Task.Delay(500).Wait(); + //} + } + else + { + // No lock. Rebuild now. + Rebuild(CancellationToken.None); + } - try - { - // no need to vary xml sitemap by customer roles: it's relevant to crawlers only. - _services.WorkContext.CurrentCustomer = bot; + // DRY: call self to get sitemap partiition object + return GetSitemapPart(index, true); + } + + private bool SitemapFileExists(int index, out string path, out string name) + { + path = BuildSitemapFilePath(index, out name); - // we need a scoped lock, because we're going to split the cache entries. - var documents = Generate(); + // Does not work reliably with symlinks due to framework caching + //var exists = _tenantFolder.FileExists(path); - for (int i = 0; i < documents.Count; i++) - { - // Put segment into cache - cacheKey = XMLSITEMAP_DOCUMENT_KEY.FormatInvariant(i, storeId, langId); - cache.Put(cacheKey, documents[i], TimeSpan.FromDays(1)); - } - } - finally - { - // Undo impersonation - _services.WorkContext.CurrentCustomer = prevCustomer; - } - } - } + var exists = File.Exists(_tenantFolder.MapPath(path)); - var page = index ?? 0; - cacheKey = XMLSITEMAP_DOCUMENT_KEY.FormatInvariant(page, storeId, langId); - - if (cache.Contains(cacheKey)) + if (!exists) { - return cache.Get(cacheKey); + path = null; + name = null; } - return null; + return exists; } - public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null) + private string BuildSitemapFilePath(int index, out string fileName) { - var storeId = _services.StoreContext.CurrentStore.Id; - var langId = _services.WorkContext.WorkingLanguage.Id; + fileName = SiteMapFileNamePattern.FormatInvariant(index); + return _tenantFolder.Combine(_siteMapDir, fileName); + } - var siteMapPath = GetSitemapPath(storeId, langId); - var lockFilePath = GetLockFilePath(storeId, langId); + private string GetLockFilePath() + { + var fileName = LockFileNamePattern.FormatInvariant(_storeId, _langId); + return _tenantFolder.Combine(_baseDir, fileName); + } - if (!_lockFileManager.TryAcquireLock(lockFilePath, out var lockFile)) + public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null) + { + if (!_lockFileManager.TryAcquireLock(GetLockFilePath(), out var lockFile)) { Logger.Warn("XML Sitemap rebuild already in process."); return; @@ -234,7 +225,8 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac { // Impersonate var prevCustomer = _services.WorkContext.CurrentCustomer; - var bot = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); + // no need to vary xml sitemap by customer roles: it's relevant to crawlers only. + _services.WorkContext.CurrentCustomer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); try { @@ -262,12 +254,12 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac var firstEntityName = batch.First().EntityName; var lastEntityName = batch.Last().EntityName; - var slugs = GetUrlRecordCollectionsForBatch(batch, langId); + var slugs = GetUrlRecordCollectionsForBatch(batch, _langId); nodes.AddRange(batch.Select(x => new XmlSitemapNode { LastMod = x.LastMod, - Loc = _urlHelper.RouteUrl(x.EntityName, new { SeName = slugs[x.EntityName].GetSlug(langId, x.Id, true) }, protocol) + Loc = _urlHelper.RouteUrl(x.EntityName, new { SeName = slugs[x.EntityName].GetSlug(_langId, x.Id, true) }, protocol) })); } @@ -285,11 +277,19 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac cancellationToken.ThrowIfCancellationRequested(); callback?.Invoke(total, total, "Finalizing"); + + if (nodes.Count == 0) + { + // Ensure that at least one entry exists. Otherwise, + // the system will try to rebuild again. + nodes.Add(new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = _urlHelper.RouteUrl("HomePage") }); + } + var documents = GetSiteMapDocuments(nodes.AsReadOnly(), protocol); cancellationToken.ThrowIfCancellationRequested(); - SaveToDisk(siteMapPath, documents, langId, storeId); + SaveToDisk(documents); } finally { @@ -299,75 +299,35 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac } } - private void SaveToDisk(string path, List documents, int languageId, int storeId) + private void SaveToDisk(List documents) { - EnsureBaseDirectoryExists(); - - var folder = _services.ApplicationEnvironment.TenantFolder; - - if (folder.DirectoryExists(path)) + if (_tenantFolder.DirectoryExists(_siteMapDir)) { - folder.DeleteDirectory(path); + _tenantFolder.DeleteDirectory(_siteMapDir); } - folder.CreateDirectory(path); + _tenantFolder.CreateDirectory(_siteMapDir); for (int i = 0; i < documents.Count; i++) { // Save segment to disk - var fileName = "sitemap-" + i + ".xml"; - var filePath = folder.Combine(path, fileName); - - folder.CreateTextFile(filePath, documents[i]); - } - } - - private string GetSitemapPath(int storeId, int languageId) - { - return _services.ApplicationEnvironment.TenantFolder.Combine(BasePath + "/" + storeId + "/" + languageId); - } - - private string GetLockFilePath(int storeId, int languageId) - { - return _services.ApplicationEnvironment.TenantFolder.Combine(GetSitemapPath(storeId, languageId), "_sitemap.lock"); - } - - private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int languageId) - { - var result = new Dictionary(); - var languageIds = new[] { languageId, 0 }; + var fileName = SiteMapFileNamePattern.FormatInvariant(i); + var filePath = _tenantFolder.Combine(_siteMapDir, fileName); - if (batch.First().EntityName == "Product") - { - // nothing comes after product - int min = batch.Last().Id; - int max = batch.First().Id; - - result["Product"] = _urlRecordService.GetUrlRecordCollection("Product", languageIds, new[] { min, max }, true, true); + _tenantFolder.CreateTextFile(filePath, documents[i]); } - - var entityGroups = batch.ToMultimap(x => x.EntityName, x => x.Id); - foreach (var group in entityGroups) - { - var isRange = group.Key == "Product"; - var entityIds = isRange ? new[] { group.Value.Last(), group.Value.First() } : group.Value.ToArray(); - - result[group.Key] = _urlRecordService.GetUrlRecordCollection(group.Key, languageIds, entityIds, isRange, isRange); - } - - return result; } - private IEnumerable EnumerateEntities(QueryHolder queries) + private IEnumerable EnumerateEntities(QueryHolder queries) { - var entities = Enumerable.Empty(); + var entities = Enumerable.Empty(); if (queries.Categories != null) { var categories = queries.Categories.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); foreach (var x in categories) { - yield return new XmlSitemapEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }; + yield return new NamedEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }; } } @@ -376,7 +336,7 @@ private IEnumerable EnumerateEntities(QueryHolder queries) var manufacturers = queries.Manufacturers.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); foreach (var x in manufacturers) { - yield return new XmlSitemapEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }; + yield return new NamedEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }; } } @@ -385,7 +345,7 @@ private IEnumerable EnumerateEntities(QueryHolder queries) var topics = queries.Topics.Select(x => new { x.Id }).ToList(); foreach (var x in topics) { - yield return new XmlSitemapEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }; + yield return new NamedEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }; } } @@ -419,57 +379,36 @@ private IEnumerable EnumerateEntities(QueryHolder queries) foreach (var x in products) { - yield return new XmlSitemapEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }; + yield return new NamedEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }; } } } } - /// - /// Generates the collection of XML sitemap documents for the current site. If there are less than 1.000 sitemap - /// nodes, only one sitemap document will exist in the collection, otherwise a sitemap index document will be - /// the first entry in the collection and all other entries will be sitemap XML documents. - /// - /// A collection of XML sitemap documents. - /// This method operates uncached and always rebuilds the sitemap when called. - protected virtual IList Generate() + private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int languageId) { - var protocol = _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http"; - - var nodes = new List(); + var result = new Dictionary(); + var languageIds = new[] { languageId, 0 }; - using (var scope = new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) + if (batch.First().EntityName == "Product") { - if (_seoSettings.XmlSitemapIncludesCategories) - { - nodes.AddRange(GetCategoryNodes(protocol)); - } - - if (_seoSettings.XmlSitemapIncludesManufacturers) - { - nodes.AddRange(GetManufacturerNodes(protocol)); - } + // nothing comes after product + int min = batch.Last().Id; + int max = batch.First().Id; - if (_seoSettings.XmlSitemapIncludesTopics) - { - nodes.AddRange(GetTopicNodes(protocol)); - } + result["Product"] = _urlRecordService.GetUrlRecordCollection("Product", languageIds, new[] { min, max }, true, true); + } - if (_seoSettings.XmlSitemapIncludesProducts) - { - nodes.AddRange(GetProductNodes(protocol)); - } + var entityGroups = batch.ToMultimap(x => x.EntityName, x => x.Id); + foreach (var group in entityGroups) + { + var isRange = group.Key == "Product"; + var entityIds = isRange ? new[] { group.Value.Last(), group.Value.First() } : group.Value.ToArray(); - var customNodes = GetCustomNodes(protocol); - if (customNodes != null) - { - nodes.AddRange(customNodes); - } + result[group.Key] = _urlRecordService.GetUrlRecordCollection(group.Key, languageIds, entityIds, isRange, isRange); } - var documents = GetSiteMapDocuments(nodes.AsReadOnly(), protocol); - - return documents; + return result; } protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes, string protocol) @@ -544,7 +483,7 @@ private string GetSitemapIndexDocument(IEnumerable GetCategoryNodes(string protocol) + protected virtual IEnumerable GetCustomNodes(string protocol) { - var categories = _categoryService.GetAllCategories(showHidden: false, storeId: _services.StoreContext.CurrentStore.Id); - - _services.DbContext.DetachAll(); - - return categories.Select(x => - { - var node = new XmlSitemapNode - { - Loc = _urlHelper.RouteUrl("Category", new { SeName = x.GetSeName() }, protocol), - LastMod = x.UpdatedOnUtc, - //ChangeFreq = ChangeFrequency.Weekly, - //Priority = 0.8f - }; - - // TODO: add hreflang links if LangCount is > 1 and PrependSeoCode is true - - return node; - }); + return Enumerable.Empty(); } - protected virtual IEnumerable GetManufacturerNodes(string protocol) + /// + /// Checks the size of the XML sitemap document. If it is over 10MB, logs an error. + /// + /// The sitemap XML document. + private void CheckDocumentSize(string siteMapXml) { - var manufacturers = _manufacturerService.GetAllManufacturers(false); - - _services.DbContext.DetachAll(); - - return manufacturers.Select(x => + if (siteMapXml.Length >= MaximumSiteMapSizeInBytes) { - var node = new XmlSitemapNode - { - Loc = _urlHelper.RouteUrl("Manufacturer", new { SeName = x.GetSeName() }, protocol), - LastMod = x.UpdatedOnUtc, - //ChangeFreq = ChangeFrequency.Weekly, - //Priority = 0.8f - }; - - // TODO: add hreflang links if LangCount is > 1 and PrependSeoCode is true - - return node; - }); + Logger.Error(new InvalidOperationException($"Sitemap exceeds the maximum size of 10MB. This is because you have unusually long URL's. Consider reducing the MaximumSitemapNodeCount. Size:<{siteMapXml.Length}>")); + } } - protected virtual IEnumerable GetTopicNodes(string protocol) + /// + /// Checks the count of the number of sitemaps. If it is over 50,000, logs an error. + /// + /// The sitemap count. + private void CheckSitemapCount(int sitemapCount) { - var topics = _topicService.GetAllTopics(_services.StoreContext.CurrentStore.Id).AlterQuery(q => + if (sitemapCount > MaximumSiteMapCount) { - return q.Where(t => t.IncludeInSitemap && !t.RenderAsWidget); - }); - - _services.DbContext.DetachAll(); + var ex = new InvalidOperationException($"Sitemap index file exceeds the maximum number of allowed sitemaps of 50,000. Count:<{sitemapCount}>"); + Logger.Warn(ex, ex.Message); + } + } - return topics.Select(x => + public bool IsRebuilding + { + get { - var node = new XmlSitemapNode - { - Loc = _urlHelper.RouteUrl("Topic", new { SeName = x.GetSeName() }, protocol), - LastMod = DateTime.UtcNow, - //ChangeFreq = ChangeFrequency.Weekly, - //Priority = 0.8f - }; - - // TODO: add hreflang links if LangCount is > 1 and PrependSeoCode is true - - return node; - }); + return _lockFileManager.IsLocked(GetLockFilePath()); + } } - protected virtual IEnumerable GetProductNodes(string protocol) + public virtual bool IsGenerated { - var nodes = new List(); - - var searchQuery = new CatalogSearchQuery() - .VisibleOnly() - .VisibleIndividuallyOnly(true) - .HasStoreId(_services.StoreContext.CurrentStoreIdIfMultiStoreMode); - - var query = _catalogSearchService.PrepareQuery(searchQuery); - query = query.OrderByDescending(x => x.Id); - - for (var pageIndex = 0; pageIndex < 9999999; ++pageIndex) + get { - var products = new PagedList(query, pageIndex, 1000); - - nodes.AddRange(products.Select(x => - { - var node = new XmlSitemapNode - { - Loc = _urlHelper.RouteUrl("Product", new { SeName = x.GetSeName() }, protocol), - LastMod = x.UpdatedOnUtc, - //ChangeFreq = ChangeFrequency.Weekly, - //Priority = 0.8f - }; - - // TODO: add hreflang links if LangCount is > 1 and PrependSeoCode is true - return node; - })); - - _services.DbContext.DetachAll(); - - if (!products.HasNextPage) - break; + return SitemapFileExists(0, out _, out _); } - - return nodes; } - protected virtual IEnumerable GetCustomNodes(string protocol) + public virtual void Invalidate() { - return Enumerable.Empty(); + if (_tenantFolder.DirectoryExists(_siteMapDir)) + { + _tenantFolder.DeleteDirectory(_siteMapDir); + } } - /// - /// Checks the size of the XML sitemap document. If it is over 10MB, logs an error. - /// - /// The sitemap XML document. - private void CheckDocumentSize(string siteMapXml) + #region Nested classes + + class QueryHolder { - if (siteMapXml.Length >= MaximumSiteMapSizeInBytes) + public IQueryable Categories { get; set; } + public IQueryable Manufacturers { get; set; } + public IQueryable Topics { get; set; } + public IQueryable Products { get; set; } + + public int GetTotalRecordCount() { - Logger.Error(new InvalidOperationException($"Sitemap exceeds the maximum size of 10MB. This is because you have unusually long URL's. Consider reducing the MaximumSitemapNodeCount. Size:<{siteMapXml.Length}>")); + int num = 0; + if (Categories != null) num += Categories.Count(); + if (Manufacturers != null) num += Manufacturers.Count(); + if (Topics != null) num += Topics.Count(); + if (Products != null) num += Products.Count(); + + return num; } } - /// - /// Checks the count of the number of sitemaps. If it is over 50,000, logs an error. - /// - /// The sitemap count. - private void CheckSitemapCount(int sitemapCount) + class NamedEntity : BaseEntity, ISlugSupported { - if (sitemapCount > MaximumSiteMapCount) + public string EntityName { get; set; } + public DateTime LastMod { get; set; } + + public override string GetEntityName() { - var ex = new InvalidOperationException($"Sitemap index file exceeds the maximum number of allowed sitemaps of 50,000. Count:<{sitemapCount}>"); - Logger.Warn(ex, ex.Message); + return EntityName; } } - } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs new file mode 100644 index 0000000000..ec5e519913 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace SmartStore.Services.Seo +{ + public class XmlSitemapPartition + { + public string Name { get; set; } + public int Index { get; set; } + public int StoreId { get; set; } + public int LanguageId { get; set; } + public DateTime ModifiedOnUtc { get; set; } + public Stream Stream { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 0d82cf5087..28cc59a3ab 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -582,6 +582,8 @@ + + diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs index a2f0849df8..24ba430c44 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs @@ -11,19 +11,17 @@ public class CompressAttribute : ActionFilterAttribute public override void OnActionExecuting(ActionExecutingContext filterContext) { var response = HttpContext.Current.Response; - string acceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"].EmptyNull().ToLower(); + var acceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"].EmptyNull().ToLower(); if (acceptEncoding.Contains("gzip") && !(PreferDeflate && acceptEncoding.Contains("deflate"))) { response.Filter = new GZipStream(response.Filter, CompressionMode.Compress); - response.Headers.Remove("Content-Encoding"); response.AppendHeader("Content-Encoding", "gzip"); } else if (acceptEncoding.Contains("deflate")) { response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress); - response.Headers.Remove("Content-Encoding"); response.AppendHeader("Content-Encoding", "deflate"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs index b382b0e371..68e51ef76c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs @@ -497,8 +497,6 @@ public ActionResult Warnings() { var msg = T("Admin.System.Warnings.TaskScheduler.Fail", _taskScheduler.BaseUrl, exception.Message); - var xxx = T("Admin.System.Warnings.TaskScheduler.Fail"); - model.Add(new SystemWarningModel { Level = SystemWarningLevel.Fail, @@ -513,7 +511,7 @@ public ActionResult Warnings() string sitemapUrl = null; try { - sitemapUrl = WebHelper.GetAbsoluteUrl(Url.RouteUrl("SitemapSEO"), this.Request); + sitemapUrl = WebHelper.GetAbsoluteUrl(Url.RouteUrl("XmlSitemap"), this.Request); var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(sitemapUrl)); request.Method = "HEAD"; request.Timeout = 15000; diff --git a/src/Presentation/SmartStore.Web/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Controllers/CommonController.cs index d54963f455..bd0bf71a6f 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CommonController.cs @@ -651,7 +651,7 @@ public ActionResult RobotsTextFile() var sb = new StringBuilder(); sb.Append("User-agent: *"); sb.Append(newLine); - sb.AppendFormat("Sitemap: {0}", Url.RouteUrl("SitemapSEO", (object)null, _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http")); + sb.AppendFormat("Sitemap: {0}", Url.RouteUrl("XmlSitemap", (object)null, _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http")); sb.AppendLine(); var disallows = disallowPaths.Concat(localizableDisallowPaths); diff --git a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs index 396a335bce..9d6369178a 100644 --- a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs @@ -21,7 +21,7 @@ namespace SmartStore.Web.Controllers { - public partial class HomeController : PublicControllerBase + public partial class HomeController : PublicControllerBase { private readonly Lazy _categoryService; private readonly Lazy _productService; @@ -29,10 +29,8 @@ public partial class HomeController : PublicControllerBase private readonly Lazy _catalogSearchService; private readonly Lazy _catalogHelper; private readonly Lazy _topicService; - private readonly Lazy _sitemapGenerator; private readonly Lazy _captchaSettings; private readonly Lazy _commonSettings; - private readonly Lazy _seoSettings; private readonly Lazy _customerSettings; private readonly Lazy _privacySettings; @@ -43,10 +41,8 @@ public HomeController( Lazy catalogSearchService, Lazy catalogHelper, Lazy topicService, - Lazy sitemapGenerator, Lazy captchaSettings, Lazy commonSettings, - Lazy seoSettings, Lazy customerSettings, Lazy privacySettings) { @@ -56,10 +52,8 @@ public HomeController( _catalogSearchService = catalogSearchService; _catalogHelper = catalogHelper; _topicService = topicService; - _sitemapGenerator = sitemapGenerator; _captchaSettings = captchaSettings; _commonSettings = commonSettings; - _seoSettings = seoSettings; _customerSettings = customerSettings; _privacySettings = privacySettings; } @@ -143,22 +137,6 @@ public ActionResult ContactUsSend(ContactUsModel model, bool captchaValid) return View(model); } - [RequireHttpsByConfigAttribute(SslRequirement.No)] - public ActionResult SitemapSeo(int? index = null) - { - if (!_seoSettings.Value.XmlSitemapEnabled) - return HttpNotFound(); - - string content = _sitemapGenerator.Value.GetSitemap(index); - - if (content == null) - { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Sitemap index is out of range."); - } - - return Content(content, "text/xml", Encoding.UTF8); - } - [RequireHttpsByConfigAttribute(SslRequirement.No)] public ActionResult Sitemap() { diff --git a/src/Presentation/SmartStore.Web/Controllers/MediaController.cs b/src/Presentation/SmartStore.Web/Controllers/MediaController.cs index 81f4b57773..dec8ab499d 100644 --- a/src/Presentation/SmartStore.Web/Controllers/MediaController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/MediaController.cs @@ -15,6 +15,10 @@ using SmartStore.Core.Events; using SmartStore.Web.Framework.Security; using SmartStore.Core.Data; +using SmartStore.Web.Framework.Filters; +using System.Net; +using SmartStore.Core.Domain.Seo; +using SmartStore.Services.Seo; namespace SmartStore.Web.Controllers { @@ -36,6 +40,9 @@ public partial class MediaController : Controller private readonly IMediaFileSystem _mediaFileSystem; private readonly MediaSettings _mediaSettings; + private readonly Lazy _seoSettings; + private readonly Lazy _sitemapGenerator; + public MediaController( IPictureService pictureService, IImageProcessor imageProcessor, @@ -43,7 +50,9 @@ public MediaController( IUserAgent userAgent, IEventPublisher eventPublisher, IMediaFileSystem mediaFileSystem, - MediaSettings mediaSettings) + MediaSettings mediaSettings, + Lazy seoSettings, + Lazy sitemapGenerator) { _pictureService = pictureService; _imageProcessor = imageProcessor; @@ -52,12 +61,40 @@ public MediaController( _eventPublisher = eventPublisher; _mediaFileSystem = mediaFileSystem; _mediaSettings = mediaSettings; + _seoSettings = seoSettings; + _sitemapGenerator = sitemapGenerator; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } + #region XML sitemap + + [Compress] + [RequireHttpsByConfigAttribute(SslRequirement.No)] + public ActionResult XmlSitemap(int? index = null) + { + if (!_seoSettings.Value.XmlSitemapEnabled) + return HttpNotFound(); + + try + { + var partition = _sitemapGenerator.Value.GetSitemapPart(index ?? 0); + return new FileStreamResult(partition.Stream, "text/xml"); + } + catch (IndexOutOfRangeException) + { + return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Sitemap index is out of range."); + } + catch (Exception ex) + { + return new HttpStatusCodeResult(HttpStatusCode.InternalServerError, ex.Message); + } + } + + #endregion + public async Task Image(int id /* pictureId*/, string name) { string nameWithoutExtension = null; diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs b/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs index dfd8814bd6..0ace1412ee 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs @@ -319,9 +319,9 @@ public void RegisterRoutes(RouteCollection routes) new { controller = "Home", action = "Sitemap" }, new[] { "SmartStore.Web.Controllers" }); - routes.MapLocalizedRoute("SitemapSEO", + routes.MapLocalizedRoute("XmlSitemap", "sitemap.xml", - new { controller = "Home", action = "SitemapSeo" }, + new { controller = "Media", action = "XmlSitemap" }, new[] { "SmartStore.Web.Controllers" }); routes.MapLocalizedRoute("StoreClosed", diff --git a/src/Presentation/SmartStore.Web/Themes/Flex/Content/_variables.scss b/src/Presentation/SmartStore.Web/Themes/Flex/Content/_variables.scss index eb31abb906..16b59ce329 100644 --- a/src/Presentation/SmartStore.Web/Themes/Flex/Content/_variables.scss +++ b/src/Presentation/SmartStore.Web/Themes/Flex/Content/_variables.scss @@ -104,6 +104,11 @@ $yiq-contrasted-threshold: 164; $yiq-text-dark: $gray-900; $yiq-text-light: #fff; +$spacer: 1.25rem; +$spacers: ( + 6: ($spacer * 4.5) +); + $theme-colors: ( "gray": $gray-700 ); From 19f100981448825544575a24804b41c3961d8826 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 17 Nov 2018 04:42:19 +0100 Subject: [PATCH 026/657] (Perf) New XML sitemap generation strategy (wip) --- .../Seo/IXmlSitemapGenerator.cs | 13 +- .../Seo/Tasks/RebuildXmlSitemapTask.cs | 30 ++- .../Seo/XmlSitemapBuildContext.cs | 3 + .../Seo/XmlSitemapGenerator.cs | 205 +++++++++++------- .../Localization/LocalizedRoute.cs | 9 +- 5 files changed, 160 insertions(+), 100 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs index f5764b8ffc..f1c7b37d22 100644 --- a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs @@ -21,31 +21,30 @@ public partial interface IXmlSitemapGenerator XmlSitemapPartition GetSitemapPart(int index = 0); /// - /// Rebuilds the collection of XML sitemap documents for the current site. If there are less than 1.000 sitemap + /// Rebuilds the collection of XML sitemap documents for a store/language combination. If there are less than 1.000 sitemap /// nodes, only one sitemap document will exist in the collection, otherwise a sitemap index document will be /// the first entry in the collection and all other entries will be sitemap XML documents. /// - /// Optional callback for progress change - /// Cancellation token + /// The build context /// /// During rebuilding, requests are being served from the existing cache. /// Once rebuild is completed, the cache is updated. /// - void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null); + void Rebuild(XmlSitemapBuildContext ctx); /// /// Determines whether a rebuild is already running. /// - bool IsRebuilding { get; } + bool IsRebuilding(int storeId, int languageId); /// /// Indicates whether the sitemap has been generated and cached. /// - bool IsGenerated { get; } + bool IsGenerated(int storeId, int languageId); /// /// Removes the sitemap from the cache for a rebuild. /// - void Invalidate(); + void Invalidate(int storeId, int languageId); } } diff --git a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs index c7090b1996..bfd1186699 100644 --- a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs +++ b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs @@ -4,32 +4,46 @@ using System.Text; using System.Threading.Tasks; using SmartStore.Core.Domain.Seo; +using SmartStore.Services.Localization; +using SmartStore.Services.Stores; using SmartStore.Services.Tasks; namespace SmartStore.Services.Seo { public class RebuildXmlSitemapTask : ITask { + private readonly IStoreService _storeService; + private readonly ILanguageService _languageService; private readonly IXmlSitemapGenerator _generator; private readonly SeoSettings _seoSettings; - public RebuildXmlSitemapTask(IXmlSitemapGenerator generator, SeoSettings seoSettings) + public RebuildXmlSitemapTask( + IStoreService storeService, + ILanguageService languageService, + IXmlSitemapGenerator generator, + SeoSettings seoSettings) { + _storeService = storeService; + _languageService = languageService; _generator = generator; _seoSettings = seoSettings; } public void Execute(TaskExecutionContext ctx) { - //if (_generator.IsGenerated) - //{ - // _generator.Invalidate(); - //} + var stores = _storeService.GetAllStores(); - //// enforces refresh - //_generator.GetSitemap(0); + foreach (var store in stores) + { + var languages = _languageService.GetAllLanguages(false, store.Id); + var buildContext = new XmlSitemapBuildContext(store, languages.ToArray()) + { + CancellationToken = ctx.CancellationToken, + ProgressCallback = OnProgress + }; - _generator.Rebuild(ctx.CancellationToken, OnProgress); + _generator.Rebuild(buildContext); + } void OnProgress(int value, int max, string msg) { diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs index d67af75f96..6aa8504f00 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs @@ -15,11 +15,14 @@ public XmlSitemapBuildContext(Store store, Language[] languages) Store = store; Languages = languages; + Protocol = Store.ForceSslForAllPages ? "https" : "http"; } public CancellationToken CancellationToken { get; set; } public ProgressCallback ProgressCallback { get; set; } public Store Store { get; set; } public Language[] Languages { get; set; } + + public string Protocol { get; private set; } } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index f31e4b873d..31de7e12d6 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -18,13 +18,13 @@ using SmartStore.Services.Customers; using SmartStore.Services.Localization; using SmartStore.Services.Search; -using SmartStore.Services.Tasks; using SmartStore.Services.Topics; using SmartStore.Core.IO; -using SmartStore.Utilities.Threading; using SmartStore.Utilities; using System.IO; -using System.Threading.Tasks; +using SmartStore.Collections; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Services.Seo { @@ -66,11 +66,8 @@ public partial class XmlSitemapGenerator : IXmlSitemapGenerator private readonly ILockFileManager _lockFileManager; private readonly UrlHelper _urlHelper; - private readonly int _storeId; - private readonly int _langId; private readonly IVirtualFolder _tenantFolder; private readonly string _baseDir; - private readonly string _siteMapDir; public XmlSitemapGenerator( ICategoryService categoryService, @@ -101,11 +98,8 @@ public XmlSitemapGenerator( _lockFileManager = lockFileManager; _urlHelper = urlHelper; - _storeId = _services.StoreContext.CurrentStore.Id; - _langId = _services.WorkContext.WorkingLanguage.Id; _tenantFolder = _services.ApplicationEnvironment.TenantFolder; _baseDir = _tenantFolder.Combine("Sitemaps"); - _siteMapDir = _tenantFolder.Combine(_baseDir, _storeId + "/" + _langId); Logger = NullLogger.Instance; @@ -122,7 +116,10 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) { Guard.NotNegative(index, nameof(index)); - var exists = SitemapFileExists(index, out var path, out var name); + var storeId = _services.StoreContext.CurrentStore.Id; + var langId = _services.WorkContext.WorkingLanguage.Id; + + var exists = SitemapFileExists(storeId, langId, index, out var path, out var name); if (exists) { @@ -130,8 +127,8 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) { Index = index, Name = name, - LanguageId = _langId, - StoreId = _storeId, + LanguageId = langId, + StoreId = storeId, ModifiedOnUtc = _tenantFolder.GetFileLastWriteTimeUtc(path), Stream = _tenantFolder.OpenFile(path) }; @@ -152,7 +149,7 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) // If the main file (index 0) exists, the action should return NotFoundResult, // otherwise the rebuild process should be started or waited for. - if (SitemapFileExists(0, out path, out name)) + if (SitemapFileExists(storeId, langId, 0, out path, out name)) { throw new IndexOutOfRangeException("The sitemap file '{0}' does not exist.".FormatInvariant(name)); } @@ -161,31 +158,37 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) // The main sitemap document with index 0 does not exist, meaning: the whole sitemap // needs to be created and cached by partitions. - if (IsRebuilding) + if (IsRebuilding(storeId, langId)) { // The rebuild process is already running, either started // by the task scheduler or another HTTP request. // We should wait for completion. - //while (IsRebuilding) - //{ - // //Thread.Sleep(500); - // Task.Delay(500).Wait(); - //} + Thread.Sleep(500); + + while (IsRebuilding(storeId, langId)) + { + Thread.Sleep(500); + } } else { // No lock. Rebuild now. - Rebuild(CancellationToken.None); + var buildContext = new XmlSitemapBuildContext(_services.StoreContext.CurrentStore, new[] { _services.WorkContext.WorkingLanguage }) + { + CancellationToken = CancellationToken.None + }; + + Rebuild(buildContext); } - // DRY: call self to get sitemap partiition object + // DRY: call self to get sitemap partition object return GetSitemapPart(index, true); } - private bool SitemapFileExists(int index, out string path, out string name) + private bool SitemapFileExists(int storeId, int languageId, int index, out string path, out string name) { - path = BuildSitemapFilePath(index, out name); + path = BuildSitemapFilePath(storeId, languageId, index, out name); // Does not work reliably with symlinks due to framework caching //var exists = _tenantFolder.FileExists(path); @@ -201,27 +204,56 @@ private bool SitemapFileExists(int index, out string path, out string name) return exists; } - private string BuildSitemapFilePath(int index, out string fileName) + private string BuildSitemapFilePath(int storeId, int languageId, int index, out string fileName) { fileName = SiteMapFileNamePattern.FormatInvariant(index); - return _tenantFolder.Combine(_siteMapDir, fileName); + return _tenantFolder.Combine(BuildSitemapDirPath(storeId, languageId), fileName); + } + + private string BuildSitemapDirPath(int storeId, int languageId) + { + return _tenantFolder.Combine(_baseDir, storeId + "/" + languageId); } - private string GetLockFilePath() + private string GetLockFilePath(int storeId, int languageId) { - var fileName = LockFileNamePattern.FormatInvariant(_storeId, _langId); + var fileName = LockFileNamePattern.FormatInvariant(storeId, languageId); return _tenantFolder.Combine(_baseDir, fileName); } - public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallback callback = null) + public virtual void Rebuild(XmlSitemapBuildContext ctx) { - if (!_lockFileManager.TryAcquireLock(GetLockFilePath(), out var lockFile)) + Guard.NotNull(ctx, nameof(ctx)); + + var lockFiles = new List(); + + foreach (var language in ctx.Languages) + { + if (_lockFileManager.TryAcquireLock(GetLockFilePath(ctx.Store.Id, language.Id), out var lockFile)) + { + // Process only languages that are unlocked right now + // It is possible that an HTTP request triggered the generation + // of a language specific sitemap. + lockFiles.Add(lockFile); + } + } + + if (lockFiles.Count == 0) { - Logger.Warn("XML Sitemap rebuild already in process."); + Logger.Warn("XML sitemap rebuild already in process."); return; } - using (lockFile) + // All sitemaps grouped by language + var sitemaps = new Multimap(); + + var compositeFileLock = new ActionDisposable(() => + { + lockFiles.Each(x => x.Dispose()); + lockFiles.Clear(); + }); + + using (compositeFileLock) { // Impersonate var prevCustomer = _services.WorkContext.CurrentCustomer; @@ -230,9 +262,10 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac try { - var protocol = _services.StoreContext.CurrentStore.ForceSslForAllPages ? "https" : "http"; + var protocol = ctx.Protocol; + var languageIds = ctx.Languages.Select(x => x.Id).Concat(new[] { 0 }).ToArray(); var nodes = new List(); - var queries = CreateQueries(); + var queries = CreateQueries(ctx.Store.Id); var total = queries.GetTotalRecordCount(); using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) @@ -243,53 +276,59 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac var numProcessed = 0; foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) { - if (cancellationToken.IsCancellationRequested) + if (ctx.CancellationToken.IsCancellationRequested) { break; } numProcessed = ++segment * MaximumSiteMapNodeCount; - callback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); + ctx.ProgressCallback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); var firstEntityName = batch.First().EntityName; var lastEntityName = batch.Last().EntityName; - var slugs = GetUrlRecordCollectionsForBatch(batch, _langId); + var slugs = GetUrlRecordCollectionsForBatch(batch, languageIds); - nodes.AddRange(batch.Select(x => new XmlSitemapNode + foreach (var language in ctx.Languages) { - LastMod = x.LastMod, - Loc = _urlHelper.RouteUrl(x.EntityName, new { SeName = slugs[x.EntityName].GetSlug(_langId, x.Id, true) }, protocol) - })); + sitemaps[language.Id].AddRange(batch.Select(x => new XmlSitemapNode + { + LastMod = x.LastMod, + Loc = _urlHelper.RouteUrl(x.EntityName, new { SeName = slugs[x.EntityName].GetSlug(language.Id, x.Id, true) }, protocol) + })); + } } - if (!cancellationToken.IsCancellationRequested) + if (!ctx.CancellationToken.IsCancellationRequested) { - callback?.Invoke(numProcessed, total, "Processing custom nodes".FormatCurrent(numProcessed, total)); - var customNodes = GetCustomNodes(protocol); - if (customNodes != null) - { - nodes.AddRange(customNodes); - } + ctx.ProgressCallback?.Invoke(numProcessed, total, "Processing custom nodes".FormatCurrent(numProcessed, total)); + ProcessCustomNodes(ctx, sitemaps); } } - cancellationToken.ThrowIfCancellationRequested(); + ctx.CancellationToken.ThrowIfCancellationRequested(); - callback?.Invoke(total, total, "Finalizing"); - - if (nodes.Count == 0) + if (sitemaps.Count == 0) { // Ensure that at least one entry exists. Otherwise, // the system will try to rebuild again. - nodes.Add(new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = _urlHelper.RouteUrl("HomePage") }); + foreach (var language in ctx.Languages) + { + sitemaps[language.Id].Add(new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = _urlHelper.RouteUrl("HomePage") }); + } } - var documents = GetSiteMapDocuments(nodes.AsReadOnly(), protocol); + // Save all sitemaps to disk + foreach (var language in ctx.Languages) + { + if (ctx.CancellationToken.IsCancellationRequested) + break; - cancellationToken.ThrowIfCancellationRequested(); + ctx.ProgressCallback?.Invoke(total, total, "Saving sitemaps for '{0}'.".FormatInvariant(language.GetTwoLetterISOLanguageName())); - SaveToDisk(documents); + var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id], protocol); + SaveToDisk(documents, ctx.Store, language); + } } finally { @@ -299,20 +338,22 @@ public virtual void Rebuild(CancellationToken cancellationToken, ProgressCallbac } } - private void SaveToDisk(List documents) + private void SaveToDisk(List documents, Store store, Language language) { - if (_tenantFolder.DirectoryExists(_siteMapDir)) + var sitemapDir = BuildSitemapDirPath(store.Id, language.Id); + + if (_tenantFolder.DirectoryExists(sitemapDir)) { - _tenantFolder.DeleteDirectory(_siteMapDir); + _tenantFolder.DeleteDirectory(sitemapDir); } - _tenantFolder.CreateDirectory(_siteMapDir); + _tenantFolder.CreateDirectory(sitemapDir); for (int i = 0; i < documents.Count; i++) { // Save segment to disk var fileName = SiteMapFileNamePattern.FormatInvariant(i); - var filePath = _tenantFolder.Combine(_siteMapDir, fileName); + var filePath = _tenantFolder.Combine(sitemapDir, fileName); _tenantFolder.CreateTextFile(filePath, documents[i]); } @@ -358,7 +399,7 @@ private IEnumerable EnumerateEntities(QueryHolder queries) while (maxId > 1) { xxx++; - if (xxx >= 100) + if (xxx >= 50) { break; } @@ -385,10 +426,9 @@ private IEnumerable EnumerateEntities(QueryHolder queries) } } - private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int languageId) + private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int[] languageIds) { var result = new Dictionary(); - var languageIds = new[] { languageId, 0 }; if (batch.First().EntityName == "Product") { @@ -550,13 +590,18 @@ private string GetSitemapDocument(IEnumerable nodes) return xml; } - private QueryHolder CreateQueries() + private QueryHolder CreateQueries(int storeId) { + if (_services.StoreService.IsSingleStoreMode()) + { + storeId = 0; + } + var holder = new QueryHolder(); if (_seoSettings.XmlSitemapIncludesCategories) { - holder.Categories = _categoryService.GetAllCategories(showHidden: false, storeId: _services.StoreContext.CurrentStore.Id).SourceQuery; + holder.Categories = _categoryService.GetAllCategories(showHidden: false, storeId: storeId).SourceQuery; } if (_seoSettings.XmlSitemapIncludesManufacturers) @@ -566,7 +611,7 @@ private QueryHolder CreateQueries() if (_seoSettings.XmlSitemapIncludesTopics) { - holder.Topics = _topicService.GetAllTopics(_services.StoreContext.CurrentStore.Id).AlterQuery(q => + holder.Topics = _topicService.GetAllTopics(storeId).AlterQuery(q => { return q.Where(t => t.IncludeInSitemap && !t.RenderAsWidget); }).SourceQuery; @@ -577,7 +622,7 @@ private QueryHolder CreateQueries() var searchQuery = new CatalogSearchQuery() .VisibleOnly() .VisibleIndividuallyOnly(true) - .HasStoreId(_services.StoreContext.CurrentStoreIdIfMultiStoreMode); + .HasStoreId(storeId); holder.Products = _catalogSearchService.PrepareQuery(searchQuery); } @@ -590,6 +635,10 @@ protected virtual IEnumerable GetCustomNodes(string protocol) return Enumerable.Empty(); } + protected void ProcessCustomNodes(XmlSitemapBuildContext ctx, Multimap sitemaps) + { + } + /// /// Checks the size of the XML sitemap document. If it is over 10MB, logs an error. /// @@ -615,27 +664,23 @@ private void CheckSitemapCount(int sitemapCount) } } - public bool IsRebuilding + public bool IsRebuilding(int storeId, int languageId) { - get - { - return _lockFileManager.IsLocked(GetLockFilePath()); - } + return _lockFileManager.IsLocked(GetLockFilePath(storeId, languageId)); } - public virtual bool IsGenerated + public virtual bool IsGenerated(int storeId, int languageId) { - get - { - return SitemapFileExists(0, out _, out _); - } + return SitemapFileExists(storeId, languageId, 0, out _, out _); } - public virtual void Invalidate() + public virtual void Invalidate(int storeId, int languageId) { - if (_tenantFolder.DirectoryExists(_siteMapDir)) + var dir = BuildSitemapDirPath(storeId, languageId); + + if (_tenantFolder.DirectoryExists(dir)) { - _tenantFolder.DeleteDirectory(_siteMapDir); + _tenantFolder.DeleteDirectory(dir); } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs index c29c145a8c..382574d0bd 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs @@ -124,15 +124,14 @@ public override VirtualPathData GetVirtualPath(RequestContext requestContext, Ro if (data != null && DataSettings.DatabaseIsInstalled() && SeoFriendlyUrlsForLanguagesEnabled) { var helper = new LocalizedUrlHelper(requestContext.HttpContext.Request, true); - string cultureCode; - if (helper.IsLocalizedUrl(out cultureCode)) - { + if (helper.IsLocalizedUrl(out string cultureCode)) + { if (!requestContext.RouteData.Values.ContainsKey("StripInvalidSeoCode")) { data.VirtualPath = String.Concat(cultureCode, "/", data.VirtualPath).TrimEnd('/'); } - } - } + } + } return data; } From f7ec4f7daa118c4f076227020b8629796d20d0ba Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 19 Nov 2018 09:43:11 +0100 Subject: [PATCH 027/657] Search: Removed obsolete ISearchBits. Does not support index segmentation in this form anyway. --- src/Libraries/SmartStore.Core/Search/ISearchBits.cs | 12 ------------ .../SmartStore.Core/Search/ISearchEngine.cs | 6 ------ src/Libraries/SmartStore.Core/SmartStore.Core.csproj | 1 - 3 files changed, 19 deletions(-) delete mode 100644 src/Libraries/SmartStore.Core/Search/ISearchBits.cs diff --git a/src/Libraries/SmartStore.Core/Search/ISearchBits.cs b/src/Libraries/SmartStore.Core/Search/ISearchBits.cs deleted file mode 100644 index c9ba405ef8..0000000000 --- a/src/Libraries/SmartStore.Core/Search/ISearchBits.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace SmartStore.Core.Search -{ - public interface ISearchBits - { - ISearchBits And(ISearchBits other); - ISearchBits Or(ISearchBits other); - ISearchBits Xor(ISearchBits other); - long Count(); - } -} diff --git a/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs b/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs index 2a17f673b0..12235f2d44 100644 --- a/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs +++ b/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs @@ -17,12 +17,6 @@ public interface ISearchEngine /// Search hit ISearchHit Get(int id); - /// - /// Get search bits - /// - /// Search bits - ISearchBits GetBits(); - /// /// Get total number of search hits /// diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 4a2d409068..df978dc456 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -469,7 +469,6 @@ - From 2fd5885ea0cd547391e992c900d86a2feb0903da Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 19 Nov 2018 10:41:55 +0100 Subject: [PATCH 028/657] Fixes compilation error --- .../Data/Hooks/DefaultDbHookHandlerTests.cs | 3 +-- .../Public/Infrastructure/RoutesTests.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Tests/SmartStore.Core.Tests/Data/Hooks/DefaultDbHookHandlerTests.cs b/src/Tests/SmartStore.Core.Tests/Data/Hooks/DefaultDbHookHandlerTests.cs index a7d66ec9a3..e2de1576d1 100644 --- a/src/Tests/SmartStore.Core.Tests/Data/Hooks/DefaultDbHookHandlerTests.cs +++ b/src/Tests/SmartStore.Core.Tests/Data/Hooks/DefaultDbHookHandlerTests.cs @@ -150,8 +150,7 @@ public void Can_handle_importance() { HookedType = typeof(TEntity), ImplType = typeof(THook), - Important = typeof(THook).GetAttribute(false) != null, - IsLoadHook = typeof(IDbLoadHook).IsAssignableFrom(typeof(THook)) + Important = typeof(THook).GetAttribute(false) != null }); return hook; diff --git a/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs b/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs index a92d7b3f43..a27b327a94 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs +++ b/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs @@ -162,7 +162,7 @@ public void ReturnRequest_routes() public void Common_routes() { "~/contactus".ShouldMapTo(c => c.ContactUs()); - "~/sitemap.xml".ShouldMapTo(c => c.SitemapSeo(1)); + "~/sitemap.xml".ShouldMapTo(c => c.XmlSitemap(1)); } [Test] From 3524dad3836fb1032cae016fc9acb04483883757 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 19 Nov 2018 15:02:41 +0100 Subject: [PATCH 029/657] Perf: Speed up price calculation in product lists --- .../Catalog/PriceCalculationService.cs | 27 ++++---- .../Controllers/CatalogHelper.MapProduct.cs | 61 ++++++++----------- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index c91b3f533f..fb33efe0a8 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -712,26 +712,29 @@ public virtual decimal GetPreselectedPrice(Product product, Customer customer, C context = CreatePriceCalculationContext(customer: customer); } - if (product.ProductType == ProductType.BundledProduct) - { + if (product.ProductType == ProductType.BundledProduct) + { var bundleItems = context.ProductBundleItems .GetOrLoad(product.Id) .Select(x => new ProductBundleItemData(x)) .ToList(); - var bundleItemContext = CreatePriceCalculationContext(bundleItems.Select(x => x.Item.Product), customer); + + var productIds = bundleItems.Select(x => x.Item.ProductId).ToList(); + productIds.Add(product.Id); + context.Collect(productIds); // Fetch bundleItems.AdditionalCharge for all bundle items. foreach (var bundleItem in bundleItems.Where(x => x.Item.Product.CanBeBundleItem())) - { - var unused = GetPreselectedPrice(bundleItem.Item.Product, customer, currency, bundleItemContext, bundleItem, bundleItems); - } + { + var unused = GetPreselectedPrice(bundleItem.Item.Product, customer, currency, context, bundleItem, bundleItems); + } - result = GetPreselectedPrice(product, customer, currency, context, null, bundleItems); - } - else - { - result = GetPreselectedPrice(product, customer, currency, context, null, null); - } + result = GetPreselectedPrice(product, customer, currency, context, null, bundleItems); + } + else + { + result = GetPreselectedPrice(product, customer, currency, context, null, null); + } return result; } diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs index 54c8b446cc..68c961891d 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.MapProduct.cs @@ -227,14 +227,9 @@ public virtual ProductSummaryModel MapProductSummaryModel(IPagedList pr if (settings.MapLegalInfo) { var shippingInfoUrl = _urlHelper.TopicUrl("shippinginfo"); - if (shippingInfoUrl.HasValue()) - { - legalInfo = T("Tax.LegalInfoShort").Text.FormatInvariant(taxInfo, shippingInfoUrl); - } - else - { - legalInfo = T("Tax.LegalInfoShort2").Text.FormatInvariant(taxInfo); - } + legalInfo = shippingInfoUrl.HasValue() + ? T("Tax.LegalInfoShort").Text.FormatInvariant(taxInfo, shippingInfoUrl) + : T("Tax.LegalInfoShort2").Text.FormatInvariant(taxInfo); } if (prefetchSlugs) @@ -532,30 +527,23 @@ private void MapProductSummaryItem(Product product, MapProductSummaryItemContext item.DeliveryTimeHexValue = deliveryTime.ColorHexValue; } - if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock) - { - item.DisplayDeliveryTimeAccordingToStock = product.StockQuantity > 0 || (product.StockQuantity <= 0 && _catalogSettings.DeliveryTimeIdForEmptyStock.HasValue); - } - else - { - item.DisplayDeliveryTimeAccordingToStock = true; - } + item.DisplayDeliveryTimeAccordingToStock = product.ManageInventoryMethod == ManageInventoryMethod.ManageStock + ? product.StockQuantity > 0 || (product.StockQuantity <= 0 && _catalogSettings.DeliveryTimeIdForEmptyStock.HasValue) + : true; if (product.DisplayStockAvailability && product.ManageInventoryMethod == ManageInventoryMethod.ManageStock) { if (product.StockQuantity > 0) { - if (product.DisplayStockQuantity) - item.StockAvailablity = T("Products.Availability.InStockWithQuantity", product.StockQuantity); - else - item.StockAvailablity = T("Products.Availability.InStock"); + item.StockAvailablity = product.DisplayStockQuantity + ? T("Products.Availability.InStockWithQuantity", product.StockQuantity) + : T("Products.Availability.InStock"); } else { - if (product.BackorderMode == BackorderMode.NoBackorders || product.BackorderMode == BackorderMode.AllowQtyBelow0) - item.StockAvailablity = T("Products.Availability.OutOfStock"); - else if (product.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) - item.StockAvailablity = T("Products.Availability.Backordering"); + item.StockAvailablity = product.BackorderMode == BackorderMode.NoBackorders || product.BackorderMode == BackorderMode.AllowQtyBelow0 + ? T("Products.Availability.OutOfStock") + : T("Products.Availability.Backordering"); } } @@ -614,7 +602,7 @@ private decimal MapSummaryItemPrice(Product product, ref Product contextProduct, var finalPriceBase = decimal.Zero; var finalPrice = decimal.Zero; var displayPrice = decimal.Zero; - ICollection associatedProducts = null; + ICollection associatedProducts = null; var priceModel = new ProductSummaryModel.PriceModel(); item.Price = priceModel; @@ -644,11 +632,7 @@ private decimal MapSummaryItemPrice(Product product, ref Product contextProduct, .ThenBy(x => x.DisplayOrder); ctx.GroupedProducts = allAssociatedProducts.ToMultimap(x => x.ParentGroupedProductId, x => x); - - if (ctx.GroupedProducts.Any()) - { - ctx.BatchContext.AppliedDiscounts.Collect(allAssociatedProducts.Select(x => x.Id)); - } + ctx.AssociatedProductBatchContext = _dataExporter.Value.CreateProductExportContext(allAssociatedProducts, ctx.Customer, null, null, false); } associatedProducts = ctx.GroupedProducts[product.Id]; @@ -666,7 +650,7 @@ private decimal MapSummaryItemPrice(Product product, ref Product contextProduct, priceModel.AvailableForPreOrder = product.AvailableForPreOrder; } - // Return if no pricing at all. + // Return if there's no pricing at all. if (contextProduct == null || contextProduct.CustomerEntersPrice || !ctx.AllowPrices || _catalogSettings.PriceDisplayType == PriceDisplayType.Hide) { return finalPrice; @@ -686,14 +670,16 @@ private decimal MapSummaryItemPrice(Product product, ref Product contextProduct, return finalPrice; } - // Calculate prices. + // Calculate prices. + var batchContext = product.ProductType == ProductType.GroupedProduct ? ctx.AssociatedProductBatchContext : ctx.BatchContext; + if (_catalogSettings.PriceDisplayType == PriceDisplayType.PreSelectedPrice) { - displayPrice = _priceCalculationService.GetPreselectedPrice(contextProduct, ctx.Customer, ctx.Currency, ctx.BatchContext); + displayPrice = _priceCalculationService.GetPreselectedPrice(contextProduct, ctx.Customer, ctx.Currency, batchContext); } else if (_catalogSettings.PriceDisplayType == PriceDisplayType.PriceWithoutDiscountsAndAttributes) { - displayPrice = _priceCalculationService.GetFinalPrice(contextProduct, null, ctx.Customer, decimal.Zero, false, 1, null, ctx.BatchContext); + displayPrice = _priceCalculationService.GetFinalPrice(contextProduct, null, ctx.Customer, decimal.Zero, false, 1, null, batchContext); } else { @@ -701,11 +687,11 @@ private decimal MapSummaryItemPrice(Product product, ref Product contextProduct, if (product.ProductType == ProductType.GroupedProduct) { displayFromMessage = true; - displayPrice = _priceCalculationService.GetLowestPrice(product, ctx.Customer, ctx.BatchContext, associatedProducts, out contextProduct) ?? decimal.Zero; + displayPrice = _priceCalculationService.GetLowestPrice(product, ctx.Customer, batchContext, associatedProducts, out contextProduct) ?? decimal.Zero; } else { - displayPrice = _priceCalculationService.GetLowestPrice(product, ctx.Customer, ctx.BatchContext, out displayFromMessage); + displayPrice = _priceCalculationService.GetLowestPrice(product, ctx.Customer, batchContext, out displayFromMessage); } } @@ -801,7 +787,8 @@ private class MapProductSummaryItemContext public ProductSummaryModel Model { get; set; } public ProductSummaryMappingSettings Settings { get; set; } public ProductExportContext BatchContext { get; set; } - public Multimap GroupedProducts { get; set; } + public ProductExportContext AssociatedProductBatchContext { get; set; } + public Multimap GroupedProducts { get; set; } public Dictionary CachedManufacturerModels { get; set; } public IDictionary PictureInfos { get; set; } public Dictionary Resources { get; set; } From 835c50ce0a7f76b3eff2bf329078df52c4413eda Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 19 Nov 2018 16:41:51 +0100 Subject: [PATCH 030/657] Fixes product display order on category and manufacturer pages sometimes wrong when using linq search --- changelog.md | 3 ++- .../Search/Catalog/CatalogSearchQuery.cs | 18 ++++++----------- .../Catalog/LinqCatalogSearchService.cs | 20 ++++++++++++------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/changelog.md b/changelog.md index fd7247162a..3f1b0c8287 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# Release Notes +# Release Notes ## SmartStore.NET 3.2 @@ -93,6 +93,7 @@ * Region cannot be selected in checkout when entering a billing or shipping address * Fixed invalid conversion of "System.Int32" to "SmartStore.Core.Domain.Tax.VatNumberStatus" when placing an order * MegaMenu: Improved item rendering for third tier elements +* Product display order on category and manufacturer pages sometimes wrong when using linq search. ## SmartStore.NET 3.1.5 diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs index ca01fd89cd..cfc21d3a9f 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs @@ -206,12 +206,9 @@ public CatalogSearchQuery WithCategoryIds(bool? featuredOnly, params int[] ids) return this; } - string fieldName = null; - - if (featuredOnly.HasValue) - fieldName = (featuredOnly.Value ? "featuredcategoryid" : "notfeaturedcategoryid"); - else - fieldName = "categoryid"; + var fieldName = featuredOnly.HasValue + ? featuredOnly.Value ? "featuredcategoryid" : "notfeaturedcategoryid" + : "categoryid"; return WithFilter(SearchFilter.Combined(ids.Select(x => SearchFilter.ByField(fieldName, x).ExactMatch().NotAnalyzed()).ToArray())); } @@ -236,12 +233,9 @@ public CatalogSearchQuery WithManufacturerIds(bool? featuredOnly, params int[] i return this; } - string fieldName = null; - - if (featuredOnly.HasValue) - fieldName = (featuredOnly.Value ? "featuredmanufacturerid" : "notfeaturedmanufacturerid"); - else - fieldName = "manufacturerid"; + var fieldName = featuredOnly.HasValue + ? featuredOnly.Value ? "featuredmanufacturerid" : "notfeaturedmanufacturerid" + : "manufacturerid"; return WithFilter(SearchFilter.Combined(ids.Select(x => SearchFilter.ByField(fieldName, x).ExactMatch().NotAnalyzed()).ToArray())); } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs index aee829e7f3..7758cbb6ec 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs @@ -132,6 +132,8 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ var ordered = false; var utcNow = DateTime.UtcNow; var isGroupingRequired = false; + var categoryId = 0; + var manufacturerId = 0; var query = baseQuery ?? _productRepository.Table; query = query.Where(x => !x.Deleted && !x.IsSystemProduct); @@ -151,7 +153,8 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ var categoryIds = GetIdList(filters, "categoryid"); if (categoryIds.Any()) { - if (categoryIds.Count == 1 && categoryIds.First() == 0) + categoryId = categoryIds.First(); + if (categoryIds.Count == 1 && categoryId == 0) { // Has no category. query = query.Where(x => x.ProductCategories.Count == 0); @@ -168,20 +171,23 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ if (featuredCategoryIds.Any()) { isGroupingRequired = true; + categoryId = categoryId == 0 ? featuredCategoryIds.First() : categoryId; query = QueryCategories(query, featuredCategoryIds, true); } if (notFeaturedCategoryIds.Any()) { isGroupingRequired = true; + categoryId = categoryId == 0 ? notFeaturedCategoryIds.First() : categoryId; query = QueryCategories(query, notFeaturedCategoryIds, false); } var manufacturerIds = GetIdList(filters, "manufacturerid"); if (manufacturerIds.Any()) { - if (manufacturerIds.Count == 1 && manufacturerIds.First() == 0) + manufacturerId = manufacturerIds.First(); + if (manufacturerIds.Count == 1 && manufacturerId == 0) { - // has no manufacturer + // Has no manufacturer. query = query.Where(x => x.ProductManufacturers.Count == 0); } else @@ -196,11 +202,13 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ if (featuredManuIds.Any()) { isGroupingRequired = true; + manufacturerId = manufacturerId == 0 ? featuredManuIds.First() : manufacturerId; query = QueryManufacturers(query, featuredManuIds, true); } if (notFeaturedManuIds.Any()) { isGroupingRequired = true; + manufacturerId = manufacturerId == 0 ? notFeaturedManuIds.First() : manufacturerId; query = QueryManufacturers(query, notFeaturedManuIds, false); } @@ -491,14 +499,12 @@ orderby grp.Key if (sort.FieldName.IsEmpty()) { // Sort by relevance. - if (categoryIds.Any()) + if (categoryId != 0) { - var categoryId = categoryIds.First(); query = OrderBy(ref ordered, query, x => x.ProductCategories.Where(pc => pc.CategoryId == categoryId).FirstOrDefault().DisplayOrder); } - else if (manufacturerIds.Any()) + else if (manufacturerId != 0) { - var manufacturerId = manufacturerIds.First(); query = OrderBy(ref ordered, query, x => x.ProductManufacturers.Where(pm => pm.ManufacturerId == manufacturerId).FirstOrDefault().DisplayOrder); } else if (FindFilter(searchQuery.Filters, "parentid") != null) From 69ffde3a3b5570a6137a09ad20a03d897f0cf7a4 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Mon, 19 Nov 2018 22:39:24 +0100 Subject: [PATCH 031/657] (Perf) New XML sitemap generation strategy (wip) --- .../Localization/LocalizedEntityService.cs | 20 ++- .../Seo/UrlRecordService.cs | 20 ++- .../Seo/XmlSitemapGenerator.cs | 151 +++++++++++------- .../Seo/GenericPathRoute.cs | 17 +- 4 files changed, 140 insertions(+), 68 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index b814d70e45..8d3c9bd41c 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -216,17 +216,31 @@ public virtual LocalizedPropertyCollection GetLocalizedPropertyCollectionInterna where x.LocaleKeyGroup == localeKeyGroup select x; + var requestedSet = entityIds; + if (entityIds != null && entityIds.Length > 0) { if (isRange) { - var min = isSorted ? entityIds[0] : entityIds.Min(); - var max = isSorted ? entityIds[entityIds.Length - 1] : entityIds.Max(); + if (!isSorted) + { + Array.Sort(entityIds); + } + + var min = entityIds[0]; + var max = entityIds[entityIds.Length - 1]; + + if (entityIds.Length == 2 && max > min + 1) + { + // Only min & max were passed, create the range sequence. + requestedSet = Enumerable.Range(min, max - min + 1).ToArray(); + } query = query.Where(x => x.EntityId >= min && x.EntityId <= max); } else { + requestedSet = entityIds; query = query.Where(x => entityIds.Contains(x.EntityId)); } } @@ -236,7 +250,7 @@ public virtual LocalizedPropertyCollection GetLocalizedPropertyCollectionInterna query = query.Where(x => x.LanguageId == languageId); } - return new LocalizedPropertyCollection(localeKeyGroup, entityIds, query.ToList()); + return new LocalizedPropertyCollection(localeKeyGroup, requestedSet, query.ToList()); } } diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index eb3bd5ab25..bbdb23440b 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -213,17 +213,31 @@ public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityN where x.EntityName == entityName && x.IsActive select x; + var requestedSet = entityIds; + if (entityIds != null && entityIds.Length > 0) { if (isRange) { - var min = isSorted ? entityIds[0] : entityIds.Min(); - var max = isSorted ? entityIds[entityIds.Length - 1] : entityIds.Max(); + if (!isSorted) + { + Array.Sort(entityIds); + } + + var min = entityIds[0]; + var max = entityIds[entityIds.Length - 1]; + + if (entityIds.Length == 2 && max > min + 1) + { + // Only min & max were passed, create the range sequence. + requestedSet = Enumerable.Range(min, max - min + 1).ToArray(); + } query = query.Where(x => x.EntityId >= min && x.EntityId <= max); } else { + requestedSet = entityIds; query = query.Where(x => entityIds.Contains(x.EntityId)); } } @@ -241,7 +255,7 @@ public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityN } // Don't sort DESC, because latter items overwrite exisiting ones (it's the same as sorting DESC and taking the first) - return new UrlRecordCollection(entityName, entityIds, query.OrderBy(x => x.Id).ToList()); + return new UrlRecordCollection(entityName, requestedSet, query.OrderBy(x => x.Id).ToList()); } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index 31de7e12d6..78ebb8fff5 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -43,9 +43,9 @@ public partial class XmlSitemapGenerator : IXmlSitemapGenerator /// /// The maximum number of sitemap nodes allowed in a sitemap file. The absolute maximum allowed is 50,000 /// according to the specification. See http://www.sitemaps.org/protocol.html but the file size must also be - /// less than 10MB. After some experimentation, a maximum of 1.000 nodes keeps the file size below 10MB. + /// less than 10MB. After some experimentation, a maximum of 2.000 nodes keeps the file size below 10MB. /// - private const int MaximumSiteMapNodeCount = 1000; + private const int MaximumSiteMapNodeCount = 2000; /// /// The maximum size of a sitemap file in bytes (10MB). @@ -60,8 +60,6 @@ public partial class XmlSitemapGenerator : IXmlSitemapGenerator private readonly ICustomerService _customerService; private readonly ICatalogSearchService _catalogSearchService; private readonly IUrlRecordService _urlRecordService; - private readonly SeoSettings _seoSettings; - private readonly SecuritySettings _securitySettings; private readonly ICommonServices _services; private readonly ILockFileManager _lockFileManager; private readonly UrlHelper _urlHelper; @@ -78,8 +76,6 @@ public XmlSitemapGenerator( ICustomerService customerService, ICatalogSearchService catalogSearchService, IUrlRecordService urlRecordService, - SeoSettings commonSettings, - SecuritySettings securitySettings, ICommonServices services, ILockFileManager lockFileManager, UrlHelper urlHelper) @@ -92,8 +88,6 @@ public XmlSitemapGenerator( _customerService = customerService; _catalogSearchService = catalogSearchService; _urlRecordService = urlRecordService; - _seoSettings = commonSettings; - _securitySettings = securitySettings; _services = services; _lockFileManager = lockFileManager; _urlHelper = urlHelper; @@ -116,10 +110,10 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) { Guard.NotNegative(index, nameof(index)); - var storeId = _services.StoreContext.CurrentStore.Id; - var langId = _services.WorkContext.WorkingLanguage.Id; + var store = _services.StoreContext.CurrentStore; + var language = _services.WorkContext.WorkingLanguage; - var exists = SitemapFileExists(storeId, langId, index, out var path, out var name); + var exists = SitemapFileExists(store.Id, language.Id, index, out var path, out var name); if (exists) { @@ -127,8 +121,8 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) { Index = index, Name = name, - LanguageId = langId, - StoreId = storeId, + LanguageId = language.Id, + StoreId = store.Id, ModifiedOnUtc = _tenantFolder.GetFileLastWriteTimeUtc(path), Stream = _tenantFolder.OpenFile(path) }; @@ -149,7 +143,7 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) // If the main file (index 0) exists, the action should return NotFoundResult, // otherwise the rebuild process should be started or waited for. - if (SitemapFileExists(storeId, langId, 0, out path, out name)) + if (SitemapFileExists(store.Id, language.Id, 0, out path, out name)) { throw new IndexOutOfRangeException("The sitemap file '{0}' does not exist.".FormatInvariant(name)); } @@ -158,23 +152,23 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) // The main sitemap document with index 0 does not exist, meaning: the whole sitemap // needs to be created and cached by partitions. - if (IsRebuilding(storeId, langId)) + var wasRebuilding = false; + var lockFilePath = GetLockFilePath(store.Id, language.Id); + + while (IsRebuilding(lockFilePath)) { // The rebuild process is already running, either started // by the task scheduler or another HTTP request. // We should wait for completion. - Thread.Sleep(500); - - while (IsRebuilding(storeId, langId)) - { - Thread.Sleep(500); - } + wasRebuilding = true; + Thread.Sleep(1000); } - else + + if (!wasRebuilding) { // No lock. Rebuild now. - var buildContext = new XmlSitemapBuildContext(_services.StoreContext.CurrentStore, new[] { _services.WorkContext.WorkingLanguage }) + var buildContext = new XmlSitemapBuildContext(store, new[] { language }) { CancellationToken = CancellationToken.None }; @@ -262,18 +256,22 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) try { - var protocol = ctx.Protocol; var languageIds = ctx.Languages.Select(x => x.Id).Concat(new[] { 0 }).ToArray(); var nodes = new List(); - var queries = CreateQueries(ctx.Store.Id); - var total = queries.GetTotalRecordCount(); using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) { + var queries = CreateQueries(ctx); + var total = queries.GetTotalRecordCount(); var entities = EnumerateEntities(queries); - var segment = 0; + var totalSegments = (int)Math.Ceiling(total / (double)MaximumSiteMapNodeCount); + var segmentNodes = new List(); + var segment = 1; var numProcessed = 0; + + CheckSitemapCount(totalSegments); + foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) { if (ctx.CancellationToken.IsCancellationRequested) @@ -281,7 +279,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) break; } - numProcessed = ++segment * MaximumSiteMapNodeCount; + numProcessed = segment++ * MaximumSiteMapNodeCount; ctx.ProgressCallback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); var firstEntityName = batch.First().EntityName; @@ -291,12 +289,22 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) foreach (var language in ctx.Languages) { + var baseUrl = BuildBaseUrl(ctx.Store, language); + sitemaps[language.Id].AddRange(batch.Select(x => new XmlSitemapNode { LastMod = x.LastMod, - Loc = _urlHelper.RouteUrl(x.EntityName, new { SeName = slugs[x.EntityName].GetSlug(language.Id, x.Id, true) }, protocol) + Loc = BuildNodeUrl(baseUrl, x, slugs[x.EntityName], language) })); } + + if (segment % 5 == 0 || segment == totalSegments) + { + // Commit every 5th segment (10.000 nodes) temprorarily to disk to save RAM + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); } if (!ctx.CancellationToken.IsCancellationRequested) @@ -314,7 +322,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) // the system will try to rebuild again. foreach (var language in ctx.Languages) { - sitemaps[language.Id].Add(new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = _urlHelper.RouteUrl("HomePage") }); + sitemaps[language.Id].Add(new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = BuildBaseUrl(ctx.Store, language) }); } } @@ -326,18 +334,46 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) ctx.ProgressCallback?.Invoke(total, total, "Saving sitemaps for '{0}'.".FormatInvariant(language.GetTwoLetterISOLanguageName())); - var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id], protocol); + var baseUrl = BuildBaseUrl(ctx.Store, language); + var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id], baseUrl); SaveToDisk(documents, ctx.Store, language); + documents.Clear(); } } finally { // Undo impersonation _services.WorkContext.CurrentCustomer = prevCustomer; + sitemaps.Clear(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); } } } + private string BuildBaseUrl(Store store, Language language) + { + var host = _services.StoreService.GetHost(store).EnsureEndsWith("/"); + + var locSettings = _services.Settings.LoadSetting(store.Id); + if (locSettings.SeoFriendlyUrlsForLanguagesEnabled) + { + var defaultLangId = _languageService.GetDefaultLanguageId(store.Id); + if (language.Id != defaultLangId || locSettings.DefaultLanguageRedirectBehaviour < DefaultLanguageRedirectBehaviour.StripSeoCode) + { + host += language.GetTwoLetterISOLanguageName() + "/"; + } + } + + return host; + } + + private string BuildNodeUrl(string baseUrl, NamedEntity entity, UrlRecordCollection slugs, Language language) + { + return baseUrl + slugs.GetSlug(language.Id, entity.Id, true); + } + private void SaveToDisk(List documents, Store store, Language language) { var sitemapDir = BuildSitemapDirPath(store.Id, language.Id); @@ -393,21 +429,14 @@ private IEnumerable EnumerateEntities(QueryHolder queries) if (queries.Products != null) { var query = queries.Products.AsNoTracking(); - var maxId = int.MaxValue; - int xxx = 0; + while (maxId > 1) { - xxx++; - if (xxx >= 50) - { - break; - } - var products = queries.Products.AsNoTracking() .Where(x => x.Id < maxId) .OrderByDescending(x => x.Id) - .Take(() => 1000) + .Take(() => MaximumSiteMapNodeCount) .Select(x => new { x.Id, x.UpdatedOnUtc }) .ToList(); @@ -451,7 +480,7 @@ private IDictionary GetUrlRecordCollectionsForBatch return result; } - protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes, string protocol) + protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes, string baseUrl) { int siteMapCount = (int)Math.Ceiling(nodes.Count / (double)MaximumSiteMapNodeCount); CheckSitemapCount(siteMapCount); @@ -469,7 +498,7 @@ protected virtual List GetSiteMapDocuments(IReadOnlyCollection 1) { - var xml = this.GetSitemapIndexDocument(siteMaps, protocol); + var xml = this.GetSitemapIndexDocument(siteMaps, baseUrl); siteMapDocuments.Add(xml); } @@ -487,7 +516,7 @@ protected virtual List GetSiteMapDocuments(IReadOnlyCollection /// The collection of sitemaps containing their index and nodes. /// The sitemap index XML document, containing links to all the sitemap XML documents. - private string GetSitemapIndexDocument(IEnumerable>> siteMaps, string protocol) + private string GetSitemapIndexDocument(IEnumerable>> siteMaps, string baseUrl) { XNamespace ns = SiteMapsNamespace; @@ -504,7 +533,7 @@ private string GetSitemapIndexDocument(IEnumerable @@ -590,26 +619,31 @@ private string GetSitemapDocument(IEnumerable nodes) return xml; } - private QueryHolder CreateQueries(int storeId) + private QueryHolder CreateQueries(XmlSitemapBuildContext ctx) { + var storeId = ctx.Store.Id; + if (_services.StoreService.IsSingleStoreMode()) { storeId = 0; } + // Always work with store-dependant setting + var seoSettings = _services.Settings.LoadSetting(storeId); + var holder = new QueryHolder(); - if (_seoSettings.XmlSitemapIncludesCategories) + if (seoSettings.XmlSitemapIncludesCategories) { holder.Categories = _categoryService.GetAllCategories(showHidden: false, storeId: storeId).SourceQuery; } - if (_seoSettings.XmlSitemapIncludesManufacturers) + if (seoSettings.XmlSitemapIncludesManufacturers) { holder.Manufacturers = _manufacturerService.GetManufacturers(false).OrderBy(x => x.DisplayOrder).ThenBy(x => x.Name); } - if (_seoSettings.XmlSitemapIncludesTopics) + if (seoSettings.XmlSitemapIncludesTopics) { holder.Topics = _topicService.GetAllTopics(storeId).AlterQuery(q => { @@ -617,7 +651,7 @@ private QueryHolder CreateQueries(int storeId) }).SourceQuery; } - if (_seoSettings.XmlSitemapIncludesProducts) + if (seoSettings.XmlSitemapIncludesProducts) { var searchQuery = new CatalogSearchQuery() .VisibleOnly() @@ -630,11 +664,6 @@ private QueryHolder CreateQueries(int storeId) return holder; } - protected virtual IEnumerable GetCustomNodes(string protocol) - { - return Enumerable.Empty(); - } - protected void ProcessCustomNodes(XmlSitemapBuildContext ctx, Multimap sitemaps) { } @@ -666,7 +695,12 @@ private void CheckSitemapCount(int sitemapCount) public bool IsRebuilding(int storeId, int languageId) { - return _lockFileManager.IsLocked(GetLockFilePath(storeId, languageId)); + return IsRebuilding(GetLockFilePath(storeId, languageId)); + } + + private bool IsRebuilding(string lockFilePath) + { + return _lockFileManager.IsLocked(lockFilePath); } public virtual bool IsGenerated(int storeId, int languageId) @@ -700,6 +734,7 @@ public int GetTotalRecordCount() if (Manufacturers != null) num += Manufacturers.Count(); if (Topics != null) num += Topics.Count(); if (Products != null) num += Products.Count(); + //if (Products != null) num += 100000; return num; } diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs index b81cef572d..ccf42a7bfd 100644 --- a/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs @@ -18,8 +18,7 @@ namespace SmartStore.Web.Framework.Seo public class GenericPathRoute : LocalizedRoute { // Key = Prefix, Value = EntityType - private readonly Multimap _urlPrefixes = - new Multimap(StringComparer.OrdinalIgnoreCase, x => new HashSet(x, StringComparer.OrdinalIgnoreCase)); + private static readonly Multimap _urlPrefixes = new Multimap(StringComparer.OrdinalIgnoreCase); /// /// Initializes a new instance of the System.Web.Routing.Route class, using the specified URL pattern and handler class. @@ -68,12 +67,22 @@ public GenericPathRoute(string url, RouteValueDictionary defaults, RouteValueDic { } - public void RegisterUrlPrefix(string prefix, params string[] entityNames) + public static void RegisterUrlPrefix(string prefix, params string[] entityNames) { Guard.NotEmpty(prefix, nameof(prefix)); _urlPrefixes.AddRange(prefix, entityNames); } + + public static string GetUrlPrefixFor(string entityName) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + if (_urlPrefixes.Count == 0) + return null; + + return _urlPrefixes.FirstOrDefault(x => x.Value.Contains(entityName, StringComparer.OrdinalIgnoreCase)).Key; + } /// /// Returns information about the requested route. @@ -129,7 +138,7 @@ public override RouteData GetRouteData(HttpContextBase httpContext) } // Verify prefix matches any assigned entity name - if (entityNames != null && !entityNames.Contains(urlRecord.EntityName)) + if (entityNames != null && !entityNames.Contains(urlRecord.EntityName, StringComparer.OrdinalIgnoreCase)) { // does NOT match return NotFound(data); From 24c4d615e166626b4bb201372a7cfd46ec0624f0 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 20 Nov 2018 03:10:40 +0100 Subject: [PATCH 032/657] (Perf) Ultra-fast file-based XML sitemap generation * At least 10x faster * Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary. * No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly. --- changelog.md | 6 +- .../Seo/XmlSitemapGenerator.cs | 301 +++++++++++------- .../Views/ScheduleTask/_LastRun.cshtml | 2 +- 3 files changed, 198 insertions(+), 111 deletions(-) diff --git a/changelog.md b/changelog.md index 3f1b0c8287..828a9a7d03 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# Release Notes +# Release Notes ## SmartStore.NET 3.2 @@ -47,6 +47,10 @@ ### Improvements * (Perf) Significantly increased query performance for products with a lot of category assignments (> 10). +* (Perf) Ultra-fast file-based XML sitemap generation for extremely large catalogs (> 1M) + * At least 10x faster + * Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary. + * No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly. * Debitoor: Partially update customer instead of full update to avoid all fields being overwritten * #1479 Show in messages the delivery time at the time of purchase * #1184 Sort Current shopping carts & Current wishlists by ShoppingCartItem.CreatedOn. diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index 78ebb8fff5..402bdc860a 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -25,6 +25,7 @@ using SmartStore.Collections; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Localization; +using System.Threading.Tasks; namespace SmartStore.Services.Seo { @@ -219,32 +220,55 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) { Guard.NotNull(ctx, nameof(ctx)); - var lockFiles = new List(); + var languageData = new Dictionary(); foreach (var language in ctx.Languages) { - if (_lockFileManager.TryAcquireLock(GetLockFilePath(ctx.Store.Id, language.Id), out var lockFile)) + var lockFilePath = GetLockFilePath(ctx.Store.Id, language.Id); + + if (_lockFileManager.TryAcquireLock(lockFilePath, out var lockFile)) { // Process only languages that are unlocked right now // It is possible that an HTTP request triggered the generation // of a language specific sitemap. - lockFiles.Add(lockFile); + + var sitemapDir = BuildSitemapDirPath(ctx.Store.Id, language.Id); + var data = new LanguageData + { + Store = ctx.Store, + Language = language, + LockFile = lockFile, + LockFilePath = lockFilePath, + TempDir = sitemapDir + "~", + FinalDir = sitemapDir, + BaseUrl = BuildBaseUrl(ctx.Store, language) + }; + + _tenantFolder.TryDeleteDirectory(data.TempDir); + _tenantFolder.CreateDirectory(data.TempDir); + + languageData[language.Id] = data; } } - if (lockFiles.Count == 0) + if (languageData.Count == 0) { Logger.Warn("XML sitemap rebuild already in process."); return; } + var languages = languageData.Values.Select(x => x.Language); + var languageIds = languages.Select(x => x.Id).Concat(new[] { 0 }).ToArray(); + // All sitemaps grouped by language var sitemaps = new Multimap(); var compositeFileLock = new ActionDisposable(() => { - lockFiles.Each(x => x.Dispose()); - lockFiles.Clear(); + foreach (var data in languageData.Values) + { + data.LockFile.Release(); + } }); using (compositeFileLock) @@ -256,22 +280,23 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) try { - var languageIds = ctx.Languages.Select(x => x.Id).Concat(new[] { 0 }).ToArray(); var nodes = new List(); + var queries = CreateQueries(ctx); + var total = queries.GetTotalRecordCount(); + + var totalSegments = (int)Math.Ceiling(total / (double)MaximumSiteMapNodeCount); + var hasIndex = totalSegments > 1; + var indexNodes = new Multimap(); + var segment = 0; + var numProcessed = 0; + + CheckSitemapCount(totalSegments); + using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) { - var queries = CreateQueries(ctx); - var total = queries.GetTotalRecordCount(); var entities = EnumerateEntities(queries); - var totalSegments = (int)Math.Ceiling(total / (double)MaximumSiteMapNodeCount); - var segmentNodes = new List(); - var segment = 1; - var numProcessed = 0; - - CheckSitemapCount(totalSegments); - foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) { if (ctx.CancellationToken.IsCancellationRequested) @@ -279,7 +304,8 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) break; } - numProcessed = segment++ * MaximumSiteMapNodeCount; + segment++; + numProcessed = segment * MaximumSiteMapNodeCount; ctx.ProgressCallback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); var firstEntityName = batch.First().EntityName; @@ -287,57 +313,86 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) var slugs = GetUrlRecordCollectionsForBatch(batch, languageIds); - foreach (var language in ctx.Languages) + foreach (var data in languageData.Values) { - var baseUrl = BuildBaseUrl(ctx.Store, language); + var language = data.Language; + var baseUrl = data.BaseUrl; + // Create all node entries for this segment sitemaps[language.Id].AddRange(batch.Select(x => new XmlSitemapNode { LastMod = x.LastMod, Loc = BuildNodeUrl(baseUrl, x, slugs[x.EntityName], language) })); - } - if (segment % 5 == 0 || segment == totalSegments) - { - // Commit every 5th segment (10.000 nodes) temprorarily to disk to save RAM + // Create index node for this segment/language combination + if (hasIndex) + { + indexNodes[language.Id].Add(new XmlSitemapNode + { + LastMod = sitemaps[language.Id].Select(x => x.LastMod).Where(x => x.HasValue).DefaultIfEmpty().Max(), + Loc = GetSitemapIndexUrl(segment, baseUrl), + }); + } + + if (segment % 5 == 0 || segment == totalSegments) + { + // Commit every 5th segment (10.000 nodes) temporarily to disk to minimize RAM usage + var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id]); + SaveTemp(documents, data, segment - documents.Count + (hasIndex ? 1 : 0)); + + documents.Clear(); + sitemaps.RemoveAll(language.Id); + } } + slugs.Clear(); + GC.Collect(); GC.WaitForPendingFinalizers(); } + // Process custom nodes if (!ctx.CancellationToken.IsCancellationRequested) { ctx.ProgressCallback?.Invoke(numProcessed, total, "Processing custom nodes".FormatCurrent(numProcessed, total)); ProcessCustomNodes(ctx, sitemaps); + + foreach (var data in languageData.Values) + { + if (sitemaps.ContainsKey(data.Language.Id) && sitemaps[data.Language.Id].Count > 0) + { + var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[data.Language.Id]); + SaveTemp(documents, data, (segment + 1) - documents.Count + (hasIndex ? 1 : 0)); + } + else if (segment == 0) + { + // Ensure that at least one entry exists. Otherwise, + // the system will try to rebuild again. + var homeNode = new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = data.BaseUrl }; + var documents = GetSiteMapDocuments(new List { homeNode }); + SaveTemp(documents, data, 0); + } + + } } } ctx.CancellationToken.ThrowIfCancellationRequested(); - if (sitemaps.Count == 0) + ctx.ProgressCallback?.Invoke(totalSegments, totalSegments, "Finalizing...'"); + + foreach (var data in languageData.Values) { - // Ensure that at least one entry exists. Otherwise, - // the system will try to rebuild again. - foreach (var language in ctx.Languages) + // Create index documents (if any) + if (hasIndex && indexNodes.Any()) { - sitemaps[language.Id].Add(new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = BuildBaseUrl(ctx.Store, language) }); + var indexDocument = CreateSitemapIndexDocument(indexNodes[data.Language.Id]); + SaveTemp(new List { indexDocument }, data, 0); } - } - - // Save all sitemaps to disk - foreach (var language in ctx.Languages) - { - if (ctx.CancellationToken.IsCancellationRequested) - break; - - ctx.ProgressCallback?.Invoke(total, total, "Saving sitemaps for '{0}'.".FormatInvariant(language.GetTwoLetterISOLanguageName())); - var baseUrl = BuildBaseUrl(ctx.Store, language); - var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id], baseUrl); - SaveToDisk(documents, ctx.Store, language); - documents.Clear(); + // Save finally (actually renames temp folder) + SaveFinal(data); } } finally @@ -346,6 +401,14 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) _services.WorkContext.CurrentCustomer = prevCustomer; sitemaps.Clear(); + foreach (var data in languageData.Values) + { + if (_tenantFolder.DirectoryExists(data.TempDir)) + { + _tenantFolder.TryDeleteDirectory(data.TempDir); + } + } + GC.Collect(); GC.WaitForPendingFinalizers(); } @@ -374,27 +437,43 @@ private string BuildNodeUrl(string baseUrl, NamedEntity entity, UrlRecordCollect return baseUrl + slugs.GetSlug(language.Id, entity.Id, true); } - private void SaveToDisk(List documents, Store store, Language language) + private void SaveTemp(List documents, LanguageData data, int start) { - var sitemapDir = BuildSitemapDirPath(store.Id, language.Id); - - if (_tenantFolder.DirectoryExists(sitemapDir)) - { - _tenantFolder.DeleteDirectory(sitemapDir); - } - - _tenantFolder.CreateDirectory(sitemapDir); - for (int i = 0; i < documents.Count; i++) { // Save segment to disk - var fileName = SiteMapFileNamePattern.FormatInvariant(i); - var filePath = _tenantFolder.Combine(sitemapDir, fileName); + var fileName = SiteMapFileNamePattern.FormatInvariant(i + start); + var filePath = _tenantFolder.Combine(data.TempDir, fileName); _tenantFolder.CreateTextFile(filePath, documents[i]); } } + private void SaveFinal(LanguageData data) + { + // Delete current sitemap dir + _tenantFolder.TryDeleteDirectory(data.FinalDir); + + var source = _tenantFolder.MapPath(data.TempDir); + var dest = _tenantFolder.MapPath(data.FinalDir); + + // Move/Rename new (temp) dir to current + System.IO.Directory.Move(source, dest); + + int retries = 0; + while (!SitemapFileExists(data.Store.Id, data.Language.Id, 0, out _, out _)) + { + if (retries > 20) + { + break; + } + + // IO breathe: directly after a folder rename a file check fails. Wait a sec... + Task.Delay(500).Wait(); + retries++; + } + } + private IEnumerable EnumerateEntities(QueryHolder queries) { var entities = Enumerable.Empty(); @@ -431,6 +510,7 @@ private IEnumerable EnumerateEntities(QueryHolder queries) var query = queries.Products.AsNoTracking(); var maxId = int.MaxValue; + //var limit = 0; while (maxId > 1) { var products = queries.Products.AsNoTracking() @@ -440,6 +520,12 @@ private IEnumerable EnumerateEntities(QueryHolder queries) .Select(x => new { x.Id, x.UpdatedOnUtc }) .ToList(); + //limit++; + //if (limit >= 100) + //{ + // break; + //} + if (products.Count == 0) { break; @@ -480,7 +566,7 @@ private IDictionary GetUrlRecordCollectionsForBatch return result; } - protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes, string baseUrl) + protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes) { int siteMapCount = (int)Math.Ceiling(nodes.Count / (double)MaximumSiteMapNodeCount); CheckSitemapCount(siteMapCount); @@ -496,66 +582,14 @@ protected virtual List GetSiteMapDocuments(IReadOnlyCollection(siteMapCount); - if (siteMapCount > 1) - { - var xml = this.GetSitemapIndexDocument(siteMaps, baseUrl); - siteMapDocuments.Add(xml); - } - foreach (var kvp in siteMaps) { - var xml = this.GetSitemapDocument(kvp.Value); - siteMapDocuments.Add(xml); + siteMapDocuments.Add(this.GetSitemapDocument(kvp.Value)); } return siteMapDocuments; } - /// - /// Gets the sitemap index XML document, containing links to all the sitemap XML documents. - /// - /// The collection of sitemaps containing their index and nodes. - /// The sitemap index XML document, containing links to all the sitemap XML documents. - private string GetSitemapIndexDocument(IEnumerable>> siteMaps, string baseUrl) - { - XNamespace ns = SiteMapsNamespace; - - XElement root = new XElement(ns + "sitemapindex"); - - foreach (KeyValuePair> map in siteMaps) - { - // Get the latest LastModified DateTime from the sitemap nodes or null if there is none. - DateTime? lastModified = map.Value - .Select(x => x.LastMod) - .Where(x => x.HasValue) - .DefaultIfEmpty() - .Max(); - - var xel = new XElement( - ns + "sitemap", - new XElement(ns + "loc", this.GetSitemapUrl(map.Key, baseUrl)), - lastModified.HasValue ? - new XElement( - ns + "lastmod", - lastModified.Value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz")) : - null); - - root.Add(xel); - } - - var document = new XDocument(root); - var xml = document.ToString(SaveOptions.DisableFormatting); - CheckDocumentSize(xml); - - return xml; - } - - private string GetSitemapUrl(int index, string baseUrl) - { - var url = _urlHelper.RouteUrl("XmlSitemap", new { index }).TrimStart('/'); - return baseUrl + url; - } - /// /// Gets the sitemap XML document for the specified set of nodes. /// @@ -564,7 +598,7 @@ private string GetSitemapUrl(int index, string baseUrl) private string GetSitemapDocument(IEnumerable nodes) { //var languages = _languageService.GetAllLanguages(); - + XNamespace ns = SiteMapsNamespace; XNamespace xhtml = XhtmlNamespace; @@ -619,6 +653,44 @@ private string GetSitemapDocument(IEnumerable nodes) return xml; } + /// + /// Gets the sitemap index XML document, containing links to all the sitemap XML documents. + /// + /// The collection of sitemaps containing their index and nodes. + /// The sitemap index XML document, containing links to all the sitemap XML documents. + private string CreateSitemapIndexDocument(IEnumerable nodes) + { + XNamespace ns = SiteMapsNamespace; + + XElement root = new XElement(ns + "sitemapindex"); + + foreach (var node in nodes) + { + var xel = new XElement( + ns + "sitemap", + new XElement(ns + "loc", node.Loc), + node.LastMod.HasValue ? + new XElement( + ns + "lastmod", + node.LastMod.Value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz")) : + null); + + root.Add(xel); + } + + var document = new XDocument(root); + var xml = document.ToString(SaveOptions.DisableFormatting); + CheckDocumentSize(xml); + + return xml; + } + + private string GetSitemapIndexUrl(int index, string baseUrl) + { + var url = _urlHelper.RouteUrl("XmlSitemap", new { index }).TrimStart('/'); + return baseUrl + url; + } + private QueryHolder CreateQueries(XmlSitemapBuildContext ctx) { var storeId = ctx.Store.Id; @@ -720,6 +792,17 @@ public virtual void Invalidate(int storeId, int languageId) #region Nested classes + class LanguageData + { + public Store Store { get; set; } + public Language Language { get; set; } + public ILockFile LockFile { get; set; } + public string LockFilePath { get; set; } + public string TempDir { get; set; } + public string FinalDir { get; set; } + public string BaseUrl { get; set; } + } + class QueryHolder { public IQueryable Categories { get; set; } @@ -734,7 +817,7 @@ public int GetTotalRecordCount() if (Manufacturers != null) num += Manufacturers.Count(); if (Topics != null) num += Topics.Count(); if (Products != null) num += Products.Count(); - //if (Products != null) num += 100000; + //if (Products != null) num += 200000; return num; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml index 5e6444619a..f10051d9d1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml @@ -17,7 +17,7 @@ else if (Model.LastHistoryEntry.Id != 0) } if (Model.LastHistoryEntry.Error.HasValue()) { -
@T("Common.Error"): @Model.LastHistoryEntry.Error
+
@T("Common.Error"): @Model.LastHistoryEntry.Error.Truncate(100, "...")
if (Model.LastHistoryEntry.SucceededOn.HasValue && Model.LastHistoryEntry.SucceededOn != Model.LastHistoryEntry.FinishedOn) {
@T("Admin.System.ScheduleTasks.LastSuccess"): @Model.LastHistoryEntry.SucceededOn.Value.ToString("g")
From 7daf4221bb5d6f303f5fa24d24b885ace37802aa Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Tue, 20 Nov 2018 09:36:45 +0100 Subject: [PATCH 033/657] Export: Minor changes --- .../DataExchange/Export/DataExporter.cs | 4 +- .../Export/ExportDataSegmenter.cs | 67 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index edbfeb6a4f..6c751ac956 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -413,9 +413,9 @@ x is Picture || x is ProductBundleItem || x is ProductCategory || x is ProductMa } } - private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, int pageIndex = 0) + private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) { - var offset = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); + var offset = Math.Max(ctx.Request.Profile.Offset, 0); var limit = Math.Max(ctx.Request.Profile.Limit, 0); var recordsPerSegment = ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0); var totalCount = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs index 0673598fdc..6fcbd86367 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs @@ -38,20 +38,18 @@ internal interface IExportDataSegmenterProvider : IExportDataSegmenterConsumer, public class ExportDataSegmenter : IExportDataSegmenterProvider where T : BaseEntity { - private Func> _load; - private Action> _loaded; - private Func> _convert; + private readonly Func> _load; + private readonly Action> _loaded; + private readonly Func> _convert; - private int _offset; - private int _take; - private int _limit; - private int _recordsPerSegment; - private int _totalRecords; + private readonly int _offset; + private readonly int _take; + private readonly int _limit; + private readonly int _recordsPerSegment; + private readonly int _totalRecords; private int _skip; - private int _countRecords; - - private Queue _data; + private Queue _data; public ExportDataSegmenter( Func> load, @@ -84,25 +82,24 @@ public int TotalRecords { var total = Math.Max(_totalRecords - _offset, 0); - if (_limit > 0 && _limit < total) - return _limit; + if (_limit > 0 && _limit < total) + { + return _limit; + } return total; } } - /// - /// Number of processed records - /// - public int RecordCount - { - get { return _countRecords; } - } + /// + /// Number of processed records + /// + public int RecordCount { get; private set; } - /// - /// Record per segment counter - /// - public int RecordPerSegmentCount { get; set; } + /// + /// Record per segment counter + /// + public int RecordPerSegmentCount { get; set; } /// /// Whether there is data available @@ -111,7 +108,7 @@ public bool HasData { get { - if (_limit > 0 && _countRecords >= _limit) + if (_limit > 0 && RecordCount >= _limit) return false; if (_data != null && _data.Count > 0) @@ -138,7 +135,7 @@ public IReadOnlyCollection CurrentSegment { _convert(entity).Each(x => records.Add(x)); - if (++_countRecords >= _limit && _limit > 0) + if (++RecordCount >= _limit && _limit > 0) return records; if (++RecordPerSegmentCount >= _recordsPerSegment && _recordsPerSegment > 0) @@ -155,13 +152,13 @@ public IReadOnlyCollection CurrentSegment /// public bool ReadNextSegment() { - if (_limit > 0 && _countRecords >= _limit) + if (_limit > 0 && RecordCount >= _limit) return false; if (_recordsPerSegment > 0 && RecordPerSegmentCount >= _recordsPerSegment) return false; - // do not make the queue longer than necessary + // Do not make the queue longer than necessary. if (_recordsPerSegment > 0 && _data != null && _data.Count >= _recordsPerSegment) return true; @@ -183,22 +180,24 @@ public bool ReadNextSegment() _data = new Queue(_load(_skip)); } - // give provider the opportunity to make something with entity ids + // Give provider the opportunity to make something with entity ids. _loaded?.Invoke(_data.AsReadOnly()); - return (_data.Count > 0); + return _data.Count > 0; } /// - /// Dispose and reset segmenter instance + /// Dispose and reset segmenter instance. /// public void Dispose() { - if (_data != null) - _data.Clear(); + if (_data != null) + { + _data.Clear(); + } _skip = _offset; - _countRecords = 0; + RecordCount = 0; RecordPerSegmentCount = 0; } } From ee039cf16086f81bd549d389114fbeca7bf7a78f Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Tue, 20 Nov 2018 12:38:39 +0100 Subject: [PATCH 034/657] Export: Refactor and simply data preview --- .../DataExchange/Export/DataExportResult.cs | 19 ++ .../DataExchange/Export/DataExporter.cs | 164 +++++++++--------- .../DataExchange/Export/IDataExporter.cs | 4 +- .../Export/Internal/DataExporterContext.cs | 18 +- .../Controllers/ExportController.cs | 93 +++++----- .../Models/DataExchange/ExportPreviewModel.cs | 1 - .../Views/Export/Preview.cshtml | 14 +- 7 files changed, 170 insertions(+), 143 deletions(-) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs index 13032378ef..57c85801ab 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs @@ -62,4 +62,23 @@ public class ExportFileInfo public RelatedEntityType? RelatedType { get; set; } } } + + + public class DataExportPreviewResult + { + public DataExportPreviewResult() + { + Data = new List(); + } + + /// + /// Preview data. + /// + public List Data { get; set; } + + /// + /// Number of total records. + /// + public int TotalRecords { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 6c751ac956..9266e1cfb9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -234,7 +234,7 @@ private void SetProgress(DataExporterContext ctx, int loadedRecords) { if (!ctx.IsPreview && loadedRecords > 0) { - var totalRecords = ctx.RecordsPerStore.Sum(x => x.Value); + var totalRecords = ctx.StatsPerStore.Sum(x => x.Value.TotalRecords); if (ctx.Request.Profile.Limit > 0 && totalRecords > ctx.Request.Profile.Limit) { @@ -418,7 +418,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) var offset = Math.Max(ctx.Request.Profile.Offset, 0); var limit = Math.Max(ctx.Request.Profile.Limit, 0); var recordsPerSegment = ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0); - var totalCount = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; + var totalRecords = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.StatsPerStore.First(x => x.Key == ctx.Store.Id).Value.TotalRecords; switch (ctx.Request.Provider.Value.EntityType) { @@ -470,7 +470,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)] = CreateTranslationCollection(nameof(ProductVariantAttributeValue), pvav); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -498,7 +498,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), products); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -514,7 +514,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -530,7 +530,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -545,7 +545,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -555,7 +555,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) skip => GetNewsLetterSubscriptions(ctx, skip), null, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -570,7 +570,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), products); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount + offset, PageSize, limit, recordsPerSegment, totalRecords ); break; @@ -858,7 +858,7 @@ public virtual ProductExportContext CreateProductExportContext( return context; } - private IQueryable GetProductQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, int take) { IQueryable query = null; @@ -902,18 +902,25 @@ private IQueryable GetProductQuery(DataExporterContext ctx, int skip, i searchQuery = searchQuery.WithProductId(f.IdMinimum, f.IdMaximum); query = _catalogSearchService.Value.PrepareQuery(searchQuery); - query = query.OrderByDescending(x => x.CreatedOnUtc); } else { query = ctx.Request.ProductQuery; } - if (skip > 0) - query = query.Skip(() => skip); + query = query.OrderBy(x => x.Id); - if (take != int.MaxValue) - query = query.Take(() => take); + // Skip required for preview. + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + + if (take != int.MaxValue) + { + query = query.Take(() => take); + } return query; } @@ -1330,10 +1337,8 @@ private List GetShoppingCartItems(DataExporterContext ctx, int #endregion - private List Init(DataExporterContext ctx, int? totalRecords = null) + private List Init(DataExporterContext ctx) { - // Init things that are required for export and for preview. - // Init things that are only required for export in ExportCoreOuter. List result = null; ctx.ContextCurrency = _currencyService.Value.GetCurrencyById(ctx.Projection.CurrencyId ?? 0) ?? _services.WorkContext.WorkingCurrency; @@ -1387,47 +1392,45 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) result = new List { ctx.Store }; } - // Get total records for progress. + // Get record stats for each store. foreach (var store in result) { ctx.Store = store; - int totalCount = 0; + IQueryable query = null; - if (totalRecords.HasValue) - { - // Speed up preview by not counting total at each page. - totalCount = totalRecords.Value; - } - else - { - switch (ctx.Request.Provider.Value.EntityType) - { - case ExportEntityType.Product: - totalCount = GetProductQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Order: - totalCount = GetOrderQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Manufacturer: - totalCount = GetManufacturerQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Category: - totalCount = GetCategoryQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Customer: - totalCount = GetCustomerQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.NewsLetterSubscription: - totalCount = GetNewsLetterSubscriptionQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.ShoppingCartItem: - totalCount = GetShoppingCartItemQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); - break; - } - } + switch (ctx.Request.Provider.Value.EntityType) + { + case ExportEntityType.Product: + query = GetProductQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + case ExportEntityType.Order: + query = GetOrderQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + case ExportEntityType.Manufacturer: + query = GetManufacturerQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + case ExportEntityType.Category: + query = GetCategoryQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + case ExportEntityType.Customer: + query = GetCustomerQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + case ExportEntityType.NewsLetterSubscription: + query = GetNewsLetterSubscriptionQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + case ExportEntityType.ShoppingCartItem: + query = GetShoppingCartItemQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue); + break; + } + + var stats = new RecordStats + { + TotalRecords = query.Count(), + MaxId = query.Max(x => (int?)x.Id) ?? 0 + }; - ctx.RecordsPerStore.Add(store.Id, totalCount); + ctx.StatsPerStore.Add(store.Id, stats); } return result; @@ -1634,7 +1637,6 @@ private void ExportCoreOuter(DataExporterContext ctx) // lazyLoading: false, proxyCreation: false impossible due to price calculation. using (var scope = new DbContextScope(_dbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) { - // Init things required for export and not required for preview. ctx.DeliveryTimes = _deliveryTimeService.Value.GetAllDeliveryTimes().ToDictionary(x => x.Id); ctx.QuantityUnits = _quantityUnitService.Value.GetAllQuantityUnits().ToDictionary(x => x.Id); ctx.ProductTemplates = _productTemplateService.Value.GetAllProductTemplates().ToDictionary(x => x.Id, x => x.ViewPath); @@ -1805,77 +1807,69 @@ public DataExportResult Export(DataExportRequest request, CancellationToken canc return ctx.Result; } - public IList Preview(DataExportRequest request, int pageIndex, int? totalRecords = null) + public DataExportPreviewResult Preview(DataExportRequest request, int pageIndex) { - var result = new List(); - var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); + var result = new DataExportPreviewResult(); + var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); var ctx = new DataExporterContext(request, cancellation.Token, true); - var unused = Init(ctx, totalRecords); - var offset = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); + var unused = Init(ctx); + var skip = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); if (!HasPermission(ctx)) { throw new SmartException(T("Admin.AccessDenied")); } - switch (request.Provider.Value.EntityType) + result.TotalRecords = ctx.StatsPerStore.First().Value.TotalRecords; + + switch (request.Provider.Value.EntityType) { case ExportEntityType.Product: { - var items = GetProductQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetProductQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; case ExportEntityType.Order: { - var items = GetOrderQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetOrderQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; case ExportEntityType.Category: { - var items = GetCategoryQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetCategoryQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; case ExportEntityType.Manufacturer: { - var items = GetManufacturerQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetManufacturerQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; case ExportEntityType.Customer: { - var items = GetCustomerQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetCustomerQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; case ExportEntityType.NewsLetterSubscription: { - var items = GetNewsLetterSubscriptionQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetNewsLetterSubscriptionQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; case ExportEntityType.ShoppingCartItem: { - var items = GetShoppingCartItemQuery(ctx, offset, PageSize).ToList(); - items.Each(x => result.Add(ToDynamic(ctx, x))); + var items = GetShoppingCartItemQuery(ctx, skip, PageSize).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); } break; } - return result; - } - - public int GetDataCount(DataExportRequest request) - { - var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); - var ctx = new DataExporterContext(request, cancellation.Token, true); - var unused = Init(ctx); - - var totalCount = ctx.RecordsPerStore.First().Value; - return totalCount; + return result; } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs index 3bea8233e3..b014d487f5 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs @@ -14,9 +14,7 @@ public interface IDataExporter { DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken); - IList Preview(DataExportRequest request, int pageIndex, int? totalRecords = null); - - int GetDataCount(DataExportRequest request); + DataExportPreviewResult Preview(DataExportRequest request, int pageIndex); /// /// Creates a product export context for fast retrieval (eager loading) of product navigation properties diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index 75156360aa..aadfa50368 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -45,13 +45,13 @@ public DataExporterContext( UrlRecords = new Dictionary(); UrlRecordsPerPage = new Dictionary(); - RecordsPerStore = new Dictionary(); + StatsPerStore = new Dictionary(); EntityIdsLoaded = new List(); EntityIdsPerSegment = new List(); Result = new DataExportResult { - FileFolder = (IsFileBasedExport ? FolderContent : null) + FileFolder = IsFileBasedExport ? FolderContent : null }; ExecuteContext = new ExportExecuteContext(Result, CancellationToken, FolderContent); @@ -82,11 +82,11 @@ public void SetLoadedEntityIds(IEnumerable ids) /// public List EntityIdsPerSegment { get; set; } - public int RecordCount { get; set; } - public Dictionary RecordsPerStore { get; set; } - public string ProgressInfo { get; set; } + public string ProgressInfo { get; set; } + public int RecordCount { get; set; } + public Dictionary StatsPerStore { get; set; } - public DataExportRequest Request { get; private set; } + public DataExportRequest Request { get; private set; } public CancellationToken CancellationToken { get; private set; } public bool IsPreview { get; private set; } @@ -145,4 +145,10 @@ public bool IsFileBasedExport public ExportExecuteContext ExecuteContext { get; set; } public DataExportResult Result { get; set; } } + + internal class RecordStats + { + public int TotalRecords { get; set; } + public int MaxId { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs index 20c2d56343..f0caf9fc5a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs @@ -979,16 +979,22 @@ public ActionResult DeleteConfirmed(int id) public ActionResult Preview(int id) { - if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) - return AccessDeniedView(); + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + { + return AccessDeniedView(); + } var profile = _exportService.GetExportProfileById(id); - if (profile == null) - return RedirectToAction("List"); + if (profile == null) + { + return RedirectToAction("List"); + } var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider == null || provider.Metadata.IsHidden) - return RedirectToAction("List"); + if (provider == null || provider.Metadata.IsHidden) + { + return RedirectToAction("List"); + } if (!profile.Enabled) { @@ -998,7 +1004,6 @@ public ActionResult Preview(int id) } var request = new DataExportRequest(profile, provider); - var totalRecords = _dataExporter.GetDataCount(request); var model = new ExportPreviewModel { @@ -1007,7 +1012,6 @@ public ActionResult Preview(int id) ThumbnailUrl = GetThumbnailUrl(provider), GridPageSize = DataExporter.PageSize, EntityType = provider.Value.EntityType, - TotalRecords = totalRecords, LogFileExists = System.IO.File.Exists(profile.GetExportLogPath()), UsernamesEnabled = _customerSettings.Value.UsernamesEnabled }; @@ -1016,7 +1020,7 @@ public ActionResult Preview(int id) } [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult PreviewList(GridCommand command, int id, int totalRecords) + public ActionResult PreviewList(GridCommand command, int id) { if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { @@ -1034,19 +1038,20 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) return new JsonResult { Data = Enumerable.Empty() }; } - var request = new DataExportRequest(profile, provider); - var normalizedTotal = profile.Limit > 0 && totalRecords > profile.Limit + object gridData = null; + var pageIndex = command.Page - 1; + var request = new DataExportRequest(profile, provider); + var result = _dataExporter.Preview(request, pageIndex); + + var normalizedTotal = profile.Limit > 0 && result.TotalRecords > profile.Limit ? profile.Limit - : totalRecords; - var pageIndex = command.Page - 1; - object gridData = null; + : result.TotalRecords; - if (provider.Value.EntityType == ExportEntityType.Product) + if (provider.Value.EntityType == ExportEntityType.Product) { var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var product = item.Entity as Product; var model = new ExportPreviewProductModel(); @@ -1062,14 +1067,14 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) model.AdminComment = item.AdminComment; models.Add(model); } + gridData = new GridModel { Data = models, Total = normalizedTotal }; } else if (provider.Value.EntityType == ExportEntityType.Order) { var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var model = new ExportPreviewOrderModel(); model.Id = item.Id; @@ -1084,14 +1089,14 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) model.StoreName = (string)item.Store.Name; models.Add(model); } + gridData = new GridModel { Data = models, Total = normalizedTotal }; } else if (provider.Value.EntityType == ExportEntityType.Category) { var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var category = item.Entity as Category; var model = new ExportPreviewCategoryModel(); @@ -1109,9 +1114,8 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) else if (provider.Value.EntityType == ExportEntityType.Manufacturer) { var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var model = new ExportPreviewManufacturerModel(); model.Id = item.Id; @@ -1121,14 +1125,14 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) model.LimitedToStores = item.LimitedToStores; models.Add(model); } + gridData = new GridModel { Data = models, Total = normalizedTotal }; } else if (provider.Value.EntityType == ExportEntityType.Customer) { var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var customer = item.Entity as Customer; var customerRoles = item.CustomerRoles as List; @@ -1145,14 +1149,14 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) model.Username = customer.Username; models.Add(model); } + gridData = new GridModel { Data = models, Total = normalizedTotal }; } else if (provider.Value.EntityType == ExportEntityType.NewsLetterSubscription) { var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var subscription = item.Entity as NewsLetterSubscription; var model = new ExportPreviewNewsLetterSubscriptionModel(); @@ -1163,18 +1167,17 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) model.StoreName = (string)item.Store.Name; models.Add(model); } - gridData = new GridModel { Data = models, Total = normalizedTotal }; + + gridData = new GridModel { Data = models, Total = normalizedTotal }; } else if (provider.Value.EntityType == ExportEntityType.ShoppingCartItem) { var guest = T("Admin.Customers.Guest").Text; var cartTypeName = ShoppingCartType.ShoppingCart.GetLocalizedEnum(Services.Localization, Services.WorkContext); var wishlistTypeName = ShoppingCartType.Wishlist.GetLocalizedEnum(Services.Localization, Services.WorkContext); - var models = new List(); - var items = _dataExporter.Preview(request, pageIndex, totalRecords); - foreach (var item in items) + foreach (var item in result.Data) { var cartItem = item.Entity as ShoppingCartItem; var model = new ExportPreviewShoppingCartItemModel(); @@ -1196,6 +1199,7 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) model.StoreName = (string)item.Store.Name; models.Add(model); } + gridData = new GridModel { Data = models, Total = normalizedTotal }; } @@ -1205,15 +1209,18 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) [HttpPost] public ActionResult Execute(int id, string selectedIds) { - // permissions checked internally by DataExporter - + // Permissions checked internally by DataExporter. var profile = _exportService.GetExportProfileById(id); - if (profile == null) - return RedirectToAction("List"); + if (profile == null) + { + return RedirectToAction("List"); + } var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider == null || provider.Metadata.IsHidden) - return RedirectToAction("List"); + if (provider == null || provider.Metadata.IsHidden) + { + return RedirectToAction("List"); + } var taskParams = new Dictionary { @@ -1221,16 +1228,20 @@ public ActionResult Execute(int id, string selectedIds) { TaskExecutor.CurrentStoreIdParamName, Services.StoreContext.CurrentStore.Id.ToString() } }; - if (selectedIds.HasValue()) - taskParams.Add("SelectedIds", selectedIds); + if (selectedIds.HasValue()) + { + taskParams.Add("SelectedIds", selectedIds); + } _taskScheduler.RunSingleTask(profile.SchedulingTaskId, taskParams); NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress.DataExportTask")); var referrer = Services.WebHelper.GetUrlReferrer(); - if (referrer.HasValue()) - return Redirect(referrer); + if (referrer.HasValue()) + { + return Redirect(referrer); + } return RedirectToAction("List"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportPreviewModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportPreviewModel.cs index dfe06e3d42..a3041c6234 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportPreviewModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportPreviewModel.cs @@ -10,7 +10,6 @@ public class ExportPreviewModel : EntityModelBase public string Name { get; set; } public string ThumbnailUrl { get; set; } public int GridPageSize { get; set; } - public int TotalRecords { get; set; } public ExportEntityType EntityType { get; set; } public bool LogFileExists { get; set; } public bool UsernamesEnabled { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml index 988403c533..b2866cfa4b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml @@ -137,7 +137,7 @@ columns.Bound(x => x.AdminComment); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } @@ -172,7 +172,7 @@ .RightAlign(); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } @@ -209,7 +209,7 @@ .Centered(); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } @@ -244,7 +244,7 @@ .Centered(); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } @@ -278,7 +278,7 @@ columns.Bound(x => x.LastActivityDate); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } @@ -308,7 +308,7 @@ columns.Bound(x => x.CreatedOn); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } @@ -344,7 +344,7 @@ columns.Bound(x => x.CreatedOn); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id, totalRecords = Model.TotalRecords })) + .DataBinding(dataBinding => dataBinding.Ajax().Select("PreviewList", "Export", new { id = Model.Id })) .ClientEvents(events => events.OnDataBound("OnDataBound")) .EnableCustomBinding(true)) } From 9416c6db5def50453cd5da6a5974808ebf2c7753 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Tue, 20 Nov 2018 20:12:48 +0100 Subject: [PATCH 035/657] Export: Avoid segmentation by skip method (in progress) --- .../DataExchange/Export/DataExporter.cs | 436 ++++++++++++------ .../Export/ExportDataSegmenter.cs | 125 ++--- .../Export/Internal/DataExporterContext.cs | 6 +- 3 files changed, 382 insertions(+), 185 deletions(-) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 9266e1cfb9..f6d348d134 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -415,17 +415,20 @@ x is Picture || x is ProductBundleItem || x is ProductCategory || x is ProductMa private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) { - var offset = Math.Max(ctx.Request.Profile.Offset, 0); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + var offset = Math.Max(ctx.Request.Profile.Offset, 0); var limit = Math.Max(ctx.Request.Profile.Limit, 0); var recordsPerSegment = ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0); - var totalRecords = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.StatsPerStore.First(x => x.Key == ctx.Store.Id).Value.TotalRecords; - - switch (ctx.Request.Provider.Value.EntityType) + var totalRecords = offset + stats.TotalRecords; + + ctx.LastId = stats.OffsetId; + + switch (ctx.Request.Provider.Value.EntityType) { case ExportEntityType.Product: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetProducts(ctx, skip), + () => GetProducts(ctx), entities => { // Load data behind navigation properties for current queue in one go. @@ -477,7 +480,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) case ExportEntityType.Order: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetOrders(ctx, skip), + () => GetOrders(ctx), entities => { ctx.OrderExportContext = new OrderExportContext(entities, @@ -505,7 +508,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) case ExportEntityType.Manufacturer: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetManufacturers(ctx, skip), + () => GetManufacturers(ctx), entities => { ctx.ManufacturerExportContext = new ManufacturerExportContext(entities, @@ -521,7 +524,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) case ExportEntityType.Category: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetCategories(ctx, skip), + () => GetCategories(ctx), entities => { ctx.CategoryExportContext = new CategoryExportContext(entities, @@ -537,7 +540,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) case ExportEntityType.Customer: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetCustomers(ctx, skip), + () => GetCustomers(ctx), entities => { ctx.CustomerExportContext = new CustomerExportContext(entities, @@ -552,7 +555,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) case ExportEntityType.NewsLetterSubscription: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetNewsLetterSubscriptions(ctx, skip), + () => GetNewsLetterSubscriptions(ctx), null, entity => Convert(ctx, entity), offset, PageSize, limit, recordsPerSegment, totalRecords @@ -562,7 +565,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) case ExportEntityType.ShoppingCartItem: ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( - skip => GetShoppingCartItems(ctx, skip), + () => GetShoppingCartItems(ctx), entities => { var products = entities.Select(x => x.Product); @@ -858,7 +861,7 @@ public virtual ProductExportContext CreateProductExportContext( return context; } - private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, int take) + private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, int take) { IQueryable query = null; @@ -916,6 +919,10 @@ private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, { query = query.Skip(() => skipValue); } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } if (take != int.MaxValue) { @@ -925,11 +932,22 @@ private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, return query; } - private List GetProducts(DataExporterContext ctx, int skip) + private List GetProducts(DataExporterContext ctx) { - // We use ctx.EntityIdsPerSegment to avoid exporting products multiple times per segment\file (cause of associated products). - var result = new List(); - var products = GetProductQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } + + var products = GetProductQuery(ctx, null, PageSize).ToList(); + if (!products.Any()) + { + return null; + } + + var result = new List(); Multimap associatedProducts = null; if (ctx.Projection.NoGroupedProducts) @@ -942,11 +960,11 @@ private List GetProducts(DataExporterContext ctx, int skip) { if (product.ProductType == ProductType.SimpleProduct || product.ProductType == ProductType.BundledProduct) { - if (!ctx.EntityIdsPerSegment.Contains(product.Id)) - { - result.Add(product); - ctx.EntityIdsPerSegment.Add(product.Id); - } + // We use ctx.EntityIdsPerSegment to avoid exporting products multiple times per segment\file (cause of associated products). + if (ctx.EntityIdsPerSegment.Add(product.Id)) + { + result.Add(product); + } } else if (product.ProductType == ProductType.GroupedProduct) { @@ -965,31 +983,30 @@ private List GetProducts(DataExporterContext ctx, int skip) continue; } - if (!ctx.EntityIdsPerSegment.Contains(associatedProduct.Id)) + if (ctx.EntityIdsPerSegment.Add(associatedProduct.Id)) { result.Add(associatedProduct); - ctx.EntityIdsPerSegment.Add(associatedProduct.Id); } } } } else { - if (!ctx.EntityIdsPerSegment.Contains(product.Id)) + if (ctx.EntityIdsPerSegment.Add(product.Id)) { result.Add(product); - ctx.EntityIdsPerSegment.Add(product.Id); } } } } - SetProgress(ctx, products.Count); + ctx.LastId = products.Last().Id; + SetProgress(ctx, products.Count); return result; } - private IQueryable GetOrderQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetOrderQuery(DataExporterContext ctx, int? skip, int take) { var query = _orderService.Value.GetOrders( ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId, @@ -1003,93 +1020,160 @@ private IQueryable GetOrderQuery(DataExporterContext ctx, int skip, int t null, null); - if (ctx.Request.EntitiesToExport.Any()) - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + if (ctx.Request.EntitiesToExport.Any()) + { + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + } - query = query.OrderByDescending(x => x.CreatedOnUtc); + query = query.OrderBy(x => x.Id); - if (skip > 0) - query = query.Skip(() => skip); + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } - if (take != int.MaxValue) - query = query.Take(() => take); + if (take != int.MaxValue) + { + query = query.Take(() => take); + } return query; } - private List GetOrders(DataExporterContext ctx, int skip) + private List GetOrders(DataExporterContext ctx) { - var orders = GetOrderQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } + + var orders = GetOrderQuery(ctx, null, PageSize).ToList(); + if (!orders.Any()) + { + return null; + } if (ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) { ctx.SetLoadedEntityIds(orders.Select(x => x.Id)); } - SetProgress(ctx, orders.Count); + ctx.LastId = orders.Last().Id; + SetProgress(ctx, orders.Count); return orders; } - private IQueryable GetManufacturerQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetManufacturerQuery(DataExporterContext ctx, int? skip, int take) { var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; var query = _manufacturerService.Value.GetManufacturers(true, storeId); - if (ctx.Request.EntitiesToExport.Any()) - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + if (ctx.Request.EntitiesToExport.Any()) + { + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + } - query = query.OrderBy(x => x.DisplayOrder); + query = query.OrderBy(x => x.Id); - if (skip > 0) - query = query.Skip(() => skip); + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } - if (take != int.MaxValue) - query = query.Take(() => take); + if (take != int.MaxValue) + { + query = query.Take(() => take); + } return query; } - private List GetManufacturers(DataExporterContext ctx, int skip) + private List GetManufacturers(DataExporterContext ctx) { - var manus = GetManufacturerQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } - SetProgress(ctx, manus.Count); + var manus = GetManufacturerQuery(ctx, null, PageSize).ToList(); + if (!manus.Any()) + { + return null; + } + + ctx.LastId = manus.Last().Id; + SetProgress(ctx, manus.Count); return manus; } - private IQueryable GetCategoryQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetCategoryQuery(DataExporterContext ctx, int? skip, int take) { var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; var query = _categoryService.Value.BuildCategoriesQuery(null, true, null, storeId); - if (ctx.Request.EntitiesToExport.Any()) - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + if (ctx.Request.EntitiesToExport.Any()) + { + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + } - query = query - .OrderBy(x => x.ParentCategoryId) - .ThenBy(x => x.DisplayOrder); + query = query.OrderBy(x => x.Id); - if (skip > 0) - query = query.Skip(() => skip); + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } - if (take != int.MaxValue) - query = query.Take(() => take); + if (take != int.MaxValue) + { + query = query.Take(() => take); + } return query; } - private List GetCategories(DataExporterContext ctx, int skip) + private List GetCategories(DataExporterContext ctx) { - var categories = GetCategoryQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } - SetProgress(ctx, categories.Count); + var categories = GetCategoryQuery(ctx, null, PageSize).ToList(); + if (!categories.Any()) + { + return null; + } + + ctx.LastId = categories.Last().Id; + SetProgress(ctx, categories.Count); return categories; } - private IQueryable GetCustomerQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetCustomerQuery(DataExporterContext ctx, int? skip, int take) { var query = _customerRepository.Value.TableUntracked .Expand(x => x.BillingAddress) @@ -1099,20 +1183,30 @@ private IQueryable GetCustomerQuery(DataExporterContext ctx, int skip, .Expand(x => x.CustomerRoles) .Where(x => !x.Deleted); - if (ctx.Filter.IsActiveCustomer.HasValue) - query = query.Where(x => x.Active == ctx.Filter.IsActiveCustomer.Value); + if (ctx.Filter.IsActiveCustomer.HasValue) + { + query = query.Where(x => x.Active == ctx.Filter.IsActiveCustomer.Value); + } - if (ctx.Filter.IsTaxExempt.HasValue) - query = query.Where(x => x.IsTaxExempt == ctx.Filter.IsTaxExempt.Value); + if (ctx.Filter.IsTaxExempt.HasValue) + { + query = query.Where(x => x.IsTaxExempt == ctx.Filter.IsTaxExempt.Value); + } - if (ctx.Filter.CustomerRoleIds != null && ctx.Filter.CustomerRoleIds.Length > 0) - query = query.Where(x => x.CustomerRoles.Select(y => y.Id).Intersect(ctx.Filter.CustomerRoleIds).Any()); + if (ctx.Filter.CustomerRoleIds != null && ctx.Filter.CustomerRoleIds.Any()) + { + query = query.Where(x => x.CustomerRoles.Select(y => y.Id).Intersect(ctx.Filter.CustomerRoleIds).Any()); + } - if (ctx.Filter.BillingCountryIds != null && ctx.Filter.BillingCountryIds.Length > 0) - query = query.Where(x => x.BillingAddress != null && ctx.Filter.BillingCountryIds.Contains(x.BillingAddress.Id)); + if (ctx.Filter.BillingCountryIds != null && ctx.Filter.BillingCountryIds.Any()) + { + query = query.Where(x => x.BillingAddress != null && ctx.Filter.BillingCountryIds.Contains(x.BillingAddress.Id)); + } - if (ctx.Filter.ShippingCountryIds != null && ctx.Filter.ShippingCountryIds.Length > 0) - query = query.Where(x => x.ShippingAddress != null && ctx.Filter.ShippingCountryIds.Contains(x.ShippingAddress.Id)); + if (ctx.Filter.ShippingCountryIds != null && ctx.Filter.ShippingCountryIds.Any()) + { + query = query.Where(x => x.ShippingAddress != null && ctx.Filter.ShippingCountryIds.Contains(x.ShippingAddress.Id)); + } if (ctx.Filter.LastActivityFrom.HasValue) { @@ -1154,40 +1248,67 @@ private IQueryable GetCustomerQuery(DataExporterContext ctx, int skip, .Select(x => x.Customer); } - if (ctx.Request.EntitiesToExport.Any()) - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + if (ctx.Request.EntitiesToExport.Any()) + { + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + } - query = query.OrderByDescending(x => x.CreatedOnUtc); + query = query.OrderBy(x => x.Id); - if (skip > 0) - query = query.Skip(() => skip); + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } - if (take != int.MaxValue) - query = query.Take(() => take); + if (take != int.MaxValue) + { + query = query.Take(() => take); + } return query; } - private List GetCustomers(DataExporterContext ctx, int skip) + private List GetCustomers(DataExporterContext ctx) { - var customers = GetCustomerQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } - SetProgress(ctx, customers.Count); + var customers = GetCustomerQuery(ctx, null, PageSize).ToList(); + if (!customers.Any()) + { + return null; + } + + ctx.LastId = customers.Last().Id; + SetProgress(ctx, customers.Count); return customers; } - private IQueryable GetNewsLetterSubscriptionQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetNewsLetterSubscriptionQuery(DataExporterContext ctx, int? skip, int take) { - var storeId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); + var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId; var query = _subscriptionRepository.Value.TableUntracked; - if (storeId > 0) - query = query.Where(x => x.StoreId == storeId); + if (storeId > 0) + { + query = query.Where(x => x.StoreId == storeId); + } - if (ctx.Filter.IsActiveSubscriber.HasValue) - query = query.Where(x => x.Active == ctx.Filter.IsActiveSubscriber.Value); + if (ctx.Filter.IsActiveSubscriber.HasValue) + { + query = query.Where(x => x.Active == ctx.Filter.IsActiveSubscriber.Value); + } if (ctx.Filter.WorkingLanguageId != null && ctx.Filter.WorkingLanguageId != 0) { @@ -1216,43 +1337,66 @@ private IQueryable GetNewsLetterSubscriptionQuery(DataEx query = query.Where(x => createdTo >= x.CreatedOnUtc); } - if (ctx.Request.EntitiesToExport.Any()) - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + if (ctx.Request.EntitiesToExport.Any()) + { + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + } - query = query - .OrderBy(x => x.StoreId) - .ThenBy(x => x.Email); + query = query.OrderBy(x => x.Id); - if (skip > 0) - query = query.Skip(() => skip); + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } - if (take != int.MaxValue) - query = query.Take(() => take); + if (take != int.MaxValue) + { + query = query.Take(() => take); + } return query; } - private List GetNewsLetterSubscriptions(DataExporterContext ctx, int skip) + private List GetNewsLetterSubscriptions(DataExporterContext ctx) { - var subscriptions = GetNewsLetterSubscriptionQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } + + var subscriptions = GetNewsLetterSubscriptionQuery(ctx, null, PageSize).ToList(); + if (!subscriptions.Any()) + { + return null; + } - SetProgress(ctx, subscriptions.Count); + ctx.LastId = subscriptions.Last().Id; + SetProgress(ctx, subscriptions.Count); return subscriptions; } - private IQueryable GetShoppingCartItemQuery(DataExporterContext ctx, int skip, int take) + private IQueryable GetShoppingCartItemQuery(DataExporterContext ctx, int? skip, int take) { - var storeId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); + var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId; var query = _shoppingCartItemRepository.Value.TableUntracked .Expand(x => x.Customer) .Expand(x => x.Customer.CustomerRoles) .Expand(x => x.Product) - .Where(x => !x.Customer.Deleted); // && !x.Product.Deleted + .Where(x => !x.Customer.Deleted); - if (storeId > 0) - query = query.Where(x => x.StoreId == storeId); + if (storeId > 0) + { + query = query.Where(x => x.StoreId == storeId); + } if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentCarts")) { @@ -1267,14 +1411,20 @@ private IQueryable GetShoppingCartItemQuery(DataExporterContex query = query.Where(x => x.ShoppingCartTypeId == ctx.Filter.ShoppingCartTypeId.Value); } - if (ctx.Filter.IsActiveCustomer.HasValue) - query = query.Where(x => x.Customer.Active == ctx.Filter.IsActiveCustomer.Value); + if (ctx.Filter.IsActiveCustomer.HasValue) + { + query = query.Where(x => x.Customer.Active == ctx.Filter.IsActiveCustomer.Value); + } - if (ctx.Filter.IsTaxExempt.HasValue) - query = query.Where(x => x.Customer.IsTaxExempt == ctx.Filter.IsTaxExempt.Value); + if (ctx.Filter.IsTaxExempt.HasValue) + { + query = query.Where(x => x.Customer.IsTaxExempt == ctx.Filter.IsTaxExempt.Value); + } - if (ctx.Filter.CustomerRoleIds != null && ctx.Filter.CustomerRoleIds.Length > 0) - query = query.Where(x => x.Customer.CustomerRoles.Select(y => y.Id).Intersect(ctx.Filter.CustomerRoleIds).Any()); + if (ctx.Filter.CustomerRoleIds != null && ctx.Filter.CustomerRoleIds.Any()) + { + query = query.Where(x => x.Customer.CustomerRoles.Select(y => y.Id).Intersect(ctx.Filter.CustomerRoleIds).Any()); + } if (ctx.Filter.LastActivityFrom.HasValue) { @@ -1309,30 +1459,50 @@ private IQueryable GetShoppingCartItemQuery(DataExporterContex query = query.Where(x => x.BundleItemId == null); } - if (ctx.Request.EntitiesToExport.Any()) - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + if (ctx.Request.EntitiesToExport.Any()) + { + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + } - query = query - .OrderBy(x => x.ShoppingCartTypeId) - .ThenBy(x => x.CustomerId) - .ThenByDescending(x => x.CreatedOnUtc); + query = query.OrderBy(x => x.Id); - if (skip > 0) - query = query.Skip(skip); + var skipValue = skip.GetValueOrDefault(); + if (skipValue > 0) + { + query = query.Skip(() => skipValue); + } + else if (ctx.LastId > 0) + { + query = query.Where(x => x.Id > ctx.LastId); + } - if (take != int.MaxValue) - query = query.Take(take); + if (take != int.MaxValue) + { + query = query.Take(take); + } return query; } - private List GetShoppingCartItems(DataExporterContext ctx, int skip) + private List GetShoppingCartItems(DataExporterContext ctx) { - var shoppingCartItems = GetShoppingCartItemQuery(ctx, skip, PageSize).ToList(); + var stats = ctx.StatsPerStore[ctx.Store.Id]; + if (ctx.LastId >= stats.MaxId) + { + // End of data reached. + return null; + } - SetProgress(ctx, shoppingCartItems.Count); + var cartItems = GetShoppingCartItemQuery(ctx, null, PageSize).ToList(); + if (!cartItems.Any()) + { + return null; + } + + ctx.LastId = cartItems.Last().Id; + SetProgress(ctx, cartItems.Count); - return shoppingCartItems; + return cartItems; } #endregion @@ -1426,14 +1596,22 @@ private List Init(DataExporterContext ctx) var stats = new RecordStats { - TotalRecords = query.Count(), - MaxId = query.Max(x => (int?)x.Id) ?? 0 + TotalRecords = query.Count() }; - ctx.StatsPerStore.Add(store.Id, stats); + if (!ctx.IsPreview) + { + stats.MaxId = query.Max(x => (int?)x.Id) ?? 0; + if (ctx.Request.Profile.Offset > 0) + { + stats.OffsetId = query.Select(x => x.Id).FirstOrDefault(); + } + } + + ctx.StatsPerStore[store.Id] = stats; } - return result; + return result; } private void ExportCoreInner(DataExporterContext ctx, Store store) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs index 6fcbd86367..cb44a39618 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs @@ -38,7 +38,7 @@ internal interface IExportDataSegmenterProvider : IExportDataSegmenterConsumer, public class ExportDataSegmenter : IExportDataSegmenterProvider where T : BaseEntity { - private readonly Func> _load; + private readonly Func> _load; private readonly Action> _loaded; private readonly Func> _convert; @@ -48,11 +48,11 @@ public class ExportDataSegmenter : IExportDataSegmenterProvider where T : Bas private readonly int _recordsPerSegment; private readonly int _totalRecords; - private int _skip; private Queue _data; + private bool _endOfData; public ExportDataSegmenter( - Func> load, + Func> load, Action> loaded, Func> convert, int offset, @@ -69,18 +69,16 @@ public ExportDataSegmenter( _limit = limit; _recordsPerSegment = recordsPerSegment; _totalRecords = totalRecords; - - _skip = _offset; } /// - /// Total number of records + /// Total number of records. /// public int TotalRecords { get { - var total = Math.Max(_totalRecords - _offset, 0); + var total = Math.Max(_totalRecords - _offset, 0); if (_limit > 0 && _limit < total) { @@ -92,37 +90,43 @@ public int TotalRecords } /// - /// Number of processed records + /// Number of processed records. /// public int RecordCount { get; private set; } /// - /// Record per segment counter + /// Record per segment counter. /// public int RecordPerSegmentCount { get; set; } /// - /// Whether there is data available + /// Whether there is data available. /// public bool HasData { get { - if (_limit > 0 && RecordCount >= _limit) - return false; + if (_limit > 0 && RecordCount >= _limit) + { + return false; + } - if (_data != null && _data.Count > 0) - return true; + if (_data != null && _data.Count > 0) + { + return true; + } - if (_skip >= _totalRecords) - return false; + if (_endOfData) + { + return false; + } return true; } } /// - /// Gets current data segment + /// Gets current data segment. /// public IReadOnlyCollection CurrentSegment { @@ -135,50 +139,66 @@ public IReadOnlyCollection CurrentSegment { _convert(entity).Each(x => records.Add(x)); - if (++RecordCount >= _limit && _limit > 0) - return records; + if (++RecordCount >= _limit && _limit > 0) + { + return records; + } - if (++RecordPerSegmentCount >= _recordsPerSegment && _recordsPerSegment > 0) - return records; + if (++RecordPerSegmentCount >= _recordsPerSegment && _recordsPerSegment > 0) + { + return records; + } } return records; } } - /// - /// Reads the next segment - /// - /// - public bool ReadNextSegment() + /// + /// Read next segment. + /// + /// true next segment available. false no more data. + public bool ReadNextSegment() { - if (_limit > 0 && RecordCount >= _limit) - return false; + if (_limit > 0 && RecordCount >= _limit) + { + return false; + } - if (_recordsPerSegment > 0 && RecordPerSegmentCount >= _recordsPerSegment) - return false; + if (_recordsPerSegment > 0 && RecordPerSegmentCount >= _recordsPerSegment) + { + return false; + } - // Do not make the queue longer than necessary. - if (_recordsPerSegment > 0 && _data != null && _data.Count >= _recordsPerSegment) - return true; + // Do not make the queue longer than necessary. + if (_recordsPerSegment > 0 && _data != null && _data.Count >= _recordsPerSegment) + { + return true; + } - if (_skip >= _totalRecords) - return false; + var newData = _load(); - if (_data != null) - _skip += _take; + if (_data != null && _data.Count > 0) + { + var data = new List(_data); + if (newData != null) + { + data.AddRange(newData); + } - if (_data != null && _data.Count > 0) - { - var data = new List(_data); - data.AddRange(_load(_skip)); + _data = new Queue(data); + } + else + { + if (newData == null) + { + // End of data reached. + _endOfData = true; + return false; + } - _data = new Queue(data); - } - else - { - _data = new Queue(_load(_skip)); - } + _data = new Queue(newData); + } // Give provider the opportunity to make something with entity ids. _loaded?.Invoke(_data.AsReadOnly()); @@ -191,14 +211,11 @@ public bool ReadNextSegment() /// public void Dispose() { - if (_data != null) - { - _data.Clear(); - } - - _skip = _offset; RecordCount = 0; RecordPerSegmentCount = 0; - } + + _endOfData = false; + _data?.Clear(); + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index aadfa50368..317d4400e3 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -47,7 +47,7 @@ public DataExporterContext( StatsPerStore = new Dictionary(); EntityIdsLoaded = new List(); - EntityIdsPerSegment = new List(); + EntityIdsPerSegment = new HashSet(); Result = new DataExportResult { @@ -80,7 +80,8 @@ public void SetLoadedEntityIds(IEnumerable ids) /// /// All entity identifiers per segment (to avoid exporting products multiple times). /// - public List EntityIdsPerSegment { get; set; } + public HashSet EntityIdsPerSegment { get; set; } + public int LastId { get; set; } public string ProgressInfo { get; set; } public int RecordCount { get; set; } @@ -150,5 +151,6 @@ internal class RecordStats { public int TotalRecords { get; set; } public int MaxId { get; set; } + public int OffsetId { get; set; } } } From d0c0ce88e62c1034b4250be722794da2e91ff20f Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 20 Nov 2018 23:32:00 +0100 Subject: [PATCH 036/657] (perf) new grouped index for product table to speed up seeking of very large catalogs. --- .../SmartStore.Core/Domain/Catalog/Product.cs | 6 +- ...2204501_ProductIndexSeekExport.Designer.cs | 29 ++++ .../201811202204501_ProductIndexSeekExport.cs | 35 +++++ ...01811202204501_ProductIndexSeekExport.resx | 126 ++++++++++++++++++ .../Migrations/MigrationsConfiguration.cs | 8 ++ .../SmartStore.Data/SmartStore.Data.csproj | 7 + .../Sql/Indexes.SqlServer.Inverse.sql | 3 + .../SmartStore.Data/Sql/Indexes.SqlServer.sql | 13 ++ .../DataExchange/Export/DataExporter.cs | 2 +- .../Catalog/LinqCatalogSearchService.cs | 4 +- .../Seo/XmlSitemapGenerator.cs | 2 +- 11 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.Designer.cs create mode 100644 src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs create mode 100644 src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.resx diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index 3d7f68fad0..6f18a1722d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs @@ -711,14 +711,14 @@ public decimal Height ///
[DataMember] [Index("IX_Product_Published_Deleted_IsSystemProduct", 1)] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets a value indicating whether the entity has been deleted /// [Index] [Index("IX_Product_Published_Deleted_IsSystemProduct", 2)] - public bool Deleted { get; set; } + public bool Deleted { get; set; } /// /// Gets or sets a value indicating whether the entity is a system product. @@ -726,7 +726,7 @@ public decimal Height [DataMember] [Index("IX_Product_SystemName_IsSystemProduct", 2)] [Index("IX_Product_Published_Deleted_IsSystemProduct", 3)] - public bool IsSystemProduct { get; set; } + public bool IsSystemProduct { get; set; } /// /// Gets or sets the product system name. diff --git a/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.Designer.cs new file mode 100644 index 0000000000..4205b2c00f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class ProductIndexSeekExport : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ProductIndexSeekExport)); + + string IMigrationMetadata.Id + { + get { return "201811202204501_ProductIndexSeekExport"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs new file mode 100644 index 0000000000..e78d62dc3f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs @@ -0,0 +1,35 @@ +namespace SmartStore.Data.Migrations +{ + using SmartStore.Core.Data; + using System; + using System.Data.Entity.Migrations; + + public partial class ProductIndexSeekExport : DbMigration + { + public override void Up() + { + if (DataSettings.Current.IsSqlServer) + { + Sql(@"CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] +( + [Published] ASC, + [Id] ASC, + [VisibleIndividually] ASC, + [Deleted] ASC, + [IsSystemProduct] ASC, + [AvailableStartDateTimeUtc] ASC, + [AvailableEndDateTimeUtc] ASC +) +INCLUDE ([UpdatedOnUtc]) WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]"); + } + } + + public override void Down() + { + if (DataSettings.Current.IsSqlServer) + { + DropIndex("dbo.Product", "IX_SeekExport1"); + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.resx b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.resx new file mode 100644 index 0000000000..95332172d1 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 75366a9612..801c1adbe2 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -14,6 +14,14 @@ public MigrationsConfiguration() AutomaticMigrationsEnabled = false; AutomaticMigrationDataLossAllowed = true; ContextKey = "SmartStore.Core"; + + var commandTimeout = CommonHelper.GetAppSetting("sm:EfMigrationsCommandTimeout"); + if (commandTimeout.HasValue) + { + CommandTimeout = commandTimeout.Value; + } + + CommandTimeout = 9999999; } public void SeedDatabase(SmartObjectContext context) diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 5c05c03a77..f7a601595a 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -648,6 +648,10 @@ 201811161142587_TaskHistoryErrorLength.cs + + + 201811202204501_ProductIndexSeekExport.cs + @@ -1163,6 +1167,9 @@ 201811161142587_TaskHistoryErrorLength.cs + + 201811202204501_ProductIndexSeekExport.cs + diff --git a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql index dbc47e8321..82855517b1 100644 --- a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql +++ b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql @@ -8,4 +8,7 @@ DROP INDEX [IX_PSAM_SpecificationAttributeOptionId_AllowFiltering] ON [Product_S GO DROP INDEX [IX_LocalizedProperty_Key] ON [LocalizedProperty] +GO + +DROP INDEX [IX_SeekExport1] ON [Product] GO \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql index ad39c3e86c..fc515b3c99 100644 --- a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql +++ b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql @@ -8,4 +8,17 @@ CREATE NONCLUSTERED INDEX [IX_PSAM_SpecificationAttributeOptionId_AllowFiltering GO CREATE NONCLUSTERED INDEX [IX_LocalizedProperty_Key] ON [LocalizedProperty] ([Id]) INCLUDE ([EntityId], [LocaleKeyGroup], [LocaleKey]) +GO + +CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] +( + [Published] ASC, + [Id] ASC, + [VisibleIndividually] ASC, + [Deleted] ASC, + [IsSystemProduct] ASC, + [AvailableStartDateTimeUtc] ASC, + [AvailableEndDateTimeUtc] ASC +) +INCLUDE ([UpdatedOnUtc]) WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY] GO \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index f6d348d134..ab29740212 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -874,8 +874,8 @@ private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, var searchQuery = new CatalogSearchQuery() .WithCurrency(ctx.ContextCurrency) .WithLanguage(ctx.ContextLanguage) - .HasStoreId(ctx.Request.Profile.PerStore ? ctx.Store.Id : f.StoreId) .VisibleIndividuallyOnly(true) + .HasStoreId(ctx.Request.Profile.PerStore ? ctx.Store.Id : f.StoreId) .PriceBetween(f.PriceMinimum, f.PriceMaximum) .WithStockQuantity(f.AvailabilityMinimum, f.AvailabilityMaximum) .CreatedBetween(createdFrom, createdTo); diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs index 7758cbb6ec..e026c8c4da 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs @@ -656,8 +656,8 @@ protected virtual IDictionary GetFacets(CatalogSearchQuery s { var count = 0; var hasActivePredefinedFacet = false; - var minPrice = _productRepository.Table.Where(x => !x.Deleted && x.Published && !x.IsSystemProduct).Min(x => (double)x.Price); - var maxPrice = _productRepository.Table.Where(x => !x.Deleted && x.Published && !x.IsSystemProduct).Max(x => (double)x.Price); + var minPrice = _productRepository.Table.Where(x => x.Published && !x.Deleted && !x.IsSystemProduct).Min(x => (double)x.Price); + var maxPrice = _productRepository.Table.Where(x => x.Published && !x.Deleted && !x.IsSystemProduct).Max(x => (double)x.Price); minPrice = FacetUtility.MakePriceEven(minPrice); maxPrice = FacetUtility.MakePriceEven(maxPrice); diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index 402bdc860a..e0f4162c2a 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -513,7 +513,7 @@ private IEnumerable EnumerateEntities(QueryHolder queries) //var limit = 0; while (maxId > 1) { - var products = queries.Products.AsNoTracking() + var products = query .Where(x => x.Id < maxId) .OrderByDescending(x => x.Id) .Take(() => MaximumSiteMapNodeCount) From ae77a686d9570679dbb683c78144600a94d9d005 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 20 Nov 2018 23:51:04 +0100 Subject: [PATCH 037/657] Picture uploader does not react anymore when included per AJAX --- .../Shared/Components/FileUploader.cshtml | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Views/Shared/Components/FileUploader.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/Components/FileUploader.cshtml index c41bef7f05..cbc4df6102 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/Components/FileUploader.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/Components/FileUploader.cshtml @@ -45,18 +45,16 @@ -@using (Html.BeginZoneContent("end")) -{ - -} \ No newline at end of file + + From 35a5eeb6962258fb1887c406a836def5a48b7ba3 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 21 Nov 2018 13:17:55 +0100 Subject: [PATCH 038/657] Export: Testing and correcting the updated data exporter --- .../Extensions/EnumerableExtensions.cs | 30 ++++++++++++ .../DataExchange/Export/DataExporter.cs | 47 ++++++++++++------- .../Deployment/FileSystemFilePublisher.cs | 2 +- .../Deployment/PublicFolderPublisher.cs | 4 +- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs index f345fa1b19..a711c68aa7 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -394,5 +394,35 @@ public static string BuildQueryString(this NameValueCollection nvc, Encoding enc } #endregion + + #region List + + /// + /// Safe way to remove selected entries from a list. + /// + /// To be used for materialized lists only, not IEnumerable or similar. + /// Object type. + /// List. + /// Selector for the entries to be removed. + /// Number of removed entries. + public static int Remove(this IList list, Func selector) + { + Guard.NotNull(list, nameof(list)); + Guard.NotNull(selector, nameof(selector)); + + var count = 0; + for (var i = list.Count - 1; i >= 0; i--) + { + if (selector(list[i])) + { + list.RemoveAt(i); + ++count; + } + } + + return count; + } + + #endregion } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index ab29740212..811dfa2abe 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -314,7 +314,6 @@ private void StreamToFile(DataExporterContext ctx, Stream stream, string path, A using (_rwLock.GetWriteLock()) using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) { - ctx.Log.Info($"Creating file {path}."); stream.CopyTo(fileStream); } } @@ -323,7 +322,7 @@ private void StreamToFile(DataExporterContext ctx, Stream stream, string path, A { ctx.ExecuteContext.Abort = DataExchangeAbortion.Hard; ctx.Log.ErrorFormat(ex, $"Failed to stream file {path}."); - ctx.Result.LastError = ex.ToString(); + ctx.Result.LastError = ex.ToAllMessages(true); } finally { @@ -640,6 +639,7 @@ private bool CallProvider(DataExporterContext ctx, string method, string path) } var context = ctx.ExecuteContext; + var provider = ctx.Request.Provider.Value; try { @@ -647,18 +647,18 @@ private bool CallProvider(DataExporterContext ctx, string method, string path) if (method == "Execute") { - ctx.Request.Provider.Value.Execute(context); + provider.Execute(context); } else if (method == "OnExecuted") { - ctx.Request.Provider.Value.OnExecuted(context); + provider.OnExecuted(context); } } catch (Exception ex) { context.Abort = DataExchangeAbortion.Hard; ctx.Log.ErrorFormat(ex, $"The provider failed at the {method.NaIfEmpty()} method."); - ctx.Result.LastError = ex.ToString(); + ctx.Result.LastError = ex.ToAllMessages(true); } finally { @@ -666,14 +666,20 @@ private bool CallProvider(DataExporterContext ctx, string method, string path) if (method == "Execute") { - ctx.Log.Info($"Provider reports {context.RecordsSucceeded.ToString("N0")} successfully exported record(s) of type {ctx.Request.Provider.Value.EntityType.ToString()}."); + var sb = new StringBuilder(); + sb.Append($"Provider reports {context.RecordsSucceeded.ToString("N0")} successfully exported record(s) of type {provider.EntityType.ToString()} to {path.NaIfEmpty()}."); - foreach (var unit in context.ExtraDataUnits.Where(x => x.RelatedType.HasValue)) + foreach (var unit in context.ExtraDataUnits.Where(x => x.RelatedType.HasValue && x.DataStream != null)) { - StreamToFile(ctx, unit.DataStream, Path.Combine(context.Folder, unit.FileName), x => unit.DataStream = null); + // Set unit.DataStream to null so that providers know that this unit should no longer be written to. + // We need these units later for ExportProfile.ResultInfo. + var unitPath = Path.Combine(context.Folder, unit.FileName); + StreamToFile(ctx, unit.DataStream, unitPath, x => unit.DataStream = null); - ctx.Log.Info($"Provider reports {unit.RecordsSucceeded.ToString("N0")} successfully exported record(s) of type {unit.RelatedType.Value.ToString()}."); + sb.Append($"\r\nProvider reports {unit.RecordsSucceeded.ToString("N0")} successfully exported record(s) of type {unit.RelatedType.Value.ToString()} to {unitPath.NaIfEmpty()}."); } + + ctx.Log.Info(sb.ToString()); } } @@ -741,10 +747,10 @@ private bool Deploy(DataExporterContext ctx, string zipPath) if (context.Result != null) { - context.Result.LastError = ex.ToAllMessages(); + context.Result.LastError = ex.ToAllMessages(true); } - ctx.Log.ErrorFormat(ex, "Deployment \"{0}\" of type {1} failed", deployment.Name, deployment.DeploymentType.ToString()); + ctx.Log.Error(ex, $"Deployment \"{deployment.Name}\" of type {deployment.DeploymentType.ToString()} failed."); } deployment.ResultInfo = XmlHelper.Serialize(context.Result); @@ -1671,7 +1677,7 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) context.PublicFolderPath = publicDeployment.GetDeploymentFolder(true); context.PublicFolderUrl = publicDeployment.GetPublicFolderUrl(_services, ctx.Store); - var fileExtension = provider.Value.FileExtension.HasValue() ? provider.Value.FileExtension.ToLower().EnsureStartsWith(".") : ""; + var fileExtension = provider.Value.FileExtension.HasValue() ? provider.Value.FileExtension.ToLower().EnsureStartsWith(".") : ""; using (var segmenter = CreateSegmenter(ctx)) { @@ -1880,7 +1886,7 @@ private void ExportCoreOuter(DataExporterContext ctx) catch (Exception ex) { logger.ErrorsAll(ex); - ctx.Result.LastError = ex.ToString(); + ctx.Result.LastError = ex.ToAllMessages(true); } finally { @@ -1888,6 +1894,7 @@ private void ExportCoreOuter(DataExporterContext ctx) { if (!ctx.IsPreview && profile.Id != 0) { + ctx.Result.Files = ctx.Result.Files.OrderBy(x => x.RelatedType).ToList(); profile.ResultInfo = XmlHelper.Serialize(ctx.Result); _exportProfileService.Value.UpdateExportProfile(profile); @@ -1940,10 +1947,14 @@ private void ExportCoreOuter(DataExporterContext ctx) { int? orderStatusId = null; - if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Processing) - orderStatusId = (int)OrderStatus.Processing; - else if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Complete) - orderStatusId = (int)OrderStatus.Complete; + if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Processing) + { + orderStatusId = (int)OrderStatus.Processing; + } + else if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Complete) + { + orderStatusId = (int)OrderStatus.Complete; + } using (var scope = new DbContextScope(_dbContext, false, null, false, false, false, false)) { @@ -1962,7 +1973,7 @@ private void ExportCoreOuter(DataExporterContext ctx) catch (Exception ex) { logger.ErrorsAll(ex); - ctx.Result.LastError = ex.ToString(); + ctx.Result.LastError = ex.ToAllMessages(true); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs index 8fdfe9ba43..346cf02176 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs @@ -19,7 +19,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); } - context.Log.Info("Copied export data files to " + targetFolder); + context.Log.Info($"Copied export data files to {targetFolder}."); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs index a008e69b0c..7e9d67beb3 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs @@ -27,7 +27,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de File.Copy(context.ZipPath, destinationFile, true); - context.Log.Info("Copied zipped export data to " + destinationFile); + context.Log.Info($"Copied zipped export data to {destinationFile}."); } } else @@ -37,7 +37,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); } - context.Log.Info("Copied export data files to " + destinationFolder); + context.Log.Info($"Copied export data files to {destinationFolder}."); } } } From ef76318ba584017da894e9b63ce9e3060205c9e7 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 21 Nov 2018 18:28:38 +0100 Subject: [PATCH 039/657] Updated change log --- changelog.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 828a9a7d03..0d87ccaebc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# Release Notes +# Release Notes ## SmartStore.NET 3.2 @@ -51,7 +51,9 @@ * At least 10x faster * Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary. * No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly. -* Debitoor: Partially update customer instead of full update to avoid all fields being overwritten +* **Debitoor**: + * Partially update customer instead of full update to avoid all fields being overwritten. + * #1540 Place company name in front of customer name (according to address format of the particular country). * #1479 Show in messages the delivery time at the time of purchase * #1184 Sort Current shopping carts & Current wishlists by ShoppingCartItem.CreatedOn. * #1106 BMECat: import & export support for product keywords From a8a594383fcff414eb755e5f996eb4c7a95d8acf Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Wed, 21 Nov 2018 22:33:26 +0100 Subject: [PATCH 040/657] Locale editor: hide tab labels from 8th child onward to prevent line wrapping --- .../Extensions/HtmlExtensions.cs | 1 + .../UI/Components/TabStrip/TabStripRenderer.cs | 13 ++++++------- .../Administration/Content/_admin.scss | 5 +++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index 3e3d884333..4fc585afd1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -82,6 +82,7 @@ public static HelperResult LocalizedEditor(this HtmlHel var language = languageService.GetLanguageById(locale.LanguageId); x.Add().Text(language.Name) + .LinkHtmlAttributes(new { title = language.Name }) .ContentHtmlAttributes(new { @class = "locale-editor-content", data_lang = language.LanguageCulture, data_rtl = language.Rtl.ToString().ToLower() }) .Content(localizedTemplate(i)) .ImageUrl("~/Content/images/flags/" + language.FlagImageFileName) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs index c55f9d03a0..3dadb4b8db 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs @@ -367,13 +367,16 @@ protected virtual string RenderItemLink(HtmlTextWriter writer, Tab item, int ind writer.RenderBeginTag("img"); writer.RenderEndTag(); // img } - //writer.WriteEncodedText(item.Text); + + // caption + writer.AddAttribute("class", "tab-caption"); + writer.RenderBeginTag("span"); + writer.WriteEncodedText(item.Text); + writer.RenderEndTag(); // Badge if (item.BadgeText.HasValue()) { - //writer.Write(" "); - // caption writer.AddAttribute("class", "tab-caption"); writer.RenderBeginTag("span"); @@ -392,10 +395,6 @@ protected virtual string RenderItemLink(HtmlTextWriter writer, Tab item, int ind writer.WriteEncodedText(item.BadgeText); writer.RenderEndTag(); // span > badge } - else - { - writer.WriteEncodedText(item.Text); - } // nav link short summary for collapsed state if (this.Component.IsResponsive && item.Summary.HasValue()) diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss index 7fd8438551..fbf1736a30 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss @@ -789,6 +789,11 @@ legend { } } + .nav-item:nth-child(n+8) > .nav-link { + > img, > i { margin-left: 0 !important; margin-right: 0 !important; } + > span { display: none; } + } + .nav-link:hover, .nav-link.active { color: inherit; From 8a42feecc16c98ee08ba76df84103cb4e8624276 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Fri, 23 Nov 2018 23:58:09 +0100 Subject: [PATCH 041/657] (Perf) FindEqualPicture with streams, not with byte arrays --- .../SmartStore.Core/Extensions/StreamExtensions.cs | 7 ++++--- .../SmartStore.Services/Media/PictureService.cs | 12 +++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs index e67a92a8a1..6b286561a0 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs @@ -68,7 +68,8 @@ public static bool ContentsEqual(this Stream src, Stream other) if (src.Length != other.Length) return false; - const int bufferSize = 2048; + const int intSize = sizeof(Int64); + const int bufferSize = 2048; var buffer1 = new byte[bufferSize]; var buffer2 = new byte[bufferSize]; @@ -77,7 +78,7 @@ public static bool ContentsEqual(this Stream src, Stream other) int len1 = src.Read(buffer1, 0, bufferSize); int len2 = other.Read(buffer2, 0, bufferSize); - if (len1 != len2) + if (len1 != len2) return false; if (len1 == 0) @@ -86,7 +87,7 @@ public static bool ContentsEqual(this Stream src, Stream other) int iterations = (int)Math.Ceiling((double)len1 / sizeof(Int64)); for (int i = 0; i < iterations; i++) { - if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64))) + if (BitConverter.ToInt64(buffer1, i * intSize) != BitConverter.ToInt64(buffer2, i * intSize)) { return false; } diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index 438499e8f1..850ea22bdb 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -208,14 +208,16 @@ public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType, out public virtual byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable pictures, out int equalPictureId) { equalPictureId = 0; + + var myStream = new MemoryStream(pictureBinary); + try { foreach (var picture in pictures) { - var otherPictureBinary = LoadPictureBinary(picture); + myStream.Seek(0, SeekOrigin.Begin); - using (var myStream = new MemoryStream(pictureBinary)) - using (var otherStream = new MemoryStream(otherPictureBinary)) + using (var otherStream = OpenPictureStream(picture)) { if (myStream.ContentsEqual(otherStream)) { @@ -231,6 +233,10 @@ public virtual byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable Date: Mon, 26 Nov 2018 21:00:08 +0100 Subject: [PATCH 042/657] Theming: minor CSS improvement --- src/Presentation/SmartStore.Web/Content/shared/_buttons.scss | 2 +- src/Presentation/SmartStore.Web/Scripts/public.common.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Content/shared/_buttons.scss b/src/Presentation/SmartStore.Web/Content/shared/_buttons.scss index 575ff64ead..49cf3b0235 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_buttons.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_buttons.scss @@ -594,7 +594,7 @@ $btn-size-sm: $input-height-sm; &:hover, &.hover, &:focus, &.focus { - transform: translateY(-3px); + transform: translateY(-2px); } &:active, &.active, diff --git a/src/Presentation/SmartStore.Web/Scripts/public.common.js b/src/Presentation/SmartStore.Web/Scripts/public.common.js index aca5b636c7..90a925b795 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.common.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.common.js @@ -160,7 +160,7 @@ $(function () { // Init reveal on scroll with AOS library if (typeof AOS !== 'undefined') { - AOS.init({ once: true }); + AOS.init({ once: true, duration: 1000 }); } if (SmartStore.parallax !== undefined && !$('body').hasClass('no-parallax')) { From 412efcfb277156bd3fbe811407f48f79901fc57e Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Wed, 28 Nov 2018 03:43:03 +0100 Subject: [PATCH 043/657] Fixed minor CSS issue --- .../SmartStore.Web/Content/vendors/aos/scss/_easing.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_easing.scss b/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_easing.scss index e0c261f227..b419e81343 100644 --- a/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_easing.scss +++ b/src/Presentation/SmartStore.Web/Content/vendors/aos/scss/_easing.scss @@ -33,10 +33,11 @@ $aos-easing: ( // Edit info (mc): // - Removed root [data-aos] // - body[data-aos-easing="#{$key}"] & >> body[data-aos-easing="#{$key}"] [data-aos] +// - prepended 'body' to second selector because of correct specifity // ---------------------------------- @each $key, $val in $aos-easing { body[data-aos-easing="#{$key}"] [data-aos], - [data-aos][data-aos-easing="#{$key}"] { + body [data-aos][data-aos-easing="#{$key}"] { transition-timing-function: $val; } } \ No newline at end of file From 43d20d58dd2fbb38bc290d8df51d91097e20e3d2 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Wed, 28 Nov 2018 23:31:52 +0100 Subject: [PATCH 044/657] Minor refactoring --- .../SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs index d465a2d0c8..81e8a86cdb 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs @@ -9,6 +9,7 @@ namespace SmartStore.Web.Framework.UI.Blocks public interface IBlockEntity { int Id { get; set; } + int StoryId { get; } string BlockType { get; set; } string Model { get; set; } string TagLine { get; set; } From ecaaba49167ba53b0d7251255497577ad6361b0f Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 29 Nov 2018 03:34:15 +0100 Subject: [PATCH 045/657] Minor localization issue --- .../SmartStore.Data/Migrations/MigrationsConfiguration.cs | 3 +++ .../SmartStore.Services/Catalog/CopyProductService.cs | 3 +-- .../Administration/Controllers/ProductController.cs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 801c1adbe2..9fb1c0698b 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -627,6 +627,9 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Top-Produkte auch in Unterseiten anzeigen", "Subpage: List index greater than 1 or any active filter.", "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); + + + builder.AddOrUpdate("Admin.Common.CopyOf", "Copy of {0}", "Kopie von {0}"); } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 52bb1b48e5..15088a1c1f 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -493,7 +493,6 @@ private void ProcessLocalization(Product product, Product clone, IEnumerable Date: Thu, 29 Nov 2018 23:12:07 +0100 Subject: [PATCH 046/657] Summernote: turned off 'prettifyHtml' option. It produces messy html code. --- .../SmartStore.Web/Content/editors/summernote/globalinit.js | 2 +- src/Presentation/SmartStore.Web/Themes/Web.config | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Content/editors/summernote/globalinit.js b/src/Presentation/SmartStore.Web/Content/editors/summernote/globalinit.js index cf990eb64a..65ad968dc6 100644 --- a/src/Presentation/SmartStore.Web/Content/editors/summernote/globalinit.js +++ b/src/Presentation/SmartStore.Web/Content/editors/summernote/globalinit.js @@ -29,7 +29,7 @@ var summernote_image_upload_url; dialogsInBody: true, dialogsFade: true, height: 300, - prettifyHtml: true, + prettifyHtml: false, onCreateLink: function (url) { // Prevents that summernote prepends "http://" to our links (WTF!!!) var c = url[0]; diff --git a/src/Presentation/SmartStore.Web/Themes/Web.config b/src/Presentation/SmartStore.Web/Themes/Web.config index 46e041887b..b59004c448 100644 --- a/src/Presentation/SmartStore.Web/Themes/Web.config +++ b/src/Presentation/SmartStore.Web/Themes/Web.config @@ -10,9 +10,10 @@ + - + \ No newline at end of file From 55fb8c9333e61966453b43f4f8390ffc4d1d3b4b Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 30 Nov 2018 11:23:49 +0100 Subject: [PATCH 047/657] Fixes tab caption rendered twice if tab item has a badge text --- .../UI/Components/TabStrip/TabStripRenderer.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs index 3dadb4b8db..cb070f79c0 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs @@ -368,7 +368,7 @@ protected virtual string RenderItemLink(HtmlTextWriter writer, Tab item, int ind writer.RenderEndTag(); // img } - // caption + // Caption writer.AddAttribute("class", "tab-caption"); writer.RenderBeginTag("span"); writer.WriteEncodedText(item.Text); @@ -377,13 +377,7 @@ protected virtual string RenderItemLink(HtmlTextWriter writer, Tab item, int ind // Badge if (item.BadgeText.HasValue()) { - // caption - writer.AddAttribute("class", "tab-caption"); - writer.RenderBeginTag("span"); - writer.WriteEncodedText(item.Text); - writer.RenderEndTag(); // span > badge - - // label/badge + // Label/badge temp = "ml-2 badge"; temp += " badge-" + item.BadgeStyle.ToString().ToLower(); if (base.Component.Position == TabsPosition.Left) @@ -396,7 +390,7 @@ protected virtual string RenderItemLink(HtmlTextWriter writer, Tab item, int ind writer.RenderEndTag(); // span > badge } - // nav link short summary for collapsed state + // Nav link short summary for collapsed state if (this.Component.IsResponsive && item.Summary.HasValue()) { writer.AddAttribute("class", "nav-link-summary"); From e33958b09d20c1deb38511db744ae327bf3711f9 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Tue, 4 Dec 2018 00:29:15 +0100 Subject: [PATCH 048/657] scrim-gradient Sass mixin --- .../Content/shared/_mixins.scss | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Presentation/SmartStore.Web/Content/shared/_mixins.scss b/src/Presentation/SmartStore.Web/Content/shared/_mixins.scss index e38caf13d7..20f549476e 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_mixins.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_mixins.scss @@ -4,6 +4,36 @@ // Mixins // ======================================================================== +@mixin scrim-gradient($start-color: #000, $direction: 'to bottom') { + $coords: ( + 0: 1, + 19: 0.738, + 34: 0.541, + 47: 0.382, + 56.5: 0.278, + 65: 0.194, + 73: 0.126, + 80.2: 0.075, + 86.1: 0.042, + 91: 0.021, + 95.2: 0.008, + 98.2: 0.002, + 100: 0 + ); + + $hue: hue($start-color); + $saturation: saturation($start-color); + $lightness: lightness($start-color); + $stops: (); + + @each $colorstop, $alpha in $coords { + $stop: hsla($hue, $saturation, $lightness, $alpha) percentage($colorstop/100); + $stops: append($stops, $stop, comma); + } + + background: linear-gradient(unquote($direction), $stops); +} + @mixin center($xy: xy) { @if $xy == xy { left: 50%; From f6cb634d56ade9d013350ed7897e9044e719ab9f Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Tue, 4 Dec 2018 12:46:48 +0100 Subject: [PATCH 049/657] Fixes linq search returns products multiple times. Reverts "Avoid group-by in linq search if possible because it's very slow if there are millions of products". --- .../Catalog/LinqCatalogSearchService.cs | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs index e026c8c4da..fcd96b6675 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs @@ -131,7 +131,6 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ { var ordered = false; var utcNow = DateTime.UtcNow; - var isGroupingRequired = false; var categoryId = 0; var manufacturerId = 0; var query = baseQuery ?? _productRepository.Table; @@ -161,7 +160,6 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ } else { - isGroupingRequired = true; query = QueryCategories(query, categoryIds, null); } } @@ -170,13 +168,11 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ var notFeaturedCategoryIds = GetIdList(filters, "notfeaturedcategoryid"); if (featuredCategoryIds.Any()) { - isGroupingRequired = true; categoryId = categoryId == 0 ? featuredCategoryIds.First() : categoryId; query = QueryCategories(query, featuredCategoryIds, true); } if (notFeaturedCategoryIds.Any()) { - isGroupingRequired = true; categoryId = categoryId == 0 ? notFeaturedCategoryIds.First() : categoryId; query = QueryCategories(query, notFeaturedCategoryIds, false); } @@ -192,7 +188,6 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ } else { - isGroupingRequired = true; query = QueryManufacturers(query, manufacturerIds, null); } } @@ -201,13 +196,11 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ var notFeaturedManuIds = GetIdList(filters, "notfeaturedmanufacturerid"); if (featuredManuIds.Any()) { - isGroupingRequired = true; manufacturerId = manufacturerId == 0 ? featuredManuIds.First() : manufacturerId; query = QueryManufacturers(query, featuredManuIds, true); } if (notFeaturedManuIds.Any()) { - isGroupingRequired = true; manufacturerId = manufacturerId == 0 ? notFeaturedManuIds.First() : manufacturerId; query = QueryManufacturers(query, notFeaturedManuIds, false); } @@ -215,7 +208,6 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ var tagIds = GetIdList(filters, "tagid"); if (tagIds.Any()) { - isGroupingRequired = true; query = from p in query from pt in p.ProductTags.Where(pt => tagIds.Contains(pt.Id)) @@ -227,7 +219,6 @@ from pt in p.ProductTags.Where(pt => tagIds.Contains(pt.Id)) var roleIds = GetIdList(filters, "roleid"); if (roleIds.Any()) { - isGroupingRequired = true; query = from p in query join acl in _aclRepository.Table on new { pid = p.Id, pname = "Product" } equals new { pid = acl.EntityId, pname = acl.EntityName } into pacl @@ -468,7 +459,6 @@ from acl in pacl.DefaultIfEmpty() var storeId = (int)filter.Term; if (storeId != 0) { - isGroupingRequired = true; query = from p in query join sm in _storeMappingRepository.Table on new { pid = p.Id, pname = "Product" } equals new { pid = sm.EntityId, pname = sm.EntityName } into psm @@ -482,15 +472,12 @@ from sm in psm.DefaultIfEmpty() #endregion - if (isGroupingRequired) - { - // Grouping is very slow if there are many products. - query = - from p in query - group p by p.Id into grp - orderby grp.Key - select grp.FirstOrDefault(); - } + // Grouping is very slow if there are many products. + query = + from p in query + group p by p.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); #region Sorting From 096ed3272607572220af736ac5c25eb6f95d74ce Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Wed, 5 Dec 2018 03:30:36 +0100 Subject: [PATCH 050/657] Removed urlCompression node from web.config --- src/Presentation/SmartStore.Web/Web.config | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Presentation/SmartStore.Web/Web.config b/src/Presentation/SmartStore.Web/Web.config index e8a9693af2..ab1f27c441 100644 --- a/src/Presentation/SmartStore.Web/Web.config +++ b/src/Presentation/SmartStore.Web/Web.config @@ -190,7 +190,6 @@ - From 54415415f94a4310aaa6c9abb0d6eb506d99ce18 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 6 Dec 2018 04:41:12 +0100 Subject: [PATCH 051/657] Minor issues --- README.md | 7 +++-- .../SmartStore.Web/Content/shared/_typo.scss | 4 ++- .../Scripts/smartstore.parallax.js | 29 ++++++++++++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aa25d27b32..95b669b8f9 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,17 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC * and many more... ## Project Status -SmartStore.NET V3.1.0 has been released on April 20, 2018. The highlights are: +SmartStore.NET V3.1.5 has been released on 25 May, 2018. The highlights are: * **Wallet \***: Enables full or partial order payment via credit account. Includes REST-Api. * **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting. * **Cash Rounding**: define money rounding rules on currency and payment method level. * **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface. -* **Enhanced MegaMenu \***: virtual dropdowns for surplus top-level categories and brands. * **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support. +* Search engine friendly topic URLs +* Compliance with **EU-GDPR** requirements +* **Enhanced MegaMenu \***: virtual dropdowns for surplus top-level categories and brands. +* **Honeypot** bot detection for registration and contact forms. * **Amazon Pay**: * Supports merchants registered in the USA and Japan * External authentication via *Login with Amazon* button in shop frontend diff --git a/src/Presentation/SmartStore.Web/Content/shared/_typo.scss b/src/Presentation/SmartStore.Web/Content/shared/_typo.scss index a736889cf2..3575656949 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_typo.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_typo.scss @@ -143,7 +143,9 @@ a.pretty-link { } .text-body { - font-size: $lead-font-size; + @if $font-size-base < 1rem { + font-size: $lead-font-size; + } } code { diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js index 3fdcc2bedd..73c5d4afb8 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js @@ -8,6 +8,8 @@ var initialized = false, stages = [], + // to adjust speed on smaller devices + speedRatios = { xs: 0.5, sm: 0.6, md: 0.7, lg: 0.9, xl: 1 }, viewport = ResponsiveBootstrapToolkit; function update() { @@ -24,18 +26,34 @@ if (!visible) return; - if (item.filter && !viewport.is(item.filter)) + if (item.filter && !viewport.is(item.filter)) { + if (item.initialized) { + // Restore original styling + el.css('background-position', item.originalPosition); + el.css('background-attachment', item.originalAttachment); + item.initialized = false; + } + return; + } + + speed = item.ratio * speedRatios[viewport.current()]; if (item.type === 'bg') { + if (!item.initialized) { + // for smoother scrolling + el.css('background-attachment', 'fixed'); + item.initialized = true; + } + // set bg parallax offset - var ypos = Math.round((top - scrollTop) * item.ratio) + item.offset; + var ypos = Math.round((top - scrollTop) * speed) + item.offset; el.css('background-position-y', ypos + "px"); } else if (item.type === 'content') { var bottom = top + height, rate = 100 / (bottom + winHeight - top) * ((scrollTop + winHeight) - top), - ytransform = (rate - 50) * (item.ratio * -3); + ytransform = (rate - 50) * (speed * -3); el.css(window.Prefixer.css('transform'), 'translate3d(0, ' + ytransform + 'px, 0)'); } @@ -55,7 +73,10 @@ type: el.data('parallax-type') || 'bg', filter: el.data('parallax-filter'), offset: (el.data('parallax-offset') || 0) * -1, - ratio: el.data('parallax-speed') || 0.5 + ratio: el.data('parallax-speed') || 0.5, + originalPosition: el.css('background-position'), + originalAttachment: el.css('background-attachment'), + initialized: false }; }); From 2e7e6f6c1eb1dff4065af29f6e5e99c7cda2d094 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 6 Dec 2018 11:50:45 +0100 Subject: [PATCH 052/657] Fixes "Column name 'IsSystemProduct' does not exist in the target table or view." Fixes "SqlCeCommand.CommandTimeout does not support non-zero values" Fixes "Direct index renaming is not supported by SQL Server Compact. To rename an index in SQL Server Compact, you will need to recreate it." What the hell does SQL Server Compact support anyway? Resolves #1544. --- .../201811061745204_IsSystemProductIndex.cs | 13 ++++++++++--- .../Migrations/MigrationsConfiguration.cs | 18 +++++++++++------- .../SmartStore.Data/ObjectContextBase.cs | 2 +- .../Setup/MigrateDatabaseInitializer.cs | 3 ++- .../SmartStore.Data/Sql/Indexes.SqlServer.sql | 13 ------------- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs index 0bdb446c38..a51d10dc8f 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs @@ -2,20 +2,27 @@ namespace SmartStore.Data.Migrations { using System; using System.Data.Entity.Migrations; - + using SmartStore.Core.Data; + public partial class IsSystemProductIndex : DbMigration { public override void Up() { DropIndex("dbo.Product", "IX_Product_Deleted_and_Published"); - RenameIndex(table: "dbo.Product", name: "Product_SystemName_IsSystemProduct", newName: "IX_Product_SystemName_IsSystemProduct"); + if (DataSettings.Current.IsSqlServer) + { + RenameIndex(table: "dbo.Product", name: "Product_SystemName_IsSystemProduct", newName: "IX_Product_SystemName_IsSystemProduct"); + } CreateIndex("dbo.Product", new[] { "Published", "Deleted", "IsSystemProduct" }, name: "IX_Product_Published_Deleted_IsSystemProduct"); } public override void Down() { DropIndex("dbo.Product", "IX_Product_Published_Deleted_IsSystemProduct"); - RenameIndex(table: "dbo.Product", name: "IX_Product_SystemName_IsSystemProduct", newName: "Product_SystemName_IsSystemProduct"); + if (DataSettings.Current.IsSqlServer) + { + RenameIndex(table: "dbo.Product", name: "IX_Product_SystemName_IsSystemProduct", newName: "Product_SystemName_IsSystemProduct"); + } CreateIndex("dbo.Product", new[] { "Published", "Deleted" }, name: "IX_Product_Deleted_and_Published"); } } diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 9fb1c0698b..1ae92861b0 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -3,7 +3,8 @@ using System; using System.Data.Entity.Migrations; using Setup; - using SmartStore.Core.Domain.Catalog; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Utilities; @@ -15,13 +16,16 @@ public MigrationsConfiguration() AutomaticMigrationDataLossAllowed = true; ContextKey = "SmartStore.Core"; - var commandTimeout = CommonHelper.GetAppSetting("sm:EfMigrationsCommandTimeout"); - if (commandTimeout.HasValue) - { - CommandTimeout = commandTimeout.Value; - } + if (DataSettings.Current.IsSqlServer) + { + var commandTimeout = CommonHelper.GetAppSetting("sm:EfMigrationsCommandTimeout"); + if (commandTimeout.HasValue) + { + CommandTimeout = commandTimeout.Value; + } - CommandTimeout = 9999999; + CommandTimeout = 9999999; + } } public void SeedDatabase(SmartObjectContext context) diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.cs index a00e80662f..0e7c9c73bf 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.cs @@ -42,7 +42,7 @@ protected ObjectContextBase(string nameOrConnectionString, string alias = null) this.Alias = null; this.DbHookHandler = NullDbHookHandler.Instance; - if (_commandTimeoutInSeconds >= 0) + if (_commandTimeoutInSeconds >= 0 && DataSettings.Current.IsSqlServer) { Database.CommandTimeout = _commandTimeoutInSeconds; } diff --git a/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs b/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs index c11b71aedc..c92790df60 100644 --- a/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs +++ b/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs @@ -4,6 +4,7 @@ using System.Data.Entity.Infrastructure; using System.Data.Entity.Migrations; using System.Linq; +using SmartStore.Core.Data; using SmartStore.Data.Migrations; using SmartStore.Utilities; @@ -134,7 +135,7 @@ protected virtual TConfig CreateConfiguration() config.TargetDatabase = new DbConnectionInfo(this.ConnectionString, dbContextInfo.ConnectionProviderName); } - if (config.CommandTimeout == null) + if (config.CommandTimeout == null && DataSettings.Current.IsSqlServer) { var commandTimeout = CommonHelper.GetAppSetting("sm:EfMigrationsCommandTimeout"); if (commandTimeout.HasValue) diff --git a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql index fc515b3c99..c871635093 100644 --- a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql +++ b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.sql @@ -9,16 +9,3 @@ GO CREATE NONCLUSTERED INDEX [IX_LocalizedProperty_Key] ON [LocalizedProperty] ([Id]) INCLUDE ([EntityId], [LocaleKeyGroup], [LocaleKey]) GO - -CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] -( - [Published] ASC, - [Id] ASC, - [VisibleIndividually] ASC, - [Deleted] ASC, - [IsSystemProduct] ASC, - [AvailableStartDateTimeUtc] ASC, - [AvailableEndDateTimeUtc] ASC -) -INCLUDE ([UpdatedOnUtc]) WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY] -GO \ No newline at end of file From 9faada8bad9c59ffc432dfc6f3ee810adc99291d Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 6 Dec 2018 20:45:02 +0100 Subject: [PATCH 053/657] Removed drop index IX_SeekExport1 from Indexes.SqlServer.Inverse.sql (not required) --- .../SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql index 82855517b1..709dabb0ec 100644 --- a/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql +++ b/src/Libraries/SmartStore.Data/Sql/Indexes.SqlServer.Inverse.sql @@ -9,6 +9,3 @@ GO DROP INDEX [IX_LocalizedProperty_Key] ON [LocalizedProperty] GO - -DROP INDEX [IX_SeekExport1] ON [Product] -GO \ No newline at end of file From 94aff1d5f20a214b477184c86abc9d0188e3c35d Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 6 Dec 2018 21:08:30 +0100 Subject: [PATCH 054/657] Disabled parallax effect for Firefox & Edge --- src/Presentation/SmartStore.Web/Scripts/public.common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Presentation/SmartStore.Web/Scripts/public.common.js b/src/Presentation/SmartStore.Web/Scripts/public.common.js index 90a925b795..f43b0140f8 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.common.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.common.js @@ -163,7 +163,7 @@ AOS.init({ once: true, duration: 1000 }); } - if (SmartStore.parallax !== undefined && !$('body').hasClass('no-parallax')) { + if (SmartStore.parallax !== undefined && !$('body').hasClass('no-parallax') && !$('html').hasClass('moz') && !$('html').hasClass('edge')) { SmartStore.parallax.init({ context: document.body, selector: '.parallax' From 02dcfb7c94ee3d22fecc499e4aa68d85b9e59624 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 8 Dec 2018 03:36:14 +0100 Subject: [PATCH 055/657] Fixed parallax FF & Edge issues --- .../Orders/OrderService.cs | 189 +----------------- .../Content/shared/_offcanvas.scss | 1 - .../SmartStore.Web/Content/shared/_utils.scss | 9 + .../SmartStore.Web/Scripts/public.common.js | 2 +- .../Scripts/smartstore.offcanvas.js | 2 +- .../Scripts/smartstore.parallax.js | 23 ++- 6 files changed, 28 insertions(+), 198 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index 62714bfebe..586f3656e1 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -13,13 +13,8 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// public partial class OrderService : IOrderService { - #region Fields - private readonly IRepository _orderRepository; private readonly IRepository _orderItemRepository; private readonly IRepository _orderNoteRepository; @@ -29,21 +24,6 @@ public partial class OrderService : IOrderService private readonly IRepository _returnRequestRepository; private readonly IEventPublisher _eventPublisher; - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Order repository - /// Order item repository - /// Order note repository - /// Product repository - /// Recurring payment repository - /// Customer repository - /// Return request repository - /// Event published public OrderService(IRepository orderRepository, IRepository orderItemRepository, IRepository orderNoteRepository, @@ -63,17 +43,6 @@ public OrderService(IRepository orderRepository, _eventPublisher = eventPublisher; } - #endregion - - #region Methods - - #region Orders - - /// - /// Gets an order - /// - /// The order identifier - /// Order public virtual Order GetOrderById(int orderId) { if (orderId == 0) @@ -82,11 +51,6 @@ public virtual Order GetOrderById(int orderId) return _orderRepository.GetById(orderId); } - /// - /// Get orders by identifiers - /// - /// Order identifiers - /// Order public virtual IList GetOrdersByIds(int[] orderIds) { if (orderIds == null || orderIds.Length == 0) @@ -122,11 +86,6 @@ public virtual Order GetOrderByNumber(string orderNumber) return order; } - /// - /// Gets an order - /// - /// The order identifier - /// Order public virtual Order GetOrderByGuid(Guid orderGuid) { if (orderGuid == Guid.Empty) @@ -139,12 +98,6 @@ public virtual Order GetOrderByGuid(Guid orderGuid) return order; } - /// - /// Get order by payment authorization data - /// - /// System name of the payment method - /// Authorization transaction Id - /// Order entity public virtual Order GetOrderByPaymentAuthorization(string paymentMethodSystemName, string authorizationTransactionId) { if (paymentMethodSystemName.IsEmpty() || authorizationTransactionId.IsEmpty()) @@ -158,12 +111,6 @@ from o in _orderRepository.Table return query.FirstOrDefault(); } - /// - /// Get order by payment capture data - /// - /// System name of the payment method - /// Capture transaction Id - /// Order entity public virtual Order GetOrderByPaymentCapture(string paymentMethodSystemName, string captureTransactionId) { if (paymentMethodSystemName.IsEmpty() || captureTransactionId.IsEmpty()) @@ -231,23 +178,6 @@ public virtual IQueryable GetOrders( return query; } - /// - /// Search orders - /// - /// Store identifier; 0 to load all orders - /// Customer identifier; 0 to load all orders - /// Order start time; null to load all orders - /// Order end time; null to load all orders - /// Filter by order status - /// Filter by payment status - /// Filter by shipping status - /// Billing email. Leave empty to load all records. - /// Search by order GUID (Global unique identifier) or part of GUID. Leave empty to load all orders. - /// Filter by order number - /// Page index - /// Page size - /// Billing name. Leave empty to load all records. - /// Order collection public virtual IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, DateTime? endTime, int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null) @@ -259,7 +189,7 @@ public virtual IPagedList SearchOrders(int storeId, int customerId, DateT if (orderGuid.HasValue()) { - //filter by GUID. Filter in BLL because EF doesn't support casting of GUID to string + // Filter by GUID. Filter in BLL because EF doesn't support casting of GUID to string var orders = query.ToList(); orders = orders.FindAll(x => x.OrderGuid.ToString().ToLowerInvariant().Contains(orderGuid.ToLowerInvariant())); @@ -267,18 +197,10 @@ public virtual IPagedList SearchOrders(int storeId, int customerId, DateT } else { - //database layer paging return new PagedList(query, pageIndex, pageSize); } } - /// - /// Gets all orders by affiliate identifier - /// - /// Affiliate identifier - /// Page index - /// Page size - /// Orders public virtual IPagedList GetAllOrders(int affiliateId, int pageIndex, int pageSize) { var query = _orderRepository.Table; @@ -290,20 +212,11 @@ public virtual IPagedList GetAllOrders(int affiliateId, int pageIndex, in return orders; } - /// - /// Load all orders - /// - /// Order collection public virtual IList LoadAllOrders() { return SearchOrders(0, 0, null, null, null, null, null, null, null, null, 0, int.MaxValue); } - /// - /// Gets all orders by affiliate identifier - /// - /// Affiliate identifier - /// Order collection public virtual IList GetOrdersByAffiliateId(int affiliateId) { var query = from o in _orderRepository.Table @@ -314,10 +227,6 @@ orderby o.CreatedOnUtc descending return orders; } - /// - /// Inserts an order - /// - /// Order public virtual void InsertOrder(Order order) { if (order == null) @@ -326,10 +235,6 @@ public virtual void InsertOrder(Order order) _orderRepository.Insert(order); } - /// - /// Updates the order - /// - /// The order public virtual void UpdateOrder(Order order) { if (order == null) @@ -340,10 +245,6 @@ public virtual void UpdateOrder(Order order) _eventPublisher.PublishOrderUpdated(order); } - /// - /// Deletes an order - /// - /// The order public virtual void DeleteOrder(Order order) { if (order == null) @@ -353,10 +254,6 @@ public virtual void DeleteOrder(Order order) UpdateOrder(order); } - /// - /// Deletes an order note - /// - /// The order note public virtual void DeleteOrderNote(OrderNote orderNote) { if (orderNote == null) @@ -370,12 +267,6 @@ public virtual void DeleteOrderNote(OrderNote orderNote) _eventPublisher.PublishOrderUpdated(order); } - /// - /// Get an order by authorization transaction ID and payment method system name - /// - /// Authorization transaction ID - /// Payment method system name - /// Order public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName) { var query = _orderRepository.Table; @@ -405,15 +296,8 @@ public virtual void AddOrderNote(Order order, string note, bool displayToCustome } } - #endregion - #region Order items - /// - /// Gets an Order item - /// - /// Order item identifier - /// Order item public virtual OrderItem GetOrderItemById(int orderItemId) { if (orderItemId == 0) @@ -422,11 +306,6 @@ public virtual OrderItem GetOrderItemById(int orderItemId) return _orderItemRepository.GetById(orderItemId); } - /// - /// Gets an Order item - /// - /// Order item identifier - /// Order item public virtual OrderItem GetOrderItemByGuid(Guid orderItemGuid) { if (orderItemGuid == Guid.Empty) @@ -438,19 +317,7 @@ public virtual OrderItem GetOrderItemByGuid(Guid orderItemGuid) var item = query.FirstOrDefault(); return item; } - - /// - /// Gets all Order items - /// - /// Order identifier; null to load all records - /// Customer identifier; null to load all records - /// Order start time; null to load all records - /// Order end time; null to load all records - /// Order status; null to load all records - /// Order payment status; null to load all records - /// Order shippment status; null to load all records - /// Value indicating whether to load downloadable products only - /// Order collection + public virtual IList GetAllOrderItems(int? orderId, int? customerId, DateTime? startTime, DateTime? endTime, OrderStatus? os, PaymentStatus? ps, ShippingStatus? ss, @@ -505,10 +372,6 @@ where orderIds.Contains(x.OrderId) return map; } - /// - /// Delete an Order item - /// - /// The Order item public virtual void DeleteOrderItem(OrderItem orderItem) { if (orderItem == null) @@ -526,10 +389,6 @@ public virtual void DeleteOrderItem(OrderItem orderItem) #region Recurring payments - /// - /// Deletes a recurring payment - /// - /// Recurring payment public virtual void DeleteRecurringPayment(RecurringPayment recurringPayment) { if (recurringPayment == null) @@ -539,11 +398,6 @@ public virtual void DeleteRecurringPayment(RecurringPayment recurringPayment) UpdateRecurringPayment(recurringPayment); } - /// - /// Gets a recurring payment - /// - /// The recurring payment identifier - /// Recurring payment public virtual RecurringPayment GetRecurringPaymentById(int recurringPaymentId) { if (recurringPaymentId == 0) @@ -552,10 +406,6 @@ public virtual RecurringPayment GetRecurringPaymentById(int recurringPaymentId) return _recurringPaymentRepository.GetById(recurringPaymentId); } - /// - /// Inserts a recurring payment - /// - /// Recurring payment public virtual void InsertRecurringPayment(RecurringPayment recurringPayment) { if (recurringPayment == null) @@ -566,10 +416,6 @@ public virtual void InsertRecurringPayment(RecurringPayment recurringPayment) _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); } - /// - /// Updates the recurring payment - /// - /// Recurring payment public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment) { if (recurringPayment == null) @@ -580,15 +426,6 @@ public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment) _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); } - /// - /// Search recurring payments - /// - /// The store identifier; 0 to load all records - /// The customer identifier; 0 to load all records - /// The initial order identifier; 0 to load all records - /// Initial order status identifier; null to load all records - /// A value indicating whether to show hidden records - /// Recurring payment collection public virtual IList SearchRecurringPayments(int storeId, int customerId, int initialOrderId, OrderStatus? initialOrderStatus, bool showHidden = false) @@ -623,10 +460,6 @@ where query1.Contains(rp.Id) #region Return requests - /// - /// Deletes a return request - /// - /// Return request public virtual void DeleteReturnRequest(ReturnRequest returnRequest) { if (returnRequest == null) @@ -640,11 +473,6 @@ public virtual void DeleteReturnRequest(ReturnRequest returnRequest) _eventPublisher.PublishOrderUpdated(orderItem.Order); } - /// - /// Gets a return request - /// - /// Return request identifier - /// Return request public virtual ReturnRequest GetReturnRequestById(int returnRequestId) { if (returnRequestId == 0) @@ -653,17 +481,6 @@ public virtual ReturnRequest GetReturnRequestById(int returnRequestId) return _returnRequestRepository.GetById(returnRequestId); } - /// - /// Search return requests - /// - /// Store identifier; 0 to load all entries - /// Customer identifier; null to load all entries - /// Order item identifier; null to load all entries - /// Return request status; null to load all entries - /// Page index - /// Page size - /// Return request Id - /// Return requests public virtual IPagedList SearchReturnRequests(int storeId, int customerId, int orderItemId, ReturnRequestStatus? rs, int pageIndex, int pageSize, int id = 0) { var query = _returnRequestRepository.Table; @@ -693,7 +510,5 @@ public virtual IPagedList SearchReturnRequests(int storeId, int c } #endregion - - #endregion } } diff --git a/src/Presentation/SmartStore.Web/Content/shared/_offcanvas.scss b/src/Presentation/SmartStore.Web/Content/shared/_offcanvas.scss index e0128c2b39..8a6a888075 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_offcanvas.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_offcanvas.scss @@ -17,7 +17,6 @@ $offcanvas-z-index: 99999; .canvas-slidable { z-index: 0; - transform: translate3d(0, 0, 0); transition: opacity $offcanvas-duration $offcanvas-easing, transform $offcanvas-duration $offcanvas-easing; diff --git a/src/Presentation/SmartStore.Web/Content/shared/_utils.scss b/src/Presentation/SmartStore.Web/Content/shared/_utils.scss index 58ea82127d..7a6440db88 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_utils.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_utils.scss @@ -93,6 +93,7 @@ // Data binding 3d rotate // ------------------------------------------------------ + @keyframes data-binding { 0% { transform: rotateY(0deg); @@ -350,6 +351,14 @@ table th { } +// Parallax stuff +// ------------------------------------------------------ + +.parallax[data-parallax-type=content] { + will-change: transform; +} + + // Misc utils // ------------------------- diff --git a/src/Presentation/SmartStore.Web/Scripts/public.common.js b/src/Presentation/SmartStore.Web/Scripts/public.common.js index f43b0140f8..90a925b795 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.common.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.common.js @@ -163,7 +163,7 @@ AOS.init({ once: true, duration: 1000 }); } - if (SmartStore.parallax !== undefined && !$('body').hasClass('no-parallax') && !$('html').hasClass('moz') && !$('html').hasClass('edge')) { + if (SmartStore.parallax !== undefined && !$('body').hasClass('no-parallax')) { SmartStore.parallax.init({ context: document.body, selector: '.parallax' diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.offcanvas.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.offcanvas.js index cb17a036b9..cfc222c8e8 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.offcanvas.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.offcanvas.js @@ -200,7 +200,7 @@ e.preventDefault(); self.hide(); }); - + body.addClass('canvas-sliding'); body.addClass('canvas-sliding-' + (this.options.placement == 'right' ? 'left' : 'right') diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js index 73c5d4afb8..cf535aacbc 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.parallax.js @@ -7,13 +7,20 @@ ; (function ($, window, document, undefined) { var initialized = false, + isTouch = Modernizr.touchevents, stages = [], - // to adjust speed on smaller devices - speedRatios = { xs: 0.5, sm: 0.6, md: 0.7, lg: 0.9, xl: 1 }, + //// to adjust speed on smaller devices + //speedRatios = { xs: 0.5, sm: 0.6, md: 0.7, lg: 0.9, xl: 1 }, viewport = ResponsiveBootstrapToolkit; function update() { _.each(stages, function (item, i) { + if (item.type == 'bg' && isTouch) { + // Found no proper way to make bg parallax + // run reliably on touch devices. + return; + } + var el = $(item.el); var winHeight = window.innerHeight; var scrollTop = window.pageYOffset; @@ -37,7 +44,7 @@ return; } - speed = item.ratio * speedRatios[viewport.current()]; + speed = item.speed; // * speedRatios[viewport.current()]; if (item.type === 'bg') { if (!item.initialized) { @@ -47,13 +54,13 @@ } // set bg parallax offset - var ypos = Math.round((top - scrollTop) * speed) + item.offset; - el.css('background-position-y', ypos + "px"); + var ypos = Math.round((top - scrollTop) * speed) + (item.offset * -1); + el.css('background-position', 'center ' + ypos + "px"); } else if (item.type === 'content') { var bottom = top + height, rate = 100 / (bottom + winHeight - top) * ((scrollTop + winHeight) - top), - ytransform = (rate - 50) * (speed * -3); + ytransform = (rate - 50) * (speed * -6) + item.offset; el.css(window.Prefixer.css('transform'), 'translate3d(0, ' + ytransform + 'px, 0)'); } @@ -71,9 +78,9 @@ return { el: val, type: el.data('parallax-type') || 'bg', + speed: toFloat(el.data('parallax-speed'), 0.5), + offset: (el.data('parallax-offset') || 0), filter: el.data('parallax-filter'), - offset: (el.data('parallax-offset') || 0) * -1, - ratio: el.data('parallax-speed') || 0.5, originalPosition: el.css('background-position'), originalAttachment: el.css('background-attachment'), initialized: false From f26d2482524030f7e1b86f4d33f674dd7c51c732 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Sun, 9 Dec 2018 19:08:53 +0100 Subject: [PATCH 056/657] Resolves #1551 IShippingMethodFilter: IsExcluded repeat every step checkout --- .../SmartStore.Web/Controllers/CheckoutController.cs | 11 +++-------- .../Models/Checkout/CheckoutProgressModel.cs | 2 -- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs index bde2fc97f0..eb45eca2ff 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs @@ -972,15 +972,10 @@ public ActionResult Completed() [ChildActionOnly] public ActionResult CheckoutProgress(CheckoutProgressStep step) { - var model = new CheckoutProgressModel() {CheckoutProgressStep = step}; - - var cart = _workContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, _storeContext.CurrentStore.Id); - var shippingOptions = _shippingService.GetShippingOptions(cart, _workContext.CurrentCustomer.ShippingAddress, "", _storeContext.CurrentStore.Id).ShippingOptions; - - if (shippingOptions.Count <= 1 && _shippingSettings.SkipShippingIfSingleOption) + var model = new CheckoutProgressModel { - model.DisplayShippingOptions = false; - } + CheckoutProgressStep = step + }; return PartialView(model); } diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs index 0093a6091f..5ef7e38f32 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs @@ -5,8 +5,6 @@ namespace SmartStore.Web.Models.Checkout public partial class CheckoutProgressModel : ModelBase { public CheckoutProgressStep CheckoutProgressStep { get; set; } - - public bool DisplayShippingOptions { get; set; } } public enum CheckoutProgressStep From be2581253da39e78c653969a9e9fa2b320ffede9 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 10 Dec 2018 11:20:01 +0100 Subject: [PATCH 057/657] Added default language note to installed languages list --- .../Migrations/MigrationsConfiguration.cs | 11 +++++++++-- .../Controllers/LanguageController.cs | 7 +++++++ .../Administration/Views/Language/List.cshtml | 15 ++++++++++----- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 1ae92861b0..c24d77ffd2 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -632,8 +632,15 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Subpage: List index greater than 1 or any active filter.", "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); - builder.AddOrUpdate("Admin.Common.CopyOf", "Copy of {0}", "Kopie von {0}"); - } + + builder.AddOrUpdate("Admin.Configuration.Languages.DefaultLanguage.Note", + "The default language of the shop is {0}. The default is always the first published language.", + "Die Standardsprache des Shops ist {0}. Standard ist stets die erste veröffentlichte Sprache."); + + builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages.Note", + "Click Download to install a new language including all localized resources. On translate.smartstore.com you will find more details about available resources.", + "Klicken Sie auf Download, um eine neue Sprache mit allen lokalisierten Ressourcen zu installieren. Auf translate.smartstore.com finden Sie weitere Details zu verfügbaren Ressourcen."); + } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs index 75f375c200..f5388be5d7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs @@ -335,6 +335,8 @@ public ActionResult List() var lastImportInfos = GetLastResourcesImportInfos(); var languages = _languageService.GetAllLanguages(true); + var defaultLanguageId = _languageService.GetDefaultLanguageId(); + var model = languages.Select(x => { var langModel = x.ToModel(); @@ -346,6 +348,11 @@ public ActionResult List() langModel.LastResourcesImportOnString = langModel.LastResourcesImportOn.Value.RelativeFormat(false, "f"); } + if (x.Id == defaultLanguageId) + { + ViewBag.DefaultLanguageNote = T("Admin.Configuration.Languages.DefaultLanguage.Note", langModel.Name).Text; + } + return langModel; }) .ToList(); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml index af4be8644d..9f7a2db4d1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml @@ -2,6 +2,8 @@ @model List @{ ViewBag.Title = T("Admin.Configuration.Languages").Text; + + var defaultLanguageNote = ViewBag.DefaultLanguageNote as string; }
@@ -18,6 +20,12 @@
@T("Admin.Configuration.Languages.InstalledLanguages")
+ @if (defaultLanguageNote.HasValue()) + { +
+ @Html.Raw(defaultLanguageNote) +
+ }
diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs index 20136eb7bd..427f941226 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs @@ -139,6 +139,10 @@ public ActionResult Category(int categoryId, CatalogSearchQuery query) } var model = category.ToModel(); + if (query.IsSubPage && !_catalogSettings.ShowDescriptionInSubPages) + { + model.Description.ChangeValue(string.Empty); + } _services.DisplayControl.Announce(category); @@ -148,49 +152,73 @@ public ActionResult Category(int categoryId, CatalogSearchQuery query) _helper.GetCategoryBreadCrumb(category.Id, 0).Select(x => x.Value).Each(x => _breadcrumb.Track(x)); } + // Products + int[] catIds = new int[] { categoryId }; + if (_catalogSettings.ShowProductsFromSubcategories) + { + // Include subcategories + catIds = catIds.Concat(_helper.GetChildCategoryIds(categoryId)).ToArray(); + } + + query.WithCategoryIds(_catalogSettings.IncludeFeaturedProductsInNormalLists ? (bool?)null : false, catIds); + + var searchResult = _catalogSearchService.Search(query); + model.SearchResult = searchResult; + + var mappingSettings = _helper.GetBestFitProductSummaryMappingSettings(query.GetViewMode()); + model.Products = _helper.MapProductSummaryModel(searchResult.Hits, mappingSettings); + model.SubCategoryDisplayType = _catalogSettings.SubCategoryDisplayType; var customerRolesIds = _services.WorkContext.CurrentCustomer.CustomerRoles.Where(x => x.Active).Select(x => x.Id).ToList(); - var subCategories = _categoryService.GetAllCategoriesByParentCategoryId(categoryId); int pictureSize = _mediaSettings.CategoryThumbPictureSize; - var allPictureInfos = _pictureService.GetPictureInfos(subCategories.Select(x => x.PictureId.GetValueOrDefault())); var fallbackType = _catalogSettings.HideCategoryDefaultPictures ? FallbackPictureType.NoFallback : FallbackPictureType.Entity; - // subcategories - model.SubCategories = subCategories - .Select(x => - { - var subCatName = x.GetLocalized(y => y.Name); - var subCatModel = new CategoryModel.SubCategoryModel - { - Id = x.Id, - Name = subCatName, - SeName = x.GetSeName(), - }; + var hideSubCategories = _catalogSettings.SubCategoryDisplayType == SubCategoryDisplayType.Hide + || (_catalogSettings.SubCategoryDisplayType == SubCategoryDisplayType.AboveProductList && query.IsSubPage && !_catalogSettings.ShowSubCategoriesInSubPages); + var hideFeaturedProducts = _catalogSettings.IgnoreFeaturedProducts || (query.IsSubPage && !_catalogSettings.IncludeFeaturedProductsInSubPages); + + // Subcategories + if (!hideSubCategories) + { + var subCategories = _categoryService.GetAllCategoriesByParentCategoryId(categoryId); + var allPictureInfos = _pictureService.GetPictureInfos(subCategories.Select(x => x.PictureId.GetValueOrDefault())); + + model.SubCategories = subCategories + .Select(x => + { + var subCatName = x.GetLocalized(y => y.Name); + var subCatModel = new CategoryModel.SubCategoryModel + { + Id = x.Id, + Name = subCatName, + SeName = x.GetSeName(), + }; - _services.DisplayControl.Announce(x); + _services.DisplayControl.Announce(x); // prepare picture model var pictureInfo = allPictureInfos.Get(x.PictureId.GetValueOrDefault()); - subCatModel.PictureModel = new PictureModel - { - PictureId = pictureInfo?.Id ?? 0, - Size = pictureSize, - ImageUrl = _pictureService.GetUrl(pictureInfo, pictureSize, fallbackType), - FullSizeImageUrl = _pictureService.GetUrl(pictureInfo, 0, FallbackPictureType.NoFallback), - FullSizeImageWidth = pictureInfo?.Width, - FullSizeImageHeight = pictureInfo?.Height, - Title = string.Format(T("Media.Category.ImageLinkTitleFormat"), subCatName), - AlternateText = string.Format(T("Media.Category.ImageAlternateTextFormat"), subCatName) - }; - - return subCatModel; - }) - .ToList(); + subCatModel.PictureModel = new PictureModel + { + PictureId = pictureInfo?.Id ?? 0, + Size = pictureSize, + ImageUrl = _pictureService.GetUrl(pictureInfo, pictureSize, fallbackType), + FullSizeImageUrl = _pictureService.GetUrl(pictureInfo, 0, FallbackPictureType.NoFallback), + FullSizeImageWidth = pictureInfo?.Width, + FullSizeImageHeight = pictureInfo?.Height, + Title = string.Format(T("Media.Category.ImageLinkTitleFormat"), subCatName), + AlternateText = string.Format(T("Media.Category.ImageAlternateTextFormat"), subCatName) + }; + + return subCatModel; + }) + .ToList(); + } // Featured Products - if (!_catalogSettings.IgnoreFeaturedProducts) + if (!hideFeaturedProducts) { CatalogSearchResult featuredProductsResult = null; @@ -224,21 +252,6 @@ public ActionResult Category(int categoryId, CatalogSearchQuery query) } } - // Products - int[] catIds = new int[] { categoryId }; - if (_catalogSettings.ShowProductsFromSubcategories) - { - // Include subcategories - catIds = catIds.Concat(_helper.GetChildCategoryIds(categoryId)).ToArray(); - } - - query.WithCategoryIds(_catalogSettings.IncludeFeaturedProductsInNormalLists ? (bool?)null : false, catIds); - - var searchResult = _catalogSearchService.Search(query); - model.SearchResult = searchResult; - - var mappingSettings = _helper.GetBestFitProductSummaryMappingSettings(query.GetViewMode()); - model.Products = _helper.MapProductSummaryModel(searchResult.Hits, mappingSettings); // Prepare paging/sorting/mode stuff _helper.MapListActions(model.Products, category, _catalogSettings.DefaultPageSizeOptions); @@ -345,14 +358,19 @@ public ActionResult Manufacturer(int manufacturerId, CatalogSearchQuery query) } var model = manufacturer.ToModel(); + if (query.IsSubPage && !_catalogSettings.ShowDescriptionInSubPages) + { + model.Description.ChangeValue(string.Empty); + } - // prepare picture model - model.PictureModel = _helper.PrepareManufacturerPictureModel(manufacturer, model.Name); + // prepare picture model + model.PictureModel = _helper.PrepareManufacturerPictureModel(manufacturer, model.Name); var customerRolesIds = _services.WorkContext.CurrentCustomer.CustomerRoles.Where(x => x.Active).Select(x => x.Id).ToList(); // Featured products - if (!_catalogSettings.IgnoreFeaturedProducts) + var hideFeaturedProducts = _catalogSettings.IgnoreFeaturedProducts || (query.IsSubPage && !_catalogSettings.IncludeFeaturedProductsInSubPages); + if (!hideFeaturedProducts) { CatalogSearchResult featuredProductsResult = null; diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml index 194fa5a748..5aa6fda3b1 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml @@ -84,7 +84,7 @@ @{ Html.RenderWidget("categorydetails_top");} @* Description *@ - @if (!String.IsNullOrWhiteSpace(Model.Description.Value)) + @if (!string.IsNullOrWhiteSpace(Model.Description.Value)) {
@Html.Raw(Html.CollapsedText(Model.Description)) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerTemplate.ProductsInGridOrLines.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerTemplate.ProductsInGridOrLines.cshtml index 854cbc0b3e..13bb45a4d0 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerTemplate.ProductsInGridOrLines.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerTemplate.ProductsInGridOrLines.cshtml @@ -53,7 +53,7 @@ @{ Html.RenderWidget("manufacturerdetails_top"); } @* Description *@ - @if (!String.IsNullOrWhiteSpace(Model.Description)) + @if (!string.IsNullOrWhiteSpace(Model.Description)) {
@Html.Raw(Html.CollapsedText(Model.Description)) From cf3f4882420d215e5041d37de5ca76ef6a7608dd Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Wed, 14 Nov 2018 23:03:03 +0100 Subject: [PATCH 018/657] Updated changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index db52cc3d08..fd7247162a 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ * Made Topic ACL enabled * Implemented paging & filtering for Topic grid * Topics: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties. +* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter). * New security option: Use invisible reCAPTCHA * **BeezUp**: * #1459 Add option to only submit one category name per product From 62d25a890ba6dc167454beea442df1a65839a23d Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 15 Nov 2018 03:18:29 +0100 Subject: [PATCH 019/657] Minor fix --- .../vendors/fileuploader/jquery.fileupload-single-ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Content/vendors/fileuploader/jquery.fileupload-single-ui.js b/src/Presentation/SmartStore.Web/Content/vendors/fileuploader/jquery.fileupload-single-ui.js index 8277cf3a04..602bef6586 100644 --- a/src/Presentation/SmartStore.Web/Content/vendors/fileuploader/jquery.fileupload-single-ui.js +++ b/src/Presentation/SmartStore.Web/Content/vendors/fileuploader/jquery.fileupload-single-ui.js @@ -251,7 +251,7 @@ var cnt = el.closest('.fileupload-container'); cnt.find('.fileupload-thumb').css('background-image', 'url("' + data.result.imageUrl + '")'); - cnt.find('.hidden').val(data.result.pictureId); + cnt.find('.hidden').val(data.result.pictureId).trigger('change'); elCancel.addClass("hide"); elFile.removeClass("hide"); @@ -281,7 +281,7 @@ var cnt = el.closest('.fileupload-container'); cnt.find('.fileupload-thumb').css('background-image', 'url("' + el.data('fallback-url') + '")'); - cnt.find('.hidden').val(0); + cnt.find('.hidden').val(0).trigger('change'); $(this).addClass("hide"); if (options.onFileRemove) options.onFileRemove.apply(this, [e, el]); }); From 8e3622aa042c13e60a5c590c0ab35017f243025a Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 15 Nov 2018 13:19:50 +0100 Subject: [PATCH 020/657] Export: Using new URL record property collection. Removed all per item loading of slugs and localized properties. --- .../Domain/Seo/UrlRecordCollection.cs | 14 +- .../DataExchange/Export/DataExporter.cs | 101 ++++++++---- .../Export/DynamicEntityHelper.cs | 146 +++++++++--------- .../Export/Internal/DataExporterContext.cs | 8 +- 4 files changed, 165 insertions(+), 104 deletions(-) diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs index 1863ef4bb5..733e800f7a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs @@ -1,12 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Globalization; namespace SmartStore.Core.Domain.Seo { - public class UrlRecordCollection : IReadOnlyCollection + public class UrlRecordCollection : IReadOnlyCollection { private readonly string _entityName; private readonly IDictionary _dict; @@ -52,9 +51,16 @@ public void MergeWith(UrlRecordCollection other) } } - public string GetSlug(int languageId, int entityId) + public string GetSlug(int languageId, int entityId, bool returnDefaultValue = true) { - return Find(languageId, entityId)?.Slug; + var slug = Find(languageId, entityId)?.Slug; + + if (returnDefaultValue && languageId != 0 && string.IsNullOrEmpty(slug)) + { + slug = Find(0, entityId)?.Slug; + } + + return slug; } public UrlRecord Find(int languageId, int entityId) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index cfa5efd919..770d561acc 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -44,6 +44,7 @@ using SmartStore.Utilities.Threading; using SmartStore.Collections; using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Seo; namespace SmartStore.Services.DataExchange.Export { @@ -58,7 +59,7 @@ public partial class DataExporter : IDataExporter private readonly HttpContextBase _httpContext; private readonly Lazy _priceFormatter; private readonly Lazy _exportProfileService; - private readonly ILocalizedEntityService _localizedEntityService; + private readonly Lazy _localizedEntityService; private readonly Lazy _languageService; private readonly Lazy _urlRecordService; private readonly Lazy _pictureService; @@ -99,14 +100,15 @@ public partial class DataExporter : IDataExporter private readonly Lazy _catalogSettings; private readonly Lazy _localizationSettings; private readonly Lazy _taxSettings; + private readonly Lazy _seoSettings; - public DataExporter( + public DataExporter( ICommonServices services, IDbContext dbContext, HttpContextBase httpContext, Lazy priceFormatter, Lazy exportProfileService, - ILocalizedEntityService localizedEntityService, + Lazy localizedEntityService, Lazy languageService, Lazy urlRecordService, Lazy pictureService, @@ -144,7 +146,8 @@ public DataExporter( Lazy customerSettings, Lazy catalogSettings, Lazy localizationSettings, - Lazy taxSettings) + Lazy taxSettings, + Lazy seoSettings) { _services = services; _dbContext = dbContext; @@ -192,6 +195,7 @@ public DataExporter( _catalogSettings = catalogSettings; _localizationSettings = localizationSettings; _taxSettings = taxSettings; + _seoSettings = seoSettings; T = NullLocalizer.Instance; } @@ -209,7 +213,18 @@ private LocalizedPropertyCollection CreateTranslationCollection(string keyGroup, return new LocalizedPropertyCollection(keyGroup, null, Enumerable.Empty()); } - var collection = _localizedEntityService.GetLocalizedPropertyCollection(keyGroup, entities.Select(x => x.Id).Distinct().ToArray()); + var collection = _localizedEntityService.Value.GetLocalizedPropertyCollection(keyGroup, entities.Select(x => x.Id).Distinct().ToArray()); + return collection; + } + + private UrlRecordCollection CreateUrlRecordCollection(string entityName, IEnumerable entities) + { + if (entities == null || !entities.Any()) + { + return new UrlRecordCollection(entityName, null, Enumerable.Empty()); + } + + var collection = _urlRecordService.Value.GetUrlRecordCollection(entityName, entities.Select(x => x.Id).Distinct().ToArray()); return collection; } @@ -219,13 +234,15 @@ private void SetProgress(DataExporterContext ctx, int loadedRecords) { if (!ctx.IsPreview && loadedRecords > 0) { - int totalRecords = ctx.RecordsPerStore.Sum(x => x.Value); + var totalRecords = ctx.RecordsPerStore.Sum(x => x.Value); - if (ctx.Request.Profile.Limit > 0 && totalRecords > ctx.Request.Profile.Limit) - totalRecords = ctx.Request.Profile.Limit; + if (ctx.Request.Profile.Limit > 0 && totalRecords > ctx.Request.Profile.Limit) + { + totalRecords = ctx.Request.Profile.Limit; + } ctx.RecordCount = Math.Min(ctx.RecordCount + loadedRecords, totalRecords); - var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount, totalRecords); + var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount.ToString("N0"), totalRecords.ToString("N0")); ctx.Request.ProgressValueSetter.Invoke(ctx.RecordCount, totalRecords, msg); } } @@ -327,6 +344,7 @@ private void DetachAllEntitiesAndClear(DataExporterContext ctx) { ctx.AssociatedProductContext?.Clear(); ctx.TranslationsPerPage?.Clear(); + ctx.UrlRecordsPerPage?.Clear(); if (ctx.ProductExportContext != null) { @@ -421,12 +439,14 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in var associatedProducts = context.AssociatedProducts.SelectMany(x => x.Value); ctx.AssociatedProductContext = CreateProductExportContext(associatedProducts, ctx.ContextCustomer, ctx.Store.Id); - var translationEntities = entities.Where(x => x.ProductType != ProductType.GroupedProduct).Concat(associatedProducts); - ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), translationEntities); + var allProductEntities = entities.Where(x => x.ProductType != ProductType.GroupedProduct).Concat(associatedProducts); + ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), allProductEntities); + ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), allProductEntities); } else { ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), entities); + ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), entities); } context.ProductTags.LoadAll(); @@ -468,7 +488,15 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in x => _orderService.Value.GetOrderItemsByOrderIds(x), x => _shipmentService.Value.GetShipmentsByOrderIds(x) ); - }, + + ctx.OrderExportContext.OrderItems.LoadAll(); + + var orderItems = ctx.OrderExportContext.OrderItems.SelectMany(x => x.Value); + var products = orderItems.Select(x => x.Product); + + ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), products); + ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), products); + }, entity => Convert(ctx, entity), offset, PageSize, limit, recordsPerSegment, totalCount ); @@ -535,8 +563,13 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetShoppingCartItems(ctx, skip), - null, - entity => Convert(ctx, entity), + entities => + { + var products = entities.Select(x => x.Product); + ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), products); + ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), products); + }, + entity => Convert(ctx, entity), offset, PageSize, limit, recordsPerSegment, totalCount ); break; @@ -1312,23 +1345,34 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) if (ctx.IsPreview) { - ctx.Translations[nameof(Currency)] = new LocalizedPropertyCollection(nameof(Currency), null, Enumerable.Empty()); - ctx.Translations[nameof(Country)] = new LocalizedPropertyCollection(nameof(Country), null, Enumerable.Empty()); - ctx.Translations[nameof(StateProvince)] = new LocalizedPropertyCollection(nameof(StateProvince), null, Enumerable.Empty()); - ctx.Translations[nameof(DeliveryTime)] = new LocalizedPropertyCollection(nameof(DeliveryTime), null, Enumerable.Empty()); - ctx.Translations[nameof(QuantityUnit)] = new LocalizedPropertyCollection(nameof(QuantityUnit), null, Enumerable.Empty()); - ctx.Translations[nameof(Manufacturer)] = new LocalizedPropertyCollection(nameof(Manufacturer), null, Enumerable.Empty()); - ctx.Translations[nameof(Category)] = new LocalizedPropertyCollection(nameof(Category), null, Enumerable.Empty()); + foreach (var name in new string[] { nameof(Currency), nameof(Country), nameof(StateProvince), nameof(DeliveryTime), nameof(QuantityUnit), nameof(Category), nameof(Manufacturer) }) + { + ctx.Translations[name] = new LocalizedPropertyCollection(name, null, Enumerable.Empty()); + } + + foreach (var name in new string[] { nameof(Product), nameof(ProductTag), nameof(ProductBundleItem), nameof(SpecificationAttribute), nameof(SpecificationAttributeOption), + nameof(ProductAttribute), nameof(ProductVariantAttributeValue) }) + { + ctx.TranslationsPerPage[name] = new LocalizedPropertyCollection(name, null, Enumerable.Empty()); + } + + ctx.UrlRecords[nameof(Category)] = new UrlRecordCollection(nameof(Category), null, Enumerable.Empty()); + ctx.UrlRecords[nameof(Manufacturer)] = new UrlRecordCollection(nameof(Manufacturer), null, Enumerable.Empty()); + ctx.UrlRecordsPerPage[nameof(Product)] = new UrlRecordCollection(nameof(Product), null, Enumerable.Empty()); } else { - ctx.Translations[nameof(Currency)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Currency), null); - ctx.Translations[nameof(Country)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Country), null); - ctx.Translations[nameof(StateProvince)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(StateProvince), null); - ctx.Translations[nameof(DeliveryTime)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(DeliveryTime), null); - ctx.Translations[nameof(QuantityUnit)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(QuantityUnit), null); - ctx.Translations[nameof(Manufacturer)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Manufacturer), null); - ctx.Translations[nameof(Category)] = _localizedEntityService.GetLocalizedPropertyCollection(nameof(Category), null); + // Get all translations and slugs for certain entities in one go. + ctx.Translations[nameof(Currency)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(Currency), null); + ctx.Translations[nameof(Country)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(Country), null); + ctx.Translations[nameof(StateProvince)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(StateProvince), null); + ctx.Translations[nameof(DeliveryTime)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(DeliveryTime), null); + ctx.Translations[nameof(QuantityUnit)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(QuantityUnit), null); + ctx.Translations[nameof(Manufacturer)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(Manufacturer), null); + ctx.Translations[nameof(Category)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(Category), null); + + ctx.UrlRecords[nameof(Category)] = _urlRecordService.Value.GetUrlRecordCollection(nameof(Category), null); + ctx.UrlRecords[nameof(Manufacturer)] = _urlRecordService.Value.GetUrlRecordCollection(nameof(Manufacturer), null); } if (!ctx.IsPreview && ctx.Request.Profile.PerStore) @@ -1687,6 +1731,7 @@ private void ExportCoreOuter(DataExporterContext ctx) ctx.DeliveryTimes.Clear(); ctx.Stores.Clear(); ctx.Translations.Clear(); + ctx.UrlRecords.Clear(); ctx.Request.CustomData.Clear(); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 201819554e..a91aa1ab6d 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -49,7 +49,7 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec { try { - var languageId = ctx.Projection.LanguageId ?? 0; + var languageId = ctx.LanguageId; string description = ""; // Description merging. @@ -222,12 +222,13 @@ private decimal CalculatePrice( private List GetLocalized( DataExporterContext ctx, - LocalizedPropertyCollection values, + LocalizedPropertyCollection translations, + UrlRecordCollection urlRecords, T entity, params Expression>[] keySelectors) where T : BaseEntity, ILocalizedEntity { - Guard.NotNull(values, nameof(values)); + Guard.NotNull(translations, nameof(translations)); if (ctx.Languages.Count <= 1) { @@ -236,16 +237,16 @@ private List GetLocalized( var localized = new List(); var localeKeyGroup = typeof(T).Name; - var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); + //var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); foreach (var language in ctx.Languages) { var languageCulture = language.Value.LanguageCulture.EmptyNull().ToLower(); // Add SEO name. - if (isSlugSupported) + if (urlRecords != null) { - var value = _urlRecordService.Value.GetActiveSlug(entity.Id, localeKeyGroup, language.Value.Id); + var value = urlRecords.GetSlug(language.Value.Id, entity.Id, false); if (value.HasValue()) { dynamic exp = new HybridExpando(); @@ -263,7 +264,7 @@ private List GetLocalized( var member = keySelector.Body as MemberExpression; var propInfo = member.Member as PropertyInfo; string localeKey = propInfo.Name; - var value = values.GetValue(language.Value.Id, entity.Id, localeKey); + var value = translations.GetValue(language.Value.Id, entity.Id, localeKey); // We do not export empty values to not fill databases with it. if (value.HasValue()) @@ -300,8 +301,8 @@ private dynamic ToDynamic(DataExporterContext ctx, Currency currency) dynamic result = new DynamicEntity(currency); var translations = ctx.Translations[nameof(Currency)]; - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, currency.Id, nameof(currency.Name)) ?? currency.Name; - result._Localized = GetLocalized(ctx, translations, currency, x => x.Name); + result.Name = translations.GetValue(ctx.LanguageId, currency.Id, nameof(currency.Name)) ?? currency.Name; + result._Localized = GetLocalized(ctx, translations, null, currency, x => x.Name); return result; } @@ -327,8 +328,8 @@ private dynamic ToDynamic(DataExporterContext ctx, Country country) dynamic result = new DynamicEntity(country); var translations = ctx.Translations[nameof(Country)]; - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, country.Id, nameof(country.Name)) ?? country.Name; - result._Localized = GetLocalized(ctx, translations, country, x => x.Name); + result.Name = translations.GetValue(ctx.LanguageId, country.Id, nameof(country.Name)) ?? country.Name; + result._Localized = GetLocalized(ctx, translations, null, country, x => x.Name); return result; } @@ -349,8 +350,8 @@ private dynamic ToDynamic(DataExporterContext ctx, Address address) dynamic sp = new DynamicEntity(address.StateProvince); var translations = ctx.Translations[nameof(StateProvince)]; - sp.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, address.StateProvince.Id, nameof(StateProvince)) ?? address.StateProvince.Name; - sp._Localized = GetLocalized(ctx, translations, address.StateProvince, x => x.Name); + sp.Name = translations.GetValue(ctx.LanguageId, address.StateProvince.Id, nameof(StateProvince)) ?? address.StateProvince.Name; + sp._Localized = GetLocalized(ctx, translations, null, address.StateProvince, x => x.Name); result.StateProvince = sp; } @@ -431,8 +432,8 @@ private dynamic ToDynamic(DataExporterContext ctx, DeliveryTime deliveryTime) dynamic result = new DynamicEntity(deliveryTime); var translations = ctx.Translations[nameof(DeliveryTime)]; - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, deliveryTime.Id, nameof(deliveryTime.Name)) ?? deliveryTime.Name; - result._Localized = GetLocalized(ctx, translations, deliveryTime, x => x.Name); + result.Name = translations.GetValue(ctx.LanguageId, deliveryTime.Id, nameof(deliveryTime.Name)) ?? deliveryTime.Name; + result._Localized = GetLocalized(ctx, translations, null, deliveryTime, x => x.Name); return result; } @@ -457,10 +458,10 @@ private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) dynamic result = new DynamicEntity(quantityUnit); var translations = ctx.Translations[nameof(QuantityUnit)]; - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, quantityUnit.Id, nameof(quantityUnit.Name)) ?? quantityUnit.Name; - result.Description = translations.GetValue(ctx.Projection.LanguageId ?? 0, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; + result.Name = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Name)) ?? quantityUnit.Name; + result.Description = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; - result._Localized = GetLocalized(ctx, translations, quantityUnit, + result._Localized = GetLocalized(ctx, translations, null, quantityUnit, x => x.Name, x => x.Description); @@ -508,7 +509,7 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttribute pva) return null; } - var languageId = ctx.Projection.LanguageId ?? 0; + var languageId = ctx.LanguageId; var attribute = pva.ProductAttribute; dynamic result = new DynamicEntity(pva); @@ -526,13 +527,13 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttribute pva) dynamic dyn = new DynamicEntity(x); dyn.Name = pvavTranslations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; - dyn._Localized = GetLocalized(ctx, pvavTranslations, x, y => y.Name); + dyn._Localized = GetLocalized(ctx, pvavTranslations, null, x, y => y.Name); return dyn; }) .ToList(); - dynAttribute._Localized = GetLocalized(ctx, paTranslations, attribute, + dynAttribute._Localized = GetLocalized(ctx, paTranslations, null, attribute, x => x.Name, x => x.Description); @@ -563,19 +564,20 @@ private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) dynamic result = new DynamicEntity(manufacturer); var translations = ctx.Translations[nameof(Manufacturer)]; + var urlRecords = ctx.UrlRecords[nameof(Manufacturer)]; result.Picture = null; - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; + result.Name = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; - if (!ctx.IsPreview) + if (!ctx.IsPreview) { - result.SeName = manufacturer.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - result.Description = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.Description)) ?? manufacturer.Description; - result.MetaKeywords = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.MetaKeywords)) ?? manufacturer.MetaKeywords; - result.MetaDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.MetaDescription)) ?? manufacturer.MetaDescription; - result.MetaTitle = translations.GetValue(ctx.Projection.LanguageId ?? 0, manufacturer.Id, nameof(manufacturer.MetaTitle)) ?? manufacturer.MetaTitle; + result.SeName = ctx.UrlRecords[nameof(Manufacturer)].GetSlug(ctx.LanguageId, manufacturer.Id); + result.Description = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.Description)) ?? manufacturer.Description; + result.MetaKeywords = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaKeywords)) ?? manufacturer.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaDescription)) ?? manufacturer.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaTitle)) ?? manufacturer.MetaTitle; - result._Localized = GetLocalized(ctx, translations, manufacturer, + result._Localized = GetLocalized(ctx, translations, urlRecords, manufacturer, x => x.Name, x => x.Description, x => x.MetaKeywords, @@ -595,25 +597,26 @@ private dynamic ToDynamic(DataExporterContext ctx, Category category) dynamic result = new DynamicEntity(category); var translations = ctx.Translations[nameof(Category)]; + var urlRecords = ctx.UrlRecords[nameof(Category)]; - result.Picture = null; - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.Name)) ?? category.Name; - result.FullName = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.FullName)) ?? category.FullName; + result.Picture = null; + result.Name = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.Name)) ?? category.Name; + result.FullName = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.FullName)) ?? category.FullName; if (!ctx.IsPreview) { - result.SeName = category.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - result.Description = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.Description)) ?? category.Description; - result.BottomDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.BottomDescription)) ?? category.BottomDescription; - result.MetaKeywords = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.MetaKeywords)) ?? category.MetaKeywords; - result.MetaDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.MetaDescription)) ?? category.MetaDescription; - result.MetaTitle = translations.GetValue(ctx.Projection.LanguageId ?? 0, category.Id, nameof(category.MetaTitle)) ?? category.MetaTitle; + result.SeName = ctx.UrlRecords[nameof(Category)].GetSlug(ctx.LanguageId, category.Id); + result.Description = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.Description)) ?? category.Description; + result.BottomDescription = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.BottomDescription)) ?? category.BottomDescription; + result.MetaKeywords = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaKeywords)) ?? category.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaDescription)) ?? category.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaTitle)) ?? category.MetaTitle; result._CategoryTemplateViewPath = ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId) ? ctx.CategoryTemplates[category.CategoryTemplateId] : ""; - result._Localized = GetLocalized(ctx, translations, category, + result._Localized = GetLocalized(ctx, translations, urlRecords, category, x => x.Name, x => x.FullName, x => x.Description, @@ -635,6 +638,8 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam dynamic result = new DynamicEntity(product); var translations = ctx.TranslationsPerPage[nameof(Product)]; + var urlRecords = ctx.UrlRecordsPerPage[nameof(Product)]; + var localizedName = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.Name)) ?? product.Name; result.AppliedDiscounts = null; result.Downloads = null; @@ -647,18 +652,17 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam result.ProductTags = null; result.ProductSpecificationAttributes = null; result.ProductBundleItems = null; - - result.Name = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.Name)) ?? product.Name; + result.Name = localizedName; if (!ctx.IsPreview) { - result.SeName = seName ?? product.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - result.ShortDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.ShortDescription)) ?? product.ShortDescription; - result.FullDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.FullDescription)) ?? product.FullDescription; - result.MetaKeywords = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.MetaKeywords)) ?? product.MetaKeywords; - result.MetaDescription = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.MetaDescription)) ?? product.MetaDescription; - result.MetaTitle = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.MetaTitle)) ?? product.MetaTitle; - result.BundleTitleText = translations.GetValue(ctx.Projection.LanguageId ?? 0, product.Id, nameof(product.BundleTitleText)) ?? product.BundleTitleText; + result.SeName = seName ?? ctx.UrlRecordsPerPage[nameof(Product)].GetSlug(ctx.LanguageId, product.Id); + result.ShortDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.ShortDescription)) ?? product.ShortDescription; + result.FullDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.FullDescription)) ?? product.FullDescription; + result.MetaKeywords = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaKeywords)) ?? product.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaDescription)) ?? product.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaTitle)) ?? product.MetaTitle; + result.BundleTitleText = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.BundleTitleText)) ?? product.BundleTitleText; result._ProductTemplateViewPath = ctx.ProductTemplates.ContainsKey(product.ProductTemplateId) ? ctx.ProductTemplates[product.ProductTemplateId] @@ -670,7 +674,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam ToDeliveryTime(ctx, result, product.DeliveryTimeId); ToQuantityUnit(ctx, result, product.QuantityUnitId); - result._Localized = GetLocalized(ctx, translations, product, + result._Localized = GetLocalized(ctx, translations, urlRecords, product, x => x.Name, x => x.ShortDescription, x => x.FullDescription, @@ -687,7 +691,6 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen { product.MergeWithCombination(productContext.Combination); - var languageId = ctx.Projection.LanguageId ?? 0; var numberOfPictures = ctx.Projection.NumberOfPictures ?? int.MaxValue; var productDetailsPictureSize = ctx.Projection.PictureSize > 0 ? ctx.Projection.PictureSize : _mediaSettings.Value.ProductDetailsPictureSize; @@ -734,7 +737,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen { var translations = ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)]; var valueNames = variantAttributeValues - .Select(x => translations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name) + .Select(x => translations.GetValue(ctx.LanguageId, x.Id, nameof(x.Name)) ?? x.Name) .ToList(); dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); @@ -904,10 +907,11 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen { dynamic dyn = new DynamicEntity(x); var translations = ctx.TranslationsPerPage[nameof(ProductTag)]; + var localizedName = translations.GetValue(ctx.LanguageId, x.Id, nameof(x.Name)) ?? x.Name; - dyn.Name = translations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; - dyn.SeName = x.GetSeName(languageId); - dyn._Localized = GetLocalized(ctx, translations, x, y => y.Name); + dyn.Name = localizedName; + dyn.SeName = SeoExtensions.GetSeName(localizedName, _seoSettings.Value); + dyn._Localized = GetLocalized(ctx, translations, null, x, y => y.Name); return dyn; }) @@ -927,9 +931,9 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen dynamic dyn = new DynamicEntity(x); var translations = ctx.TranslationsPerPage[nameof(ProductBundleItem)]; - dyn.Name = translations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; - dyn.ShortDescription = translations.GetValue(languageId, x.Id, nameof(x.ShortDescription)) ?? x.ShortDescription; - dyn._Localized = GetLocalized(ctx, translations, x, y => y.Name, y => y.ShortDescription); + dyn.Name = translations.GetValue(ctx.LanguageId, x.Id, nameof(x.Name)) ?? x.Name; + dyn.ShortDescription = translations.GetValue(ctx.LanguageId, x.Id, nameof(x.ShortDescription)) ?? x.ShortDescription; + dyn._Localized = GetLocalized(ctx, translations, null, x, y => y.Name, y => y.ShortDescription); return dyn; }) @@ -954,7 +958,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen { var translations = ctx.Translations[nameof(Manufacturer)]; var manufacturer = productManus.First().Manufacturer; - brand = translations.GetValue(languageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; + brand = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; } if (brand.IsEmpty()) { @@ -1074,9 +1078,9 @@ private dynamic ToDynamic(DataExporterContext ctx, Order order) dynamic result = new DynamicEntity(order); result.OrderNumber = order.GetOrderNumber(); - result.OrderStatus = order.OrderStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); - result.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); - result.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); + result.OrderStatus = order.OrderStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); + result.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); + result.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); result.Customer = null; result.BillingAddress = null; @@ -1159,7 +1163,6 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute return null; } - var languageId = ctx.Projection.LanguageId ?? 0; var option = psa.SpecificationAttributeOption; var attribute = option.SpecificationAttribute; @@ -1168,19 +1171,19 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute var saTranslations = ctx.TranslationsPerPage[nameof(SpecificationAttribute)]; var saoTranslations = ctx.TranslationsPerPage[nameof(SpecificationAttributeOption)]; - dynAttribute.Name = saTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Name)) ?? attribute.Name; - dynAttribute._Localized = GetLocalized(ctx, saTranslations, attribute, x => x.Name); + dynAttribute.Name = saTranslations.GetValue(ctx.LanguageId, attribute.Id, nameof(attribute.Name)) ?? attribute.Name; + dynAttribute._Localized = GetLocalized(ctx, saTranslations, null, attribute, x => x.Name); - dynAttribute.Alias = saTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Alias)) ?? attribute.Alias; - dynAttribute._Localized = GetLocalized(ctx, saTranslations, attribute, x => x.Alias); + dynAttribute.Alias = saTranslations.GetValue(ctx.LanguageId, attribute.Id, nameof(attribute.Alias)) ?? attribute.Alias; + dynAttribute._Localized = GetLocalized(ctx, saTranslations, null, attribute, x => x.Alias); dynamic dynOption = new DynamicEntity(option); - dynOption.Name = saoTranslations.GetValue(languageId, option.Id, nameof(option.Name)) ?? option.Name; - dynOption._Localized = GetLocalized(ctx, saoTranslations, option, x => x.Name); + dynOption.Name = saoTranslations.GetValue(ctx.LanguageId, option.Id, nameof(option.Name)) ?? option.Name; + dynOption._Localized = GetLocalized(ctx, saoTranslations, null, option, x => x.Name); - dynOption.Alias = saoTranslations.GetValue(languageId, option.Id, nameof(option.Alias)) ?? option.Alias; - dynOption._Localized = GetLocalized(ctx, saoTranslations, option, x => x.Alias); + dynOption.Alias = saoTranslations.GetValue(ctx.LanguageId, option.Id, nameof(option.Alias)) ?? option.Alias; + dynOption._Localized = GetLocalized(ctx, saoTranslations, null, option, x => x.Alias); dynOption.SpecificationAttribute = dynAttribute; result.SpecificationAttributeOption = dynOption; @@ -1241,7 +1244,8 @@ private List Convert(DataExporterContext ctx, Product product) { var result = new List(); var productContext = new DynamicProductContext(); - productContext.SeName = product.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); + + productContext.SeName = ctx.UrlRecordsPerPage[nameof(Product)].GetSlug(ctx.LanguageId, product.Id); productContext.Combinations = ctx.ProductExportContext.AttributeCombinations.GetOrLoad(product.Id); productContext.AbsoluteProductUrl = _productUrlHelper.Value.GetAbsoluteProductUrl( diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index 0c351245eb..75156360aa 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Seo; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Logging; @@ -41,6 +42,8 @@ public DataExporterContext( NewsletterSubscriptions = new HashSet(); Translations = new Dictionary(); TranslationsPerPage = new Dictionary(); + UrlRecords = new Dictionary(); + UrlRecordsPerPage = new Dictionary(); RecordsPerStore = new Dictionary(); EntityIdsLoaded = new List(); @@ -97,8 +100,9 @@ public bool Supports(ExportFeatures feature) public Currency ContextCurrency { get; set; } public Customer ContextCustomer { get; set; } public Language ContextLanguage { get; set; } + public int LanguageId => Projection.LanguageId ?? 0; - public TraceLogger Log { get; set; } + public TraceLogger Log { get; set; } public Store Store { get; set; } public string FolderContent { get; private set; } @@ -122,6 +126,7 @@ public bool IsFileBasedExport /// All translations for global scopes (like Category, Manufacturer etc.) /// public Dictionary Translations { get; set; } + public Dictionary UrlRecords { get; set; } // Data loaded once per page. public ProductExportContext ProductExportContext { get; set; } @@ -135,6 +140,7 @@ public bool IsFileBasedExport /// All per page translations (like ProductVariantAttributeValue etc.) /// public Dictionary TranslationsPerPage { get; set; } + public Dictionary UrlRecordsPerPage { get; set; } public ExportExecuteContext ExecuteContext { get; set; } public DataExportResult Result { get; set; } From 3a78734cd6bdf8f03570c1ed90523a73a6317e75 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 15 Nov 2018 15:13:55 +0100 Subject: [PATCH 021/657] Avoids a compiler warning --- .../Search/Catalog/CatalogSearchResult.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs index 1a215fcf79..c19da2f48f 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Search; @@ -10,10 +9,8 @@ namespace SmartStore.Services.Search { public partial class CatalogSearchResult { - private readonly int _totalHitsCount; - private readonly Func> _hitsFactory; + private readonly Func> _hitsFactory; private IPagedList _hits; - private bool? _isSubPage; public CatalogSearchResult( ISearchEngine engine, @@ -31,7 +28,7 @@ public CatalogSearchResult( Facets = facets ?? new Dictionary(); _hitsFactory = hitsFactory ?? (() => new List()); - _totalHitsCount = totalHitsCount; + TotalHitsCount = totalHitsCount; } /// @@ -52,26 +49,23 @@ public IPagedList Hits { if (_hits == null) { - var products = _totalHitsCount == 0 + var products = TotalHitsCount == 0 ? new List() : _hitsFactory.Invoke(); - _hits = new PagedList(products, Query.PageIndex, Query.Take, _totalHitsCount); + _hits = new PagedList(products, Query.PageIndex, Query.Take, TotalHitsCount); } return _hits; } } - public int TotalHitsCount - { - get { return _totalHitsCount; } - } + public int TotalHitsCount { get; } - /// - /// The original catalog search query - /// - public CatalogSearchQuery Query + /// + /// The original catalog search query + /// + public CatalogSearchQuery Query { get; private set; From 13c895270875f13506d83606255cd56aaeace0e3 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Fri, 16 Nov 2018 04:27:03 +0100 Subject: [PATCH 022/657] (Perf) some code change for upcoming XML sitemap generator refactoring --- src/Libraries/SmartStore.Core/BaseEntity.cs | 28 +++++++++++++------ .../Common/MaintenanceService.cs | 2 +- .../DataExchange/Export/DataExporter.cs | 6 ++-- .../Export/DynamicEntityHelper.cs | 2 +- .../DataExchange/ISyncMappingService.cs | 4 +-- .../DataExchange/SyncMappingService.cs | 2 +- .../Localization/LocalizationExtensions.cs | 14 +++++----- .../Localization/LocalizedEntityService.cs | 2 +- .../Security/AclService.cs | 4 +-- .../Security/IAclService.cs | 6 ++-- .../Seo/IUrlRecordService.cs | 2 +- .../SmartStore.Services/Seo/SeoExtensions.cs | 8 +++--- .../Seo/UrlRecordService.cs | 6 ++-- .../Stores/IStoreMappingService.cs | 6 ++-- .../Stores/StoreMappingService.cs | 4 +-- .../Tasks/TaskExecutionContext.cs | 11 +++++--- 16 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs index b591189f8d..3fd0016909 100644 --- a/src/Libraries/SmartStore.Core/BaseEntity.cs +++ b/src/Libraries/SmartStore.Core/BaseEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Data.Entity.Core.Objects; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Serialization; @@ -20,18 +21,27 @@ public abstract partial class BaseEntity : IEquatable [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public virtual string GetEntityName() + { + return GetUnproxiedType().Name; + } + public Type GetUnproxiedType() { - var t = GetType(); - if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity.")) - { - // it's a proxied type - t = t.BaseType; - } + #region Old + //var t = GetType(); + //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity.")) + //{ + // // it's a proxied type + // t = t.BaseType; + //} - return t; - } + //return t; + #endregion + + return ObjectContext.GetObjectType(GetType()); + } /// /// Transient objects are not associated with an item already in storage. For instance, diff --git a/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs b/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs index 069731f52a..d5b3c8d746 100644 --- a/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs +++ b/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs @@ -50,7 +50,7 @@ public MaintenanceService(IDataProvider dataProvider, IDbContext dbContext, { //stored procedures are enabled and supported by the database. - //TODO: find a better way to get table name + // TODO: find a better way to get table name var tableName = typeof(T).Name; var result = _dbContext.SqlQuery(string.Format("SELECT IDENT_CURRENT('[{0}]')", tableName)); return Convert.ToInt32(result.FirstOrDefault()); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 770d561acc..edbfeb6a4f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -224,7 +224,7 @@ private UrlRecordCollection CreateUrlRecordCollection(string entityName, IEnumer return new UrlRecordCollection(entityName, null, Enumerable.Empty()); } - var collection = _urlRecordService.Value.GetUrlRecordCollection(entityName, entities.Select(x => x.Id).Distinct().ToArray()); + var collection = _urlRecordService.Value.GetUrlRecordCollection(entityName, null, entities.Select(x => x.Id).Distinct().ToArray()); return collection; } @@ -1371,8 +1371,8 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) ctx.Translations[nameof(Manufacturer)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(Manufacturer), null); ctx.Translations[nameof(Category)] = _localizedEntityService.Value.GetLocalizedPropertyCollection(nameof(Category), null); - ctx.UrlRecords[nameof(Category)] = _urlRecordService.Value.GetUrlRecordCollection(nameof(Category), null); - ctx.UrlRecords[nameof(Manufacturer)] = _urlRecordService.Value.GetUrlRecordCollection(nameof(Manufacturer), null); + ctx.UrlRecords[nameof(Category)] = _urlRecordService.Value.GetUrlRecordCollection(nameof(Category), null, null); + ctx.UrlRecords[nameof(Manufacturer)] = _urlRecordService.Value.GetUrlRecordCollection(nameof(Manufacturer), null, null); } if (!ctx.IsPreview && ctx.Request.Profile.PerStore) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index a91aa1ab6d..7258f5d6c9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -236,7 +236,7 @@ private List GetLocalized( } var localized = new List(); - var localeKeyGroup = typeof(T).Name; + var localeKeyGroup = entity.GetEntityName(); //var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); foreach (var language in ctx.Languages) diff --git a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs index 9d08368ae3..9bdbc6d790 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs @@ -103,7 +103,7 @@ public static SyncMapping InsertSyncMapping(this ISyncMappingService svc, T e { ContextName = contextName, EntityId = entity.Id, - EntityName = typeof(T).Name, + EntityName = entity.GetEntityName(), SourceKey = sourceKey }; @@ -125,7 +125,7 @@ public static SyncMapping GetSyncMappingByEntity(this ISyncMappingService svc throw Error.InvalidOperation("Cannot get a sync mapping record for a transient (unsaved) entity"); } - return svc.GetSyncMappingByEntity(entity.Id, typeof(T).Name, contextName); + return svc.GetSyncMappingByEntity(entity.Id, entity.GetEntityName(), contextName); } /// diff --git a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs index 395a8f208a..8b455a6955 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs @@ -113,7 +113,7 @@ public void DeleteSyncMappingsFor(T entity) where T : BaseEntity return; } - _syncMappingsRepository.DeleteAll(x => x.EntityId == entity.Id && x.EntityName == typeof(T).Name); + _syncMappingsRepository.DeleteAll(x => x.EntityId == entity.Id && x.EntityName == entity.GetEntityName()); } public void DeleteSyncMappings(string contextName, string entityName = null) diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs index 09b6a8b701..eef621f93f 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs @@ -29,7 +29,7 @@ public static LocalizedValue GetLocalized(this T entity, Expression keySelector.Compile().Invoke(x), EngineContext.Current.Resolve().WorkingLanguage, @@ -57,7 +57,7 @@ public static LocalizedValue GetLocalized(this T entity, { return GetLocalizedEx( entity, - typeof(T).Name, + entity.GetEntityName(), LocaleKeyFromExpression(keySelector.Body), x => keySelector.Compile().Invoke(x), EngineContext.Current.Resolve().GetLanguageById(languageId), @@ -88,7 +88,7 @@ public static LocalizedValue GetLocalized(this T entity, { return GetLocalizedEx( entity, - typeof(T).Name, + entity.GetEntityName(), localeKey, x => fallback, language, @@ -118,7 +118,7 @@ public static LocalizedValue GetLocalized(this T entity, { return GetLocalizedEx( entity, - typeof(T).Name, + entity.GetEntityName(), LocaleKeyFromExpression(keySelector.Body), x => keySelector.Compile().Invoke(x), language, @@ -148,8 +148,8 @@ public static LocalizedValue GetLocalized(this T entity, where T : BaseEntity, ILocalizedEntity { return GetLocalizedEx( - entity, - typeof(T).Name, + entity, + entity.GetEntityName(), LocaleKeyFromExpression(keySelector.Body), x => keySelector.Compile().Invoke(x), EngineContext.Current.Resolve().GetLanguageById(languageId), @@ -180,7 +180,7 @@ public static LocalizedValue GetLocalized(this T entity, { return GetLocalizedEx( entity, - typeof(T).Name, + entity.GetEntityName(), LocaleKeyFromExpression(keySelector.Body), x => keySelector.Compile().Invoke(x), language, diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index d6fc49be18..b814d70e45 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -328,7 +328,7 @@ public virtual void SaveLocalizedValue( throw new ArgumentException($"Expression '{keySelector}' refers to a field, not a property."); } - var keyGroup = typeof(T).Name; + var keyGroup = entity.GetEntityName(); var key = propInfo.Name; var valueStr = localeValue.Convert(); var prop = GetLocalizedProperty(languageId, entity.Id, keyGroup, key); diff --git a/src/Libraries/SmartStore.Services/Security/AclService.cs b/src/Libraries/SmartStore.Services/Security/AclService.cs index 5fb2e87e7f..426fa881f1 100644 --- a/src/Libraries/SmartStore.Services/Security/AclService.cs +++ b/src/Libraries/SmartStore.Services/Security/AclService.cs @@ -73,7 +73,7 @@ public IList GetAclRecords(T entity) where T : BaseEntity, IAclSup Guard.NotNull(entity, nameof(entity)); int entityId = entity.Id; - string entityName = typeof(T).Name; + string entityName = entity.GetEntityName(); return GetAclRecordsFor(entityName, entityId); } @@ -131,7 +131,7 @@ public virtual void InsertAclRecord(T entity, int customerRoleId) where T : B throw new ArgumentOutOfRangeException(nameof(customerRoleId)); int entityId = entity.Id; - string entityName = typeof(T).Name; + string entityName = entity.GetEntityName(); var aclRecord = new AclRecord { diff --git a/src/Libraries/SmartStore.Services/Security/IAclService.cs b/src/Libraries/SmartStore.Services/Security/IAclService.cs index b6032be635..552f467f2b 100644 --- a/src/Libraries/SmartStore.Services/Security/IAclService.cs +++ b/src/Libraries/SmartStore.Services/Security/IAclService.cs @@ -115,7 +115,7 @@ public static int[] GetCustomerRoleIdsWithAccess(this IAclService aclService, if (entity == null) return new int[0]; - return aclService.GetCustomerRoleIdsWithAccess(typeof(T).Name, entity.Id); + return aclService.GetCustomerRoleIdsWithAccess(entity.GetEntityName(), entity.Id); } /// @@ -132,7 +132,7 @@ public static bool Authorize(this IAclService aclService, T entity) where T : if (!entity.SubjectToAcl) return true; - return aclService.Authorize(typeof(T).Name, entity.Id); + return aclService.Authorize(entity.GetEntityName(), entity.Id); } /// @@ -150,7 +150,7 @@ public static bool Authorize(this IAclService aclService, T entity, Customer if (!entity.SubjectToAcl) return true; - return aclService.Authorize(typeof(T).Name, entity.Id, customer); + return aclService.Authorize(entity.GetEntityName(), entity.Id, customer); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs index bc79cb22fc..639dc8efc1 100644 --- a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs @@ -94,7 +94,7 @@ public partial interface IUrlRecordService : IScopedService /// /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). /// - UrlRecordCollection GetUrlRecordCollection(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false); + UrlRecordCollection GetUrlRecordCollection(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false); /// /// Gets all URL records for the specified entity diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index 22224a45b4..d427ecc2d8 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -131,7 +131,7 @@ public static string GetSeName(this T entity) Guard.NotNull(entity, nameof(entity)); return GetSeName( - typeof(T).Name, + entity.GetEntityName(), entity.Id, EngineContext.Current.Resolve().WorkingLanguage.Id, EngineContext.Current.Resolve(), @@ -156,7 +156,7 @@ public static string GetSeName(this T entity, Guard.NotNull(entity, nameof(entity)); return GetSeName( - typeof(T).Name, + entity.GetEntityName(), entity.Id, languageId, EngineContext.Current.Resolve(), @@ -185,7 +185,7 @@ public static string GetSeName(this T entity, Guard.NotNull(entity, nameof(entity)); return GetSeName( - typeof(T).Name, + entity.GetEntityName(), entity.Id, languageId, urlRecordService, @@ -298,7 +298,7 @@ public static string ValidateSeName(this T entity, } // ensure this sename is not reserved yet - string entityName = typeof(T).Name; + string entityName = entity.GetEntityName(); int i = 2; var tempSeName = seName; diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index ea7d446964..eb3bd5ab25 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -198,9 +198,9 @@ public virtual void PrefetchUrlRecords(string entityName, int[] languageIds, int } } - public virtual UrlRecordCollection GetUrlRecordCollection(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false) + public virtual UrlRecordCollection GetUrlRecordCollection(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) { - return GetUrlRecordCollectionInternal(entityName, null, entityIds, isRange, isSorted); + return GetUrlRecordCollectionInternal(entityName, languageIds, entityIds, isRange, isSorted); } public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) @@ -335,7 +335,7 @@ public virtual UrlRecord SaveSlug(T entity, string slug, int languageId) wher Guard.NotNull(entity, nameof(entity)); int entityId = entity.Id; - string entityName = typeof(T).Name; + string entityName = entity.GetEntityName(); UrlRecord result = null; var query = from ur in _urlRecordRepository.Table diff --git a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs index 7eed774f70..02e9f61346 100644 --- a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs @@ -107,7 +107,7 @@ public static int[] GetStoresIdsWithAccess(this IStoreMappingService svc, T e if (entity == null) return new int[0]; - return svc.GetStoresIdsWithAccess(typeof(T).Name, entity.Id); + return svc.GetStoresIdsWithAccess(entity.GetEntityName(), entity.Id); } /// @@ -124,7 +124,7 @@ public static bool Authorize(this IStoreMappingService svc, T entity) where T if (!entity.LimitedToStores) return true; - return svc.Authorize(typeof(T).Name, entity.Id); + return svc.Authorize(entity.GetEntityName(), entity.Id); } /// @@ -142,7 +142,7 @@ public static bool Authorize(this IStoreMappingService svc, T entity, int sto if (!entity.LimitedToStores) return true; - return svc.Authorize(typeof(T).Name, entity.Id, storeId); + return svc.Authorize(entity.GetEntityName(), entity.Id, storeId); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs index 04bf8acfb0..d5578486ca 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs @@ -60,7 +60,7 @@ public virtual IList GetStoreMappings(T entity) where T : BaseE Guard.NotNull(entity, nameof(entity)); int entityId = entity.Id; - string entityName = typeof(T).Name; + string entityName = entity.GetEntityName(); var query = from sm in _storeMappingRepository.Table where sm.EntityId == entityId && @@ -121,7 +121,7 @@ public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseE throw new ArgumentOutOfRangeException(nameof(storeId)); int entityId = entity.Id; - string entityName = typeof(T).Name; + string entityName = entity.GetEntityName(); var storeMapping = new StoreMapping { diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index 9313933498..466e1db24f 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -7,10 +7,13 @@ namespace SmartStore.Services.Tasks { - /// - /// Provides the context for the Execute method of the interface. - /// - public class TaskExecutionContext + public delegate void ProgressCallback(int value, int maximum, string message); + + + /// + /// Provides the context for the Execute method of the interface. + /// + public class TaskExecutionContext { private readonly IComponentContext _componentContext; private readonly ScheduleTaskHistory _originalTaskHistory; From 32c01c889f8330fb814aac8c9f34c1617ff1e7b4 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 16 Nov 2018 12:56:26 +0100 Subject: [PATCH 023/657] Log full error message(s) for schedule task history entries --- .../Extensions/MiscExtensions.cs | 49 +++++-- .../Mapping/Tasks/ScheduleTaskHistoryMap.cs | 2 +- ...1142587_TaskHistoryErrorLength.Designer.cs | 29 ++++ .../201811161142587_TaskHistoryErrorLength.cs | 18 +++ ...01811161142587_TaskHistoryErrorLength.resx | 126 ++++++++++++++++++ .../SmartStore.Data/SmartStore.Data.csproj | 7 + .../SmartStore.Services/Tasks/TaskExecutor.cs | 2 +- .../Views/ScheduleTask/Edit.cshtml | 6 +- 8 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.Designer.cs create mode 100644 src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs create mode 100644 src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.resx diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index dac874caae..3241c7d8fa 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -3,7 +3,7 @@ using System.Text; namespace SmartStore -{ +{ public static class MiscExtensions { public static void Dump(this Exception exception) @@ -16,7 +16,7 @@ public static void Dump(this Exception exception) catch { } } - public static string ToAllMessages(this Exception exception) + public static string ToAllMessages(this Exception exception, bool includeStackTrace = false) { var sb = new StringBuilder(); @@ -24,8 +24,21 @@ public static string ToAllMessages(this Exception exception) { if (!sb.ToString().EmptyNull().Contains(exception.Message)) { - sb.Grow(exception.Message, " * "); + if (includeStackTrace) + { + if (sb.Length > 0) + { + sb.AppendLine(); + sb.AppendLine(); + } + sb.AppendLine(exception.ToString()); + } + else + { + sb.Grow(exception.Message, " * "); + } } + exception = exception.InnerException; } @@ -52,8 +65,10 @@ public static bool IsNullOrDefault(this T? value) where T : struct /// public static string ToHexString(this byte[] bytes, int length = 0) { - if (bytes == null || bytes.Length <= 0) - return ""; + if (bytes == null || bytes.Length <= 0) + { + return ""; + } var sb = new StringBuilder(); @@ -61,9 +76,12 @@ public static string ToHexString(this byte[] bytes, int length = 0) { sb.Append(b.ToString("x2")); - if (length > 0 && sb.Length >= length) - break; + if (length > 0 && sb.Length >= length) + { + break; + } } + return sb.ToString(); } @@ -75,21 +93,24 @@ public static string ToHexString(this byte[] bytes, int length = 0) /// Delimiter to use public static void Grow(this StringBuilder sb, string grow, string delimiter) { - if (delimiter == null) - throw new ArgumentNullException(nameof(delimiter)); + Guard.NotNull(delimiter, nameof(delimiter)); if (!string.IsNullOrWhiteSpace(grow)) { - if (sb.Length <= 0) - sb.Append(grow); - else - sb.AppendFormat("{0}{1}", delimiter, grow); + if (sb.Length <= 0) + { + sb.Append(grow); + } + else + { + sb.AppendFormat("{0}{1}", delimiter, grow); + } } } public static string SafeGet(this string[] arr, int index) { - return (arr != null && index < arr.Length ? arr[index] : ""); + return arr != null && index < arr.Length ? arr[index] : ""; } } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs index 8a715d40a8..817cabd4e6 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs @@ -10,7 +10,7 @@ public ScheduleTaskHistoryMap() ToTable("ScheduleTaskHistory"); HasKey(x => x.Id); Property(x => x.MachineName).IsRequired().HasMaxLength(400); - Property(x => x.Error).HasMaxLength(1000); + Property(x => x.Error); Property(x => x.ProgressMessage).HasMaxLength(1000); HasRequired(x => x.ScheduleTask) diff --git a/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.Designer.cs new file mode 100644 index 0000000000..61f95300e6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class TaskHistoryErrorLength : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(TaskHistoryErrorLength)); + + string IMigrationMetadata.Id + { + get { return "201811161142587_TaskHistoryErrorLength"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs new file mode 100644 index 0000000000..6a1180c68d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs @@ -0,0 +1,18 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class TaskHistoryErrorLength : DbMigration + { + public override void Up() + { + AlterColumn("dbo.ScheduleTaskHistory", "Error", c => c.String()); + } + + public override void Down() + { + AlterColumn("dbo.ScheduleTaskHistory", "Error", c => c.String(maxLength: 1000)); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.resx b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.resx new file mode 100644 index 0000000000..95332172d1 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 5a84c2e130..5c05c03a77 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -644,6 +644,10 @@ 201811082148279_LocalizedPropertyKeyGroupIndex.cs + + + 201811161142587_TaskHistoryErrorLength.cs + @@ -1156,6 +1160,9 @@ 201811082148279_LocalizedPropertyKeyGroupIndex.cs + + 201811161142587_TaskHistoryErrorLength.cs + diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs index 6190bf0e94..361141e2c8 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -128,7 +128,7 @@ public void Execute( exception = ex; faulted = true; canceled = ex is OperationCanceledException; - lastError = ex.Message.Truncate(995, "..."); + lastError = ex.ToAllMessages(true); if (canceled) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml index 7b4b687a79..2a7590b235 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml @@ -120,12 +120,12 @@ @if (Model.LastHistoryEntry.Error.HasValue()) {
+ @Html.SmartLabelFor(model => model.LastHistoryEntry.Error)
- @Html.DisplayFor(model => model.LastHistoryEntry.Error) +
@Html.DisplayFor(model => model.LastHistoryEntry.Error)
@@ -92,10 +100,7 @@ +
@using (Ajax.BeginForm("Authenticate", new AjaxOptions { @@ -74,33 +77,36 @@
} -
- @if (Model.Title.HasValue()) - { -
-
- @Html.Raw("<" + Model.TitleTag + Html.LanguageAttributes(Model.Title) + " class=\"block-title\">") - @Html.Raw(Model.Title) - @Html.Raw("") +@if (Model.Title.HasValue() || Model.Intro.HasValue() || Model.Body.HasValue()) +{ +
+ @if (Model.Title.HasValue()) + { +
+
+ @Html.Raw("<" + Model.TitleTag + Html.LanguageAttributes(Model.Title) + " class=\"block-title\">") + @Html.Raw(Model.Title) + @Html.Raw("") +
-
- } + } - @if (Model.Intro.HasValue()) - { -

- @Html.Raw(Model.Intro) -

- } + @if (Model.Intro.HasValue()) + { +

+ @Html.Raw(Model.Intro) +

+ } - @{ Html.RenderWidget("topic_body_before"); } + @{ Html.RenderWidget("topic_body_before"); } - @if (Model.Body.HasValue()) - { -
- @Html.Raw(Model.Body) -
- } + @if (Model.Body.HasValue()) + { +
+ @Html.Raw(Model.Body) +
+ } - @{ Html.RenderWidget("topic_body_after"); } -
+ @{ Html.RenderWidget("topic_body_after"); } +
+} From 78cb78270e01dafd5a1aaff7c5f9c5f7b546597f Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 13 Dec 2018 09:22:39 +0100 Subject: [PATCH 063/657] Amazon Pay: Fixes missing scope parameter in OffAmazonPayments.Widgets.Wallet --- .../Views/AmazonPayCheckout/PaymentMethod.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayCheckout/PaymentMethod.cshtml b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayCheckout/PaymentMethod.cshtml index a736fed4e8..233d0251c7 100644 --- a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayCheckout/PaymentMethod.cshtml +++ b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayCheckout/PaymentMethod.cshtml @@ -57,7 +57,7 @@ try { new OffAmazonPayments.Widgets.Wallet({ sellerId: '@Model.SellerId', - scope: '', + scope: 'profile payments:widget payments:shipping_address payments:billing_address', amazonOrderReferenceId: '@Model.OrderReferenceId', design: { designMode: 'responsive' From 9fb73b334c6762e8f7278cac9349edf3e428b3dd Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 13 Dec 2018 18:44:46 +0100 Subject: [PATCH 064/657] Theming: enhanced #scroll-top button --- .../SmartStore.Web/Themes/Flex/Content/_layout.scss | 11 +++++++---- .../Views/Shared/Layouts/_Layout.cshtml | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Themes/Flex/Content/_layout.scss b/src/Presentation/SmartStore.Web/Themes/Flex/Content/_layout.scss index 22107df483..cf7cd56db7 100644 --- a/src/Presentation/SmartStore.Web/Themes/Flex/Content/_layout.scss +++ b/src/Presentation/SmartStore.Web/Themes/Flex/Content/_layout.scss @@ -109,15 +109,18 @@ body { right: 0; width: 36px; height: 36px; - border-radius: 3px 0 0 3px; - border: 1px solid rgba(0,0,0, 0.15); + border-radius: $border-radius 0 0 $border-radius; + border: 1px solid rgba(#000, 0.1); border-right-width: 0; - background: rgba(255,255,255, 0.9); + background: rgba(#fff, 0.9); color: $body-color !important; text-decoration: none; transform: translate3d(100%, 0, 0); - transition: transform .2s ease-in-out; + transition: all .5s ease; + opacity: 0; + &.in { transform: translate3d(0, 0, 0) !important; + opacity: 1; } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml index 6670f8fa52..17e37593b0 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml @@ -132,7 +132,7 @@
- + From 5bad2a1a21a4f49ac78983f90aafd57c076d6b8b Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 13 Dec 2018 18:45:52 +0100 Subject: [PATCH 065/657] Theming: granularity > separated assets registration from _Document layout --- .../SmartStore.Web/SmartStore.Web.csproj | 1 + .../Views/Shared/Layouts/_Document.cshtml | 109 ++++-------------- .../Views/Shared/Partials/_Assets.cshtml | 69 +++++++++++ 3 files changed, 90 insertions(+), 89 deletions(-) create mode 100644 src/Presentation/SmartStore.Web/Views/Shared/Partials/_Assets.cshtml diff --git a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj index e012358e35..9e347df693 100644 --- a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj +++ b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj @@ -1780,6 +1780,7 @@ PreserveNewest + Web.config diff --git a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml index 4a16d4be4d..d65c873084 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml @@ -1,88 +1,19 @@ @using SmartStore.Core; -@{ - var jsRoot = "~/Scripts/"; - var vendorsRoot = "~/Content/vendors/"; - var contentRoot = "~/Content/"; - - // add css assets - Html.AddCssFileParts( - contentRoot + "fontastic/fontastic.css", - vendorsRoot + "font-awesome/font-awesome.css", - vendorsRoot + "pnotify/css/pnotify.css", - vendorsRoot + "pnotify/css/pnotify.mobile.css", - vendorsRoot + "pnotify/css/pnotify.buttons.css"); - - Html.AppendScriptParts(ResourceLocation.Head, - vendorsRoot + "modernizr/modernizr.js", - vendorsRoot + "jquery/jquery-3.2.1.js"/*, - vendorsRoot + "jquery/jquery-migrate-3.0.0.js"*/); - - Html.AppendScriptParts(ResourceLocation.Foot, - // Vendors - vendorsRoot + "underscore/underscore.js", - vendorsRoot + "underscore/underscore.string.js", - vendorsRoot + "jquery/jquery.addeasing.js", - vendorsRoot + "jquery-ui/effect.js", - vendorsRoot + "jquery-ui/effect-shake.js", - vendorsRoot + "jquery/jquery.unobtrusive-ajax.js", - vendorsRoot + "jquery/jquery.validate.js", - vendorsRoot + "jquery/jquery.validate.unobtrusive.js", - vendorsRoot + "jquery/jquery.ba-outside-events.js", - vendorsRoot + "jquery/jquery.scrollTo.js", - vendorsRoot + "moment/moment.js", - vendorsRoot + "datetimepicker/js/tempusdominus-bootstrap-4.js", - vendorsRoot + "select2/js/select2.js", - vendorsRoot + "pnotify/js/pnotify.js", - vendorsRoot + "pnotify/js/pnotify.mobile.js", - vendorsRoot + "pnotify/js/pnotify.buttons.js", - vendorsRoot + "pnotify/js/pnotify.animate.js", - vendorsRoot + "slick/slick.js", - vendorsRoot + "touchspin/jquery.bootstrap-touchspin.js", - vendorsRoot + "aos/js/aos.js", - contentRoot + "bs4/js/bootstrap.bundle.js", - // Common - jsRoot + "underscore.mixins.js", - jsRoot + "smartstore.system.js", - jsRoot + "smartstore.touchevents.js", - jsRoot + "smartstore.jquery.utils.js", - jsRoot + "smartstore.globalization.js", - jsRoot + "jquery.validate.unobtrusive.custom.js", - jsRoot + "smartstore.viewport.js", - jsRoot + "smartstore.doajax.js", - jsRoot + "smartstore.eventbroker.js", - jsRoot + "smartstore.hacks.js", - jsRoot + "smartstore.common.js", - jsRoot + "smartstore.selectwrapper.js", - jsRoot + "smartstore.throbber.js", - jsRoot + "smartstore.thumbzoomer.js", - jsRoot + "smartstore.responsiveNav.js", - jsRoot + "smartstore.keynav.js", - jsRoot + "smartstore.articlelist.js", - jsRoot + "smartstore.megamenu.js", - jsRoot + "smartstore.offcanvas.js", - jsRoot + "smartstore.parallax.js", - // Shop - jsRoot + "public.common.js", - jsRoot + "public.search.js", - jsRoot + "public.offcanvas-cart.js", - jsRoot + "public.offcanvas-menu.js", - jsRoot + "public.product.js"); - - //Html.AddBodyCssClass(this.WorkContext.WorkingLanguage.Rtl ? "rtl" : ""); -} - @Html.SmartTitle(true) + @Html.SmartTitle(true) - - - + + + - @*This is used so that themes can inject content into the header*@ + @{ Html.RenderPartial("_Assets"); } + + @*This is used so that themes can inject content into the header*@ @{ Html.RenderPartial("Head"); } @Html.SmartMetaRobots() @@ -90,34 +21,34 @@ @{ Html.RenderPartial("_ClientRes"); } @{ Html.RenderPartial("_GoogleFonts"); } - @Html.SmartCssFiles(this.Url, ResourceLocation.Head) - @Html.SmartScripts(this.Url, ResourceLocation.Head) + @Html.SmartCssFiles(this.Url, ResourceLocation.Head) + @Html.SmartScripts(this.Url, ResourceLocation.Head) - @{ Html.RenderWidget("head_html_tag"); } + @{ Html.RenderWidget("head_html_tag"); } - @Html.CanonicalUrls() + @Html.CanonicalUrls() @Html.LinkRels() - @{ + @{ Html.RenderAction("RssHeaderLink", "News", new { area = "" }); Html.RenderAction("RssHeaderLink", "Blog", new { area = "" }); } - @*Favicon - upload favicon.ico or favicon-[StoreId].ico file either to the root web or your theme directory*@ + @*Favicon - upload favicon.ico or favicon-[StoreId].ico file either to the root web or your theme directory*@ @{ Html.RenderAction("Favicon", "Common", new { area = "" }); } - + @Html.CustomHead() @{ Html.RenderZone("head"); } - + - + @{ Html.RenderZone("start"); } - @RenderBody() + @RenderBody() @{ Html.RenderZone("aftercontent"); } - - @Html.SmartCssFiles(this.Url, ResourceLocation.Foot) - @Html.SmartScripts(this.Url, ResourceLocation.Foot) + + @Html.SmartCssFiles(this.Url, ResourceLocation.Foot) + @Html.SmartScripts(this.Url, ResourceLocation.Foot) @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, vendorsRoot + "select2/js/i18n", "*.js", null) @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, vendorsRoot + "moment/locale", "*.js", null) diff --git a/src/Presentation/SmartStore.Web/Views/Shared/Partials/_Assets.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/Partials/_Assets.cshtml new file mode 100644 index 0000000000..e48f35e1c4 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Shared/Partials/_Assets.cshtml @@ -0,0 +1,69 @@ +@{ + var jsRoot = "~/Scripts/"; + var vendorsRoot = "~/Content/vendors/"; + var contentRoot = "~/Content/"; + + // add css assets + Html.AddCssFileParts( + contentRoot + "fontastic/fontastic.css", + vendorsRoot + "font-awesome/font-awesome.css", + vendorsRoot + "pnotify/css/pnotify.css", + vendorsRoot + "pnotify/css/pnotify.mobile.css", + vendorsRoot + "pnotify/css/pnotify.buttons.css"); + + Html.AppendScriptParts(ResourceLocation.Head, + vendorsRoot + "modernizr/modernizr.js", + vendorsRoot + "jquery/jquery-3.2.1.js"/*, + vendorsRoot + "jquery/jquery-migrate-3.0.0.js"*/); + + Html.AppendScriptParts(ResourceLocation.Foot, + // Vendors + vendorsRoot + "underscore/underscore.js", + vendorsRoot + "underscore/underscore.string.js", + vendorsRoot + "jquery/jquery.addeasing.js", + vendorsRoot + "jquery-ui/effect.js", + vendorsRoot + "jquery-ui/effect-shake.js", + vendorsRoot + "jquery/jquery.unobtrusive-ajax.js", + vendorsRoot + "jquery/jquery.validate.js", + vendorsRoot + "jquery/jquery.validate.unobtrusive.js", + vendorsRoot + "jquery/jquery.ba-outside-events.js", + vendorsRoot + "jquery/jquery.scrollTo.js", + vendorsRoot + "moment/moment.js", + vendorsRoot + "datetimepicker/js/tempusdominus-bootstrap-4.js", + vendorsRoot + "select2/js/select2.js", + vendorsRoot + "pnotify/js/pnotify.js", + vendorsRoot + "pnotify/js/pnotify.mobile.js", + vendorsRoot + "pnotify/js/pnotify.buttons.js", + vendorsRoot + "pnotify/js/pnotify.animate.js", + vendorsRoot + "slick/slick.js", + vendorsRoot + "touchspin/jquery.bootstrap-touchspin.js", + vendorsRoot + "aos/js/aos.js", + contentRoot + "bs4/js/bootstrap.bundle.js", + // Common + jsRoot + "underscore.mixins.js", + jsRoot + "smartstore.system.js", + jsRoot + "smartstore.touchevents.js", + jsRoot + "smartstore.jquery.utils.js", + jsRoot + "smartstore.globalization.js", + jsRoot + "jquery.validate.unobtrusive.custom.js", + jsRoot + "smartstore.viewport.js", + jsRoot + "smartstore.doajax.js", + jsRoot + "smartstore.eventbroker.js", + jsRoot + "smartstore.hacks.js", + jsRoot + "smartstore.common.js", + jsRoot + "smartstore.selectwrapper.js", + jsRoot + "smartstore.throbber.js", + jsRoot + "smartstore.thumbzoomer.js", + jsRoot + "smartstore.responsiveNav.js", + jsRoot + "smartstore.keynav.js", + jsRoot + "smartstore.articlelist.js", + jsRoot + "smartstore.megamenu.js", + jsRoot + "smartstore.offcanvas.js", + jsRoot + "smartstore.parallax.js", + // Shop + jsRoot + "public.common.js", + jsRoot + "public.search.js", + jsRoot + "public.offcanvas-cart.js", + jsRoot + "public.offcanvas-menu.js", + jsRoot + "public.product.js"); +} \ No newline at end of file From b9daa1fafc0bdd7a04b84718ed9f97c4f5e1d9f5 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 13 Dec 2018 23:21:14 +0100 Subject: [PATCH 066/657] Theming: FontAwesome icons should be 14px fixed, as they are based on a 14px grid. --- .../Content/vendors/font-awesome/font-awesome.css | 2 +- .../Content/vendors/font-awesome/font-awesome.min.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.css b/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.css index eab1cbb5b7..3afa96bacf 100644 --- a/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.css +++ b/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.css @@ -14,7 +14,7 @@ .fa { display: inline-block; font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; + font-size: 14px; /* MC: changed from 'inherit' to '14px' */ text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; diff --git a/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.min.css b/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.min.css index f7b70903ee..a9a5f6d60e 100644 --- a/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.min.css +++ b/src/Presentation/SmartStore.Web/Content/vendors/font-awesome/font-awesome.min.css @@ -1,4 +1,4 @@ /*! * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('fontawesome-webfont.eot?v=4.7.0');src:url('fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('fontawesome-webfont.woff?v=4.7.0') format('woff'),url('fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} + */@font-face{font-family:'FontAwesome';src:url('fontawesome-webfont.eot?v=4.7.0');src:url('fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('fontawesome-webfont.woff?v=4.7.0') format('woff'),url('fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:14px;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} From b4ab5934acd57c66422763c6edd93fcb871b1943 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 13 Dec 2018 23:22:48 +0100 Subject: [PATCH 067/657] Theming: enhanced modal dialog fade in/out animations --- .../Administration/Content/_variables.scss | 6 +++ .../SmartStore.Web/Content/shared/_modal.scss | 39 +++++++++++++++---- .../Scripts/smartstore.common.js | 22 +++++++---- .../Views/Shared/Layouts/_Document.cshtml | 4 +- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss b/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss index 7e0de9e97f..321501a1e6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss @@ -175,6 +175,12 @@ $component-active-bg: lighten($primary, 36%); $component-active-color: inherit; +$modal-content-border-width: 0; +$modal-backdrop-opacity: .55; +$modal-content-box-shadow-xs: 0 12px 26px rgba(#32325d, .12), 0 3px 10px rgba(#000, .08); +$modal-content-box-shadow-sm-up: 0 50px 100px rgba(#32325d, .12), 0 15px 35px rgba(#32325d, .27), 0 5px 15px rgba(#000, .25); + + /*$custom-control-indicator-bg: $gray-200; $custom-control-indicator-disabled-bg: $gray-100; $custom-control-indicator-checked-color: #fff; diff --git a/src/Presentation/SmartStore.Web/Content/shared/_modal.scss b/src/Presentation/SmartStore.Web/Content/shared/_modal.scss index 76f57c64e1..dc62ad85a1 100644 --- a/src/Presentation/SmartStore.Web/Content/shared/_modal.scss +++ b/src/Presentation/SmartStore.Web/Content/shared/_modal.scss @@ -2,21 +2,46 @@ /// /// +//$modal-transition-timing: cubic-bezier(0.25, 0.46, 0.45, 0.94); // easeOutQuad +$modal-transition-timing: cubic-bezier(0.165, 0.84, 0.44, 1); // easeOutQuart +//$modal-transition-timing: cubic-bezier(0.19, 1, 0.22, 1); // easeOutExpo + // FLEX MODAL // ------------------------------------ -.modal-backdrop.fade, + +.modal-backdrop.fade { + // fade out + transition: opacity 0.4s $modal-transition-timing; + // fade in + &.show { + transition: opacity 0.15s ease-out; + } +} + .modal.fade { - transition: opacity 0.2s linear, transform 0.2s ease-out; + .modal-dialog { + transition: opacity 0.4s $modal-transition-timing, transform 0.4s $modal-transition-timing; + transform: scale(1.1, 1.1); + will-change: transform, opacity; + } + + &.show .modal-dialog { + transform: scale(1, 1); + opacity: 1; + } } -.modal.fade .modal-dialog { - transition: opacity 0.2s linear, transform 0.2s ease-out; - transform: translate(0, -75px) scale(0.9, 0.9); +.modal-open #page { + // fade in + filter: blur(2px); + transition: filter 0.4s $modal-transition-timing 0.15s; } -.modal.show .modal-dialog { - transform: translate(0, 0); +.modal-hiding #page { + // fade out + filter: blur(0); + transition: filter 0.4s $modal-transition-timing; } .modal-content { diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js index 46a38183a6..d64551d8ab 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js @@ -52,7 +52,7 @@ '' ].join(""); - modal = $(html).appendTo('body').on('hidden.bs.modal', function (e) { + modal = $(html).appendTo('body').on('hidden.bs.modal', function (e) { modal.remove(); }); @@ -311,7 +311,9 @@ // on document ready $(function () { - var rtl = SmartStore.globalization.culture.isRTL; + var rtl = SmartStore.globalization.culture.isRTL, + win = $(window), + body = $(document.body); function getFunction(code, argNames) { var fn = window, parts = (code || "").split("."); @@ -515,7 +517,7 @@ elLabel.text(sel); }); - $('body').on('mouseenter mouseleave mousedown change', '.mf-dropdown > select', function (e) { + body.on('mouseenter mouseleave mousedown change', '.mf-dropdown > select', function (e) { var btn = $(this).parent().find('> .btn'); if (e.type == "mouseenter") { btn.addClass('hover'); @@ -523,7 +525,7 @@ else if (e.type == "mousedown") { btn.addClass('active focus').removeClass('hover'); _.delay(function () { - $('body').one('mousedown touch', function (e) { btn.removeClass('active focus'); }); + body.one('mousedown touch', function (e) { btn.removeClass('active focus'); }); }, 50); } else if (e.type == "mouseleave") { @@ -632,14 +634,14 @@ (function () { $('#scroll-top').on('click', function (e) { e.preventDefault(); - $(window).scrollTo(0, 600); + win.scrollTo(0, 600); return false; }); var prevY; var throttledScroll = _.throttle(function (e) { - var y = $(window).scrollTop(); + var y = win.scrollTop(); if (_.isNumber(prevY)) { // Show scroll button only when scrolled up if (y < prevY && y > 500) { @@ -653,8 +655,12 @@ prevY = y; }, 100); - $(window).on("scroll", throttledScroll); - })(); + win.on("scroll", throttledScroll); + })(); + + // Modal stuff + $(document).on('hide.bs.modal', '.modal', function (e) { body.addClass('modal-hiding'); }) + $(document).on('hidden.bs.modal', '.modal', function (e) { body.removeClass('modal-hiding'); }) }); })( jQuery, this, document ); diff --git a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml index d65c873084..af9251b06f 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Document.cshtml @@ -49,8 +49,8 @@ @Html.SmartCssFiles(this.Url, ResourceLocation.Foot) @Html.SmartScripts(this.Url, ResourceLocation.Foot) - @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, vendorsRoot + "select2/js/i18n", "*.js", null) - @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, vendorsRoot + "moment/locale", "*.js", null) + @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, "~/Content/vendors/select2/js/i18n", "*.js", null) + @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, "~/Content/vendors/moment/locale", "*.js", null) @{ Html.RenderZone("end"); } From bd6534ac7d5127c7612739fa31baf63ab24b2755 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Fri, 14 Dec 2018 02:17:45 +0100 Subject: [PATCH 068/657] Theming: minor fix --- .../SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml index 17e37593b0..f0d14c3065 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/Layouts/_Layout.cshtml @@ -131,7 +131,7 @@ @{ Html.RenderZone("page-end"); } - + From dad0336921ef26b16244dd3e85e3492a4cd1633e Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Sat, 15 Dec 2018 03:24:58 +0100 Subject: [PATCH 069/657] Theming: updated to FontAwesome 5 Free (Pro usage also possible with switch 'fa-use-pro', but only with a valid personal license) --- .../DataExchange/Export/ExportExtensions.cs | 12 +- .../Views/AmazonPay/Configure.cshtml | 4 +- .../SmartStore.Clickatell/AdminMenu.cs | 2 +- src/Plugins/SmartStore.DevTools/AdminMenu.cs | 4 +- .../Views/DevTools/BackendExtension.cshtml | 2 +- .../ExternalAuthFacebook/PublicInfo.cshtml | 2 +- .../AdminMenu.cs | 2 +- .../SmartStore.GoogleMerchantCenter/Events.cs | 2 +- .../Views/PayPalPlus/Configure.cshtml | 6 +- .../Extensions/HtmlExtensions.cs | 28 +- .../Administration/Content/_variables.scss | 15 +- .../Content/filemanager/js/file.js | 4 +- .../Content/filemanager/js/utils.js | 30 +- .../Administration/Content/theme.scss | 15 +- .../Models/Orders/OrderModel.cs | 4 +- .../Views/ActivityLog/ListLogs.cshtml | 4 +- .../Views/Affiliate/Edit.cshtml | 2 +- .../Administration/Views/Blog/Edit.cshtml | 2 +- .../Administration/Views/Blog/List.cshtml | 2 +- .../Administration/Views/Campaign/Edit.cshtml | 4 +- .../Administration/Views/Category/Edit.cshtml | 4 +- .../Views/Category/_CreateOrUpdate.cshtml | 6 +- .../Views/CheckoutAttribute/Edit.cshtml | 2 +- .../Views/Common/CheckUpdate.cshtml | 4 +- .../Views/Common/Maintenance.cshtml | 6 +- .../Administration/Views/Country/Edit.cshtml | 2 +- .../Administration/Views/Currency/Edit.cshtml | 2 +- .../Administration/Views/Currency/List.cshtml | 4 +- .../Views/Currency/_CreateOrUpdate.cshtml | 4 +- .../Administration/Views/Customer/Edit.cshtml | 2 +- .../Views/Customer/Reports.cshtml | 2 +- .../Views/Customer/_CreateOrUpdate.cshtml | 4 +- .../Views/CustomerRole/Edit.cshtml | 2 +- .../Views/DeliveryTime/Edit.cshtml | 2 +- .../Views/DeliveryTime/List.cshtml | 2 +- .../Administration/Views/Discount/Edit.cshtml | 2 +- .../Views/Discount/_CreateOrUpdate.cshtml | 2 +- .../Views/EmailAccount/Edit.cshtml | 2 +- .../Administration/Views/Export/Edit.cshtml | 8 +- .../Views/Export/EditDeployment.cshtml | 2 +- .../Views/Export/Preview.cshtml | 6 +- .../Views/Export/ProfileFileDetails.cshtml | 2 +- .../Export/_CreateOrUpdate.Deployment.cshtml | 2 +- .../Views/Export/_ProfileList.cshtml | 2 +- .../Views/Export/_Tab.Deployment.cshtml | 4 +- .../Views/Forum/EditForum.cshtml | 2 +- .../Views/Forum/EditForumGroup.cshtml | 2 +- .../Administration/Views/GiftCard/Edit.cshtml | 2 +- .../Views/GiftCard/_CreateOrUpdate.cshtml | 2 +- .../Administration/Views/Home/Index.cshtml | 2 +- .../Administration/Views/Home/UaTester.cshtml | 2 +- .../Administration/Views/Import/Edit.cshtml | 6 +- .../Administration/Views/Import/List.cshtml | 2 +- .../Views/Import/_ColumnMappings.cshtml | 6 +- .../Views/Import/_Update.cshtml | 8 +- .../Administration/Views/Language/Edit.cshtml | 2 +- .../Partials/AvailableLanguages.cshtml | 2 +- .../Administration/Views/Log/List.cshtml | 4 +- .../Administration/Views/Log/View.cshtml | 2 +- .../Views/Manufacturer/Edit.cshtml | 4 +- .../Views/Manufacturer/List.cshtml | 2 +- .../Views/MessageTemplate/Edit.cshtml | 8 +- .../Views/MessageTemplate/List.cshtml | 2 +- .../Administration/Views/News/Edit.cshtml | 2 +- .../Administration/Views/News/List.cshtml | 2 +- .../Views/NewsLetterSubscription/List.cshtml | 2 +- .../Views/OnlineCustomer/List.cshtml | 2 +- .../Administration/Views/Order/Edit.cshtml | 4 +- .../Administration/Views/Order/List.cshtml | 6 +- .../Views/Order/ShipmentDetails.cshtml | 4 +- .../Order/_Edit.BillingAndShipment.cshtml | 6 +- .../Views/Order/_Edit.Info.cshtml | 6 +- .../Views/Payment/Providers.cshtml | 2 +- .../Views/Plugin/ConfigurePlugin.cshtml | 2 +- .../Views/Plugin/ConfigureProvider.cshtml | 2 +- .../Administration/Views/Plugin/List.cshtml | 8 +- .../Administration/Views/Poll/Edit.cshtml | 2 +- .../Administration/Views/Product/Edit.cshtml | 8 +- .../Administration/Views/Product/List.cshtml | 2 +- .../Views/Product/LowStockReport.cshtml | 2 +- .../Product/_CreateOrUpdate.Attributes.cshtml | 4 +- .../Product/_CreateOrUpdate.Downloads.cshtml | 2 +- .../Views/Product/_CreateOrUpdate.cshtml | 10 +- .../Views/ProductAttribute/Edit.cshtml | 2 +- .../Views/ProductReview/Edit.cshtml | 2 +- .../Views/ProductReview/List.cshtml | 2 +- .../Views/QuantityUnit/Edit.cshtml | 2 +- .../Views/QueuedEmail/Edit.cshtml | 4 +- .../Views/QueuedEmail/List.cshtml | 6 +- .../Views/RecurringPayment/Edit.cshtml | 2 +- .../Views/RecurringPayment/List.cshtml | 2 +- .../Views/ReturnRequest/Edit.cshtml | 2 +- .../Views/RoxyFileManager/Index.cshtml | 24 +- .../Views/ScheduleTask/Edit.cshtml | 2 +- .../Views/ScheduleTask/List.cshtml | 2 +- .../ScheduleTask/_MinimalTaskWidget.cshtml | 4 +- .../Administration/Views/Setting/Blog.cshtml | 2 +- .../Views/Setting/DataExchange.cshtml | 2 +- .../Views/Setting/GeneralCommon.cshtml | 2 +- .../Administration/Views/Setting/Media.cshtml | 4 +- .../Administration/Views/Setting/Order.cshtml | 2 +- .../Views/Setting/Payment.cshtml | 2 +- .../Shared/EditorTemplates/Download.cshtml | 4 +- .../Views/Shared/Layouts/_AdminRoot.cshtml | 4 +- .../Views/Shared/Partials/Menu.cshtml | 7 +- .../Views/Shared/Partials/Navbar.cshtml | 14 +- .../Views/Shared/Partials/_Providers.cshtml | 4 +- .../Views/Shipping/EditMethod.cshtml | 2 +- .../ShoppingCart/CurrentWishlists.cshtml | 2 +- .../Views/SpecificationAttribute/Edit.cshtml | 2 +- .../Views/SpecificationAttribute/List.cshtml | 2 +- .../Administration/Views/Store/Edit.cshtml | 2 +- .../Views/Theme/Configure.cshtml | 6 +- .../Administration/Views/Theme/List.cshtml | 8 +- .../Views/Theme/PreviewTool.cshtml | 4 +- .../Administration/Views/Topic/Edit.cshtml | 2 +- .../Administration/Views/Topic/List.cshtml | 2 +- .../Views/UrlRecord/Edit.cshtml | 4 +- .../Views/UrlRecord/List.cshtml | 2 +- .../Administration/sitemap.config | 133 +- .../Content/editors/summernote/globalinit.js | 12 +- .../summernote/plugins/smartstore.cssclass.js | 4 +- .../summernote/plugins/smartstore.image.js | 4 +- .../SmartStore.Web/Content/shared/_alert.scss | 2 +- .../Content/shared/_choice.scss | 3 +- .../Content/shared/_dropdown.scss | 10 +- .../SmartStore.Web/Content/shared/_fa.scss | 57 + .../Content/shared/_mixins.scss | 37 +- .../SmartStore.Web/Content/shared/_nav.scss | 6 +- .../Content/shared/_star-rating.scss | 6 +- .../Content/shared/_variables-shared.scss | 178 + .../Content/skinning/_photoswipe.scss | 2 +- .../Content/vendors/fa5/css/all.css | 4223 +++++++++++++++++ .../Content/vendors/fa5/css/all.min.css | 5 + .../Content/vendors/fa5/css/brands.css | 13 + .../Content/vendors/fa5/css/brands.min.css | 5 + .../Content/vendors/fa5/css/fontawesome.css | 4193 ++++++++++++++++ .../vendors/fa5/css/fontawesome.min.css | 5 + .../Content/vendors/fa5/css/regular.css | 14 + .../Content/vendors/fa5/css/regular.min.css | 5 + .../Content/vendors/fa5/css/solid.css | 15 + .../Content/vendors/fa5/css/solid.min.css | 5 + .../Content/vendors/fa5/css/svg-with-js.css | 345 ++ .../vendors/fa5/css/svg-with-js.min.css | 5 + .../Content/vendors/fa5/css/v4-shims.css | 2166 +++++++++ .../Content/vendors/fa5/css/v4-shims.min.css | 5 + .../Content/vendors/fa5/scss/_animated.scss | 20 + .../vendors/fa5/scss/_bordered-pulled.scss | 20 + .../Content/vendors/fa5/scss/_core.scss | 20 + .../vendors/fa5/scss/_fixed-width.scss | 6 + .../Content/vendors/fa5/scss/_icons.scss | 1332 ++++++ .../Content/vendors/fa5/scss/_larger.scss | 23 + .../Content/vendors/fa5/scss/_list.scss | 18 + .../Content/vendors/fa5/scss/_mixins.scss | 57 + .../vendors/fa5/scss/_rotated-flipped.scss | 23 + .../vendors/fa5/scss/_screen-reader.scss | 5 + .../Content/vendors/fa5/scss/_shims.scss | 2062 ++++++++ .../Content/vendors/fa5/scss/_stacked.scss | 31 + .../Content/vendors/fa5/scss/_variables.scss | 1346 ++++++ .../Content/vendors/fa5/scss/brands.scss | 21 + .../Content/vendors/fa5/scss/fontawesome.scss | 16 + .../Content/vendors/fa5/scss/regular.scss | 22 + .../Content/vendors/fa5/scss/solid.scss | 23 + .../Content/vendors/fa5/scss/v4-shims.scss | 6 + .../vendors/fa5/webfonts/fa-brands-400.eot | Bin 0 -> 134000 bytes .../vendors/fa5/webfonts/fa-brands-400.svg | 1260 +++++ .../vendors/fa5/webfonts/fa-brands-400.ttf | Bin 0 -> 133764 bytes .../vendors/fa5/webfonts/fa-brands-400.woff | Bin 0 -> 86736 bytes .../vendors/fa5/webfonts/fa-brands-400.woff2 | Bin 0 -> 73936 bytes .../vendors/fa5/webfonts/fa-regular-400.eot | Bin 0 -> 40308 bytes .../vendors/fa5/webfonts/fa-regular-400.svg | 471 ++ .../vendors/fa5/webfonts/fa-regular-400.ttf | Bin 0 -> 40080 bytes .../vendors/fa5/webfonts/fa-regular-400.woff | Bin 0 -> 18164 bytes .../vendors/fa5/webfonts/fa-regular-400.woff2 | Bin 0 -> 14868 bytes .../vendors/fa5/webfonts/fa-solid-900.eot | Bin 0 -> 208828 bytes .../vendors/fa5/webfonts/fa-solid-900.svg | 2760 +++++++++++ .../vendors/fa5/webfonts/fa-solid-900.ttf | Bin 0 -> 208608 bytes .../vendors/fa5/webfonts/fa-solid-900.woff | Bin 0 -> 102120 bytes .../vendors/fa5/webfonts/fa-solid-900.woff2 | Bin 0 -> 79072 bytes .../Controllers/CustomerController.cs | 22 +- src/Presentation/SmartStore.Web/Global.asax | 2 +- .../Scripts/smartstore.common.js | 4 +- .../SmartStore.Web/SmartStore.Web.csproj | 48 + .../Themes/Flex/Content/_megamenu.scss | 6 +- .../Themes/Flex/Content/_product.scss | 3 +- .../Themes/Flex/Content/_search.scss | 4 +- .../Themes/Flex/Content/_shopbar.scss | 5 +- .../Themes/Flex/Content/_variables.scss | 15 +- .../Themes/Flex/Content/theme.scss | 3 +- .../SmartStore.Web/Themes/Flex/theme.config | 1 + .../Views/Boards/Partials/_ForumPost.cshtml | 2 +- .../SmartStore.Web/Views/Boards/Topic.cshtml | 6 +- .../Views/Catalog/CompareProducts.cshtml | 4 +- .../Catalog/Partials/OffCanvasCompare.cshtml | 4 +- .../Views/Checkout/Completed.cshtml | 2 +- .../Common/Partials/AccountDropdown.cshtml | 12 +- .../Views/Common/Partials/Breadcrumb.cshtml | 2 +- .../Views/Common/Partials/Footer.cshtml | 14 +- .../Views/Common/Partials/Menu.cshtml | 10 +- .../Views/Customer/Orders.cshtml | 6 +- .../Customer/Partials/MyAccountMenu.cshtml | 2 +- .../Views/Entity/Partials/Picker.List.cshtml | 2 +- .../SmartStore.Web/Views/Order/Details.cshtml | 2 +- .../PrivateMessages/Partials/Inbox.cshtml | 2 +- .../PrivateMessages/Partials/SentItems.cshtml | 2 +- .../Views/PrivateMessages/View.cshtml | 2 +- .../Views/Product/Product.cshtml | 2 +- .../Shared/EditorTemplates/DateTime.cshtml | 2 +- .../Views/Shared/EditorTemplates/Html.cshtml | 2 +- .../Shared/EditorTemplates/QtyInput.cshtml | 2 +- .../Views/Shared/EditorTemplates/Time.cshtml | 2 +- .../Partials/Product.List.FilterSort.cshtml | 4 +- .../Partials/Product.List.Item.Buttons.cshtml | 2 +- .../Shared/Partials/Product.List.Pager.cshtml | 4 +- .../Views/Shared/Partials/_Assets.cshtml | 17 +- .../ShoppingCart/Partials/CartItems.cshtml | 2 +- .../Partials/OffCanvasShoppingCart.cshtml | 4 +- .../Partials/OffCanvasWishlist.cshtml | 6 +- .../Views/ShoppingCart/Wishlist.cshtml | 2 +- 219 files changed, 21311 insertions(+), 432 deletions(-) create mode 100644 src/Presentation/SmartStore.Web/Content/shared/_fa.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/all.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/all.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/brands.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/brands.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/fontawesome.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/fontawesome.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/regular.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/regular.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/solid.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/solid.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/svg-with-js.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/svg-with-js.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/v4-shims.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/css/v4-shims.min.css create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_animated.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_bordered-pulled.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_core.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_fixed-width.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_icons.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_larger.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_list.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_mixins.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_rotated-flipped.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_screen-reader.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_shims.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_stacked.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/_variables.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/brands.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/fontawesome.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/regular.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/solid.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/scss/v4-shims.scss create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-brands-400.eot create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-brands-400.svg create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-brands-400.ttf create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-brands-400.woff create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-brands-400.woff2 create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-regular-400.eot create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-regular-400.svg create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-regular-400.ttf create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-regular-400.woff create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-regular-400.woff2 create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-solid-900.eot create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-solid-900.svg create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-solid-900.ttf create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-solid-900.woff create mode 100644 src/Presentation/SmartStore.Web/Content/vendors/fa5/webfonts/fa-solid-900.woff2 diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs index c05c807ac7..83a3fb4f96 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs @@ -250,17 +250,17 @@ public static string GetIconClass(this ExportDeploymentType type) switch (type) { case ExportDeploymentType.FileSystem: - return "fa-folder-open-o"; + return "far fa-folder-open"; case ExportDeploymentType.Email: - return "fa-envelope-o"; + return "far fa-envelope"; case ExportDeploymentType.Http: - return "fa-globe"; + return "fa fa-globe"; case ExportDeploymentType.Ftp: - return "fa-files-o"; + return "far fa-copy"; case ExportDeploymentType.PublicFolder: - return "fa-unlock"; + return "fa fa-unlock"; default: - return "fa-question"; + return "fa fa-question"; } } } diff --git a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml index 213e0c1cf5..1a342b477f 100644 --- a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml +++ b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml @@ -47,11 +47,11 @@ diff --git a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs index 0795b68f34..f336662023 100644 --- a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs +++ b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs @@ -10,7 +10,7 @@ protected override void BuildMenuCore(TreeNode pluginsNode) var menuItem = new MenuItem().ToBuilder() .Text("Clickatell SMS Provider") .ResKey("Plugins.FriendlyName.SmartStore.Clickatell") - .Icon("paper-plane-o") + .Icon("far fa-paper-plane") .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.Clickatell", area = "Admin" }) .ToItem(); diff --git a/src/Plugins/SmartStore.DevTools/AdminMenu.cs b/src/Plugins/SmartStore.DevTools/AdminMenu.cs index 8808b9731c..8110443480 100644 --- a/src/Plugins/SmartStore.DevTools/AdminMenu.cs +++ b/src/Plugins/SmartStore.DevTools/AdminMenu.cs @@ -9,7 +9,7 @@ protected override void BuildMenuCore(TreeNode pluginsNode) { var menuItem = new MenuItem().ToBuilder() .Text("Developer Tools") - .Icon("terminal") + .Icon("far fa-terminal") .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.DevTools", area = "Admin" }) .ToItem(); @@ -18,7 +18,7 @@ protected override void BuildMenuCore(TreeNode pluginsNode) // uncomment to add to admin menu (see plugin sub-menu) //var backendExtensionItem = new MenuItem().ToBuilder() // .Text("Backend extension") - // .Icon("area-chart") + // .Icon("chart-area") // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) // .ToItem(); //pluginsNode.Append(backendExtensionItem); diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml index 26a5c5311a..0af50e879f 100644 --- a/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml @@ -8,7 +8,7 @@
- + My SmartStore.NET backend extension page
diff --git a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml index 167d36243f..2d894ad278 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml +++ b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml @@ -6,6 +6,6 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs index 9c8413dd67..6a62863d9a 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs @@ -9,7 +9,7 @@ protected override void BuildMenuCore(TreeNode pluginsNode) { var menuItem = new MenuItem().ToBuilder() .Text("Google Merchant Center") - .Icon("google") + .Icon("fab fa-google") .ResKey("Plugins.FriendlyName.SmartStore.GoogleMerchantCenter") .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) .ToItem(); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs index 62ec60a9a6..f129d4c009 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs @@ -27,7 +27,7 @@ public void HandleEvent(TabStripCreated eventMessage) eventMessage.ItemFactory.Add().Text("GMC") .Name("tab-gmc") - .Icon("fa fa-google fa-lg fa-fw") + .Icon("fab fa-google fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "GMC" }) .Route("SmartStore.GoogleMerchantCenter", new { action = "ProductEditTab", productId = productId }) .Ajax(); diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml index 9458875371..efce2e5bbc 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml @@ -95,7 +95,7 @@ - + @T(Model.ExperienceProfileId.HasValue() ? "Common.Refresh" : "Common.AddNew") @@ -103,7 +103,7 @@ { - + @T("Admin.Common.Delete") } @@ -125,7 +125,7 @@ { - + @T("Admin.Common.Delete") } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index 4fc585afd1..40e05538a9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -817,7 +817,7 @@ public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string Guard.NotNull(helper, nameof(helper)); Guard.NotEmpty(fileExtension, nameof(fileExtension)); - var icon = "file-o"; + var icon = "far fa-file"; var ext = fileExtension; if (ext != null && ext.StartsWith(".")) @@ -830,7 +830,7 @@ public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string switch (ext.ToLowerInvariant()) { case "pdf": - icon = "file-pdf-o"; + icon = "far fa-file-pdf"; break; case "doc": case "docx": @@ -839,18 +839,18 @@ public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string case "dot": case "dotx": case "dotm": - icon = "file-word-o"; + icon = "far fa-file-word"; break; case "xls": case "xlsx": case "xlsm": case "xlsb": case "ods": - icon = "file-excel-o"; + icon = "far fa-file-excel"; break; case "csv": case "tab": - icon = "table"; + icon = "fa fa-file-csv"; break; case "ppt": case "pptx": @@ -862,25 +862,25 @@ public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string case "potm": case "pps": case "ppsm": - icon = "file-powerpoint-o"; + icon = "far fa-file-powerpoint"; break; case "zip": case "rar": case "7z": - icon = "file-archive-o"; + icon = "far fa-file-archive"; break; case "png": case "jpg": case "jpeg": case "bmp": case "psd": - icon = "file-image-o"; + icon = "far fa-file-image"; break; case "mp3": case "wav": case "ogg": case "wma": - icon = "file-audio-o"; + icon = "far fa-file-audio"; break; case "mp4": case "mkv": @@ -889,25 +889,25 @@ public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string case "asf": case "mpg": case "mpeg": - icon = "file-video-o"; + icon = "far fa-file-video"; break; case "txt": - icon = "file-text-o"; + icon = "far fa-file-alt"; break; case "exe": - icon = "gear"; + icon = "fa fa-cog"; break; case "xml": case "html": case "htm": - icon = "file-code-o"; + icon = "far fa-file-code"; break; } } var label = ext.NaIfEmpty().ToUpper(); - var result = "".FormatInvariant( + var result = "".FormatInvariant( icon, extraCssClasses.HasValue() ? " " + extraCssClasses : "", label); diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss b/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss index 321501a1e6..d14b8cd8b4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss @@ -110,9 +110,18 @@ $h5-font-size: 1.25rem; $h6-font-size: 1rem; $small-font-size: 90%; - $headings-font-weight: 400; + +// Shadows + +$box-shadow-sm: 0 0 .625rem 0 rgba(#000, .05); +$box-shadow: 0 1rem 2.25rem rgba(#32325d, .03), 0 0.3125rem 1rem rgba(#000, .12); +$box-shadow-lg: 0 1rem 3rem rgba(#000, .125); + + +// Tables + $table-cell-padding: .75rem; $table-cell-padding-sm: .3rem; $table-border-color: $gray-300; @@ -150,8 +159,8 @@ $btn-border-radius-lg: $input-border-radius-lg; //.1875rem; $btn-border-radius-sm: $input-border-radius-sm; //.125rem; $btn-disabled-opacity: 0.5; -$dropdown-box-shadow: 0 3px 12px rgba(27,31,35, 0.15); //0 2px 6px rgba(#000, .15); -$dropdown-border-color: rgba(#000, 0.08); +$dropdown-box-shadow: $box-shadow; // 0 3px 12px rgba(27,31,35, 0.15); //0 2px 6px rgba(#000, .15); +$dropdown-border-color: rgba(#000, 0.05); $dropdown-link-color: $body-color; //$gray-800; $dropdown-link-hover-color: darken($dropdown-link-color, 5%); $dropdown-link-hover-bg: lighten($gray-200, 3%); diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js index 9d27380ad6..fa252adf39 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js @@ -101,7 +101,7 @@ function File(filePath, fileSize, modTime, w, h, mime) { ]; var html = [ '
  • ', - '
    ', + '
    ', '' + RoxyUtils.FormatDate(new Date(this.time * 1000)) + '', '' + this.name + '', '' + RoxyUtils.FormatFileSize(this.size) + '', @@ -176,7 +176,7 @@ function File(filePath, fileSize, modTime, w, h, mime) { var icon = RoxyIconHints[fileType]; var li = $('li[data-path="' + item.fullPath + '"]'); li.toggleClass('file-image', fileType == 'image'); - $('.file-icon', li).attr('class', "").addClass('file-icon fa fa-fw fa-' + icon.name).css('color', icon.color); + $('.file-icon', li).attr('class', "").addClass('file-icon fa-fw ' + icon.name).css('color', icon.color); $('.name', li).text(newName); $('li[data-path="' + newPath + '"]').attr('data-path', newPath); ret = true; diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js index d056f763ce..bd7c547e9e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js @@ -107,21 +107,21 @@ RoxyLang = { } var RoxyIconHints = { - "pdf": { name: "file-pdf-o", color: "#F44336" }, - "document": { name: "file-word-o", color: "#2B579A" }, - "spreadsheet": { name: "file-excel-o", color: "#217346" }, - "database": { name: "database", color: "#3ba074" }, - "presentation": { name: "file-powerpoint-o", color: "#D24726" }, - "archive": { name: "file-archive-o", color: "#3F51B5" }, - "audio": { name: "file-audio-o", color: "#009688" }, - "markup": { name: "file-code-o", color: "#4CAF50" }, - "code": { name: "bolt", color: "#4CAF50" }, - "exe": { name: "gear", color: "#58595B" }, - "image": { name: "file-image-o", color: "#e77c00" }, - "text": { name: "file-text-o", color: "#607D8B" }, - "video": { name: "file-video-o", color: "#FF5722" }, - "font": { name: "font", color: "#797985" }, - "misc": { name: "file-o", color: "#ccc" } + "pdf": { name: "far fa-file-pdf", color: "#F44336" }, + "document": { name: "far fa-file-word", color: "#2B579A" }, + "spreadsheet": { name: "far fa-file-excel", color: "#217346" }, + "database": { name: "fa fa-database", color: "#3ba074" }, + "presentation": { name: "far fa-file-powerpoint", color: "#D24726" }, + "archive": { name: "far fa-file-archive", color: "#3F51B5" }, + "audio": { name: "far fa-file-audio", color: "#009688" }, + "markup": { name: "far fa-file-code", color: "#4CAF50" }, + "code": { name: "fa fa-bolt", color: "#4CAF50" }, + "exe": { name: "fa fa-cog", color: "#58595B" }, + "image": { name: "far fa-file-image", color: "#e77c00" }, + "text": { name: "far fa-file-alt", color: "#607D8B" }, + "video": { name: "far fa-file-video", color: "#FF5722" }, + "font": { name: "fa fa-font", color: "#797985" }, + "misc": { name: "far fa-file", color: "#ccc" } } function RoxyUtils() { } diff --git a/src/Presentation/SmartStore.Web/Administration/Content/theme.scss b/src/Presentation/SmartStore.Web/Administration/Content/theme.scss index 923ebaec36..dabb611b58 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/theme.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/theme.scss @@ -41,20 +41,20 @@ } } -.btn-secondary { +/*.btn-secondary { @include button-variant($secondary, $secondary, $secondary, rgba(#000, 0.15), darken($secondary, 10%), rgba(#000, 0.2)); -} +}*/ -.btn-light { +/*.btn-light { @include button-variant($light, $light, $light, rgba(#000, 0.15), darken($light, 10%), rgba(#000, 0.2)); -} +}*/ -.btn-secondary, +/*.btn-secondary, .btn-light { &:focus, &.focus { border-color: rgba(#000, 0.2); } -} +}*/ // Light button icons lighter (looks nice) // ----------------------------------------------------- @@ -68,7 +68,7 @@ &:not(:hover):not(.hover):not(:active):not(.active):not(:focus):not(.focus):not([disabled]):not(.disabled) { > i { //opacity: 0.6; - color: lighten($body-color, 20%); + color: lighten($body-color, 12%); } } } @@ -79,6 +79,7 @@ @import '../../Content/shared/_variables-shared.scss'; @import '../../Content/shared/_mixins.scss'; +@import '../../Content/shared/_fa.scss'; @import '../../Content/shared/_typo.scss'; @import '../../Content/shared/_alert.scss'; @import '../../Content/shared/_buttons.scss'; diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs index fd2e79a904..7d595a21f8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs @@ -183,9 +183,9 @@ public string PaymentStatusLabelClass case Core.Domain.Payments.PaymentStatus.Paid: return "fa fa-fw fa-check text-success"; case Core.Domain.Payments.PaymentStatus.PartiallyRefunded: - return "fa fa-fw fa-exchange text-warning"; + return "fa fa-fw fa-exchange-alt text-warning"; case Core.Domain.Payments.PaymentStatus.Refunded: - return "fa fa-fw fa-exchange text-success"; + return "fa fa-fw fa-exchange-alt text-success"; case Core.Domain.Payments.PaymentStatus.Voided: return "fa fa-fw fa-ban muted"; default: diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml index d66c8247a9..104618e045 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml @@ -13,11 +13,11 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/Edit.cshtml index d3a0e807e8..c6c90d06ee 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/Edit.cshtml @@ -18,7 +18,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Blog/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Blog/Edit.cshtml index a7001577b3..95c5235662 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Blog/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Blog/Edit.cshtml @@ -21,7 +21,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml index eb2bc78b07..1301a3b84e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml @@ -8,7 +8,7 @@ }
    - + @T("Admin.ContentManagement.Blog.BlogPosts")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Campaign/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Campaign/Edit.cshtml index b0b8562938..8a1f55e5f5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Campaign/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Campaign/Edit.cshtml @@ -19,7 +19,7 @@ - + @T("Admin.Common.Preview") } @@ -31,7 +31,7 @@ @T("Admin.Common.SaveContinue")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/Edit.cshtml index 24a9d2773f..16788e029a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/Edit.cshtml @@ -13,7 +13,7 @@ @{ Html.RenderWidget("admin_button_toolbar_before"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml index f40907d2e0..7a0d9bfd6b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml @@ -7,7 +7,7 @@ @Html.SmartStore().TabStrip().Name("category-edit").OnAjaxSuccess("categoryEditTab_onAjaxSuccess").Style(TabsStyle.Material).Position(TabsPosition.Left).Items(x => { - x.Add().Text(T("Admin.Catalog.Categories.Info").Text).Icon("fa fa-pencil fa-lg fa-fw").Content(TabInfo()).Selected(true); + x.Add().Text(T("Admin.Catalog.Categories.Info").Text).Icon("fa fa-pencil-alt fa-lg fa-fw").Content(TabInfo()).Selected(true); x.Add().Text(T("Admin.Common.SEO").Text).Icon("fa fa-search fa-lg fa-fw").Content(TabSeo()); x.Add().Text(T("Admin.Catalog.Categories.Products").Text).Icon("fa fa-cube fa-lg fa-fw").Content(TabProducts()); x.Add().Text(T("Admin.Catalog.Categories.Discounts").Text).Icon("fa fa-percent fa-lg fa-fw").Content(TabDiscounts()); @@ -81,7 +81,7 @@
  • @@ -138,7 +138,7 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/Edit.cshtml index a669015600..0eafa758be 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/Edit.cshtml @@ -20,7 +20,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Common/CheckUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Common/CheckUpdate.cshtml index 6d753c9fc9..55a33211b3 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Common/CheckUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Common/CheckUpdate.cshtml @@ -6,7 +6,7 @@
    - + @ViewBag.Title
    @@ -41,7 +41,7 @@ @Html.Raw(T("Admin.CheckUpdate.AutoUpdatePossibleInfo"))

    - + @T("Admin.CheckUpdate.UpdateNow")

    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Common/Maintenance.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Common/Maintenance.cshtml index 1a0526f6dd..6d9f727847 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Common/Maintenance.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Common/Maintenance.cshtml @@ -42,7 +42,7 @@ @@ -93,7 +93,7 @@ @@ -143,7 +143,7 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Country/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Country/Edit.cshtml index ca742f081f..841d226c66 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Country/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Country/Edit.cshtml @@ -18,7 +18,7 @@ @T("Admin.Common.SaveContinue") - + @T("Admin.Common.Delete") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Currency/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Currency/Edit.cshtml index b3221627a1..e09d8871f5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Currency/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Currency/Edit.cshtml @@ -18,7 +18,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Currency/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Currency/List.cshtml index 2bad1d1227..7ad1e8c55d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Currency/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Currency/List.cshtml @@ -9,7 +9,7 @@
    - + @T("Admin.Configuration.Currencies")
    @@ -22,7 +22,7 @@ @T("Admin.Common.Save") - + @T("Admin.Configuration.Currencies.GetLiveRates")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Currency/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Currency/_CreateOrUpdate.cshtml index 3be6d4f87a..045995d078 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Currency/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Currency/_CreateOrUpdate.cshtml @@ -20,7 +20,7 @@ @Html.SmartStore().TabStrip().Name("currency-edit").Style(TabsStyle.Material).Position(TabsPosition.Top).Items(x => { x.Add().Text(T("Admin.Common.Info").Text) - //.Icon("fa fa-pencil fa-lg fa-fw") + //.Icon("fa fa-pencil-alt fa-lg fa-fw") .Content(TabInfo()) .Selected(true); @@ -28,7 +28,7 @@ //.Icon("fa fa-globe fa-lg fa-fw") .Content(TabStores()); - //generate an event + //generate an event EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "currency-edit", this.Html, this.Model)); }) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/Edit.cshtml index 7e1f08386e..30f350f1b4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/Edit.cshtml @@ -40,7 +40,7 @@ @if (!Model.Deleted) { } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/Reports.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/Reports.cshtml index c6243d97b1..9bee9eea4f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/Reports.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/Reports.cshtml @@ -5,7 +5,7 @@ }
    - + @T("Admin.Customers.Reports")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml index 4533ced3fc..0eafdd7c64 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml @@ -97,7 +97,7 @@ { @@ -719,7 +719,7 @@

    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/CustomerRole/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/CustomerRole/Edit.cshtml index a9d5fe0350..77861faa53 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/CustomerRole/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/CustomerRole/Edit.cshtml @@ -18,7 +18,7 @@ @T("Admin.Common.SaveContinue")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/Edit.cshtml index 466a0c5d8b..79ff89dac5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/List.cshtml index fcc0bdc0c4..796ded1627 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/DeliveryTime/List.cshtml @@ -3,7 +3,7 @@ }
    - + @T("Admin.Configuration.DeliveryTimes")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Discount/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Discount/Edit.cshtml index 423fe32eb4..9bd378f0fb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Discount/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Discount/Edit.cshtml @@ -19,7 +19,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Discount/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Discount/_CreateOrUpdate.cshtml index 7aff619be0..d4f58841c8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Discount/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Discount/_CreateOrUpdate.cshtml @@ -409,7 +409,7 @@ name="deleterequirement{{= discountRequirementId }}" id="deleterequirement{{= discountRequirementId }}" onclick="deleteRequirement({{= discountRequirementId }})"> - +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/Edit.cshtml index 37f27e47ac..215f4acbd4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml index d7780bc634..928cefbf41 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml @@ -22,13 +22,13 @@ if (Model.Id != 0) { } - + @T("Admin.Common.Preview") } @@ -36,7 +36,7 @@ @if (Model.LogFileExists) { - + @T("Admin.Configuration.ActivityLog") } @@ -53,7 +53,7 @@ @if (!Model.IsSystemProfile) { } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml index 08c85de8d2..a63c46efc1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml @@ -22,7 +22,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml index b2866cfa4b..c0a53dc65a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Preview.cshtml @@ -19,16 +19,16 @@ @if (Model.LogFileExists) { - + @T("Admin.Configuration.ActivityLog") } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/ProfileFileDetails.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/ProfileFileDetails.cshtml index e50f06f489..58b01c5355 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/ProfileFileDetails.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/ProfileFileDetails.cshtml @@ -49,7 +49,7 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/_CreateOrUpdate.Deployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_CreateOrUpdate.Deployment.cshtml index 8c60028e15..e312ffa95b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_CreateOrUpdate.Deployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_CreateOrUpdate.Deployment.cshtml @@ -185,7 +185,7 @@ option = $('select[name="@(Html.FieldNameFor(x => x.DeploymentType))"]').find('option[value="' + item.id + '"]'); } - html += ' '; + html += ' '; html += item.text; html += ''; diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml index 256bed40e3..5b12410204 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml @@ -88,7 +88,7 @@ - + @T("Admin.Configuration.ActivityLog") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml index 5a7b260832..f890ef7440 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml @@ -40,7 +40,7 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml index 27941ed1cf..5d00bffdf5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml @@ -25,7 +25,7 @@ @if (Model.ColumnMappings.Any()) { } @@ -119,7 +119,7 @@ @@ -410,7 +410,7 @@ leftLabel.attr('title', ''); } - context.find('i').removeClass('fa-globe fa-chain-broken').addClass(isLocalized ? 'fa-globe' : 'fa-chain-broken'); + context.find('i').removeClass('fa-globe fa-unlink').addClass(isLocalized ? 'fa-globe' : 'fa-unlink'); } function initSelectBox(element, isProperty) { @@ -478,7 +478,7 @@ if (option.length === 0) { html += '
    '; html += ''; - html += ' '; + html += ' '; html += ''; } else { @@ -501,7 +501,7 @@ html += ''; html += '' : ' text-warning" />'); html += ''; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Language/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Language/Edit.cshtml index 5e37409a78..98fbea9085 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Language/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Language/Edit.cshtml @@ -27,7 +27,7 @@ @T("Admin.Configuration.Languages.Import")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Language/Partials/AvailableLanguages.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Language/Partials/AvailableLanguages.cshtml index a8c7bb96e8..e2be412332 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Language/Partials/AvailableLanguages.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Language/Partials/AvailableLanguages.cshtml @@ -72,7 +72,7 @@ - + @T("Common.Refresh") } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml index 4cd8d53eae..7409eae3f8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml @@ -13,11 +13,11 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Log/View.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Log/View.cshtml index 0ecad951ca..528364c8bc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Log/View.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Log/View.cshtml @@ -13,7 +13,7 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/Edit.cshtml index 4a2c61cbd8..0456f81dc4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/Edit.cshtml @@ -13,7 +13,7 @@ @{ Html.RenderWidget("admin_button_toolbar_before"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml index b1c4c0e914..ce22947113 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml @@ -5,7 +5,7 @@ }
    - + @T("Admin.Catalog.Manufacturers")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml index cfdb659b5f..94d40249ad 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml @@ -7,7 +7,7 @@ {
    - + @T("Admin.ContentManagement.MessageTemplates.EditMessageTemplateDetails") - @Model.Name @Html.ActionLink("(" + T("Admin.ContentManagement.MessageTemplates.BackToList") + ")", "List")
    @@ -16,7 +16,7 @@ @* CAUTION: saving a template to XML overwrites the original raw content *@ @**@ - + @T("Admin.Common.Preview") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml index 31f743131c..5b75096ee9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml @@ -6,7 +6,7 @@ }
    - + @T("Admin.ContentManagement.MessageTemplates")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml index 06fcf0f6aa..f762b5a245 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml @@ -19,7 +19,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml index 39a1707d01..a88278a11e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml @@ -21,7 +21,7 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml index 5f1666018a..6ea6c224f0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml @@ -5,7 +5,7 @@ }
    - + @T("Admin.Promotions.newsLetterSubscriptions")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/OnlineCustomer/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/OnlineCustomer/List.cshtml index 3766723ab5..b46d89452e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/OnlineCustomer/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/OnlineCustomer/List.cshtml @@ -8,7 +8,7 @@ }
    - + @T("Admin.Customers.OnlineCustomers")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml index 520b170a40..6cfd7225f6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml @@ -22,12 +22,12 @@ @if (Model.DisplayPdfInvoice) { - + @T("Admin.Orders.PdfInvoice") } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml index a46bf9a68e..ff4f515541 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml @@ -11,7 +11,7 @@ {
    - + @T("Admin.Orders")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml index 0872b1b569..48ee154e8d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml @@ -18,7 +18,7 @@ }
    @@ -36,7 +36,7 @@ @Html.EditorFor(model => model.TrackingNumber) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml index a422025620..9b53e5d2cb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml @@ -10,7 +10,7 @@
    @@ -25,11 +25,11 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Info.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Info.cshtml index 20dc195c1c..3a226ca316 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Info.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Info.cshtml @@ -524,7 +524,7 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml index 043794089b..e6ab7fdeea 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml @@ -18,7 +18,7 @@ @Html.SmartStore().TabStrip().Name("product-edit").OnAjaxSuccess("productEditTab_onAjaxSuccess").HtmlAttributes(new { data_product_id = Model.Id }).Style(TabsStyle.Material).Position(TabsPosition.Left).Items(x => { x.Add().Text(T("Admin.Catalog.Products.Info")) - .Icon("fa fa-pencil fa-lg fa-fw") + .Icon("fa fa-pencil-alt fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Info" }) .Content(Html.Partial("_CreateOrUpdate.Info", Model).ToHtmlString()) .Selected(true); @@ -54,7 +54,7 @@ if (Model.ProductTypeId != (int)ProductType.GroupedProduct) { x.Add().Text(T("Admin.Catalog.Products.Price").Text) - .Icon("fa fa-euro fa-lg fa-fw") + .Icon("fa fa-euro-sign fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Price" }) .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Price" }) .Ajax(); @@ -68,7 +68,7 @@ .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Discounts" }) .Ajax(); x.Add().Text(T("Admin.Catalog.Products.Pictures").Text) - .Icon("fa fa-picture-o fa-lg fa-fw") + .Icon("far fa-image fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Pictures" }) .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Pictures" }) .Ajax(); @@ -78,7 +78,7 @@ .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Categories" }) .Ajax(); x.Add().Text(T("Admin.Catalog.Products.Manufacturers").Text) - .Icon("fa fa-building-o fa-lg fa-fw") + .Icon("far fa-building fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Manufacturers" }) .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Manufacturers" }) .Ajax(); @@ -91,7 +91,7 @@ if (Model.ProductTypeId != (int)ProductType.BundledProduct) { x.Add().Text(T("Admin.Catalog.Products.ProductVariantAttributes.Attributes").Text) - .Icon("fa fa-list-alt fa-lg fa-fw") + .Icon("far fa-list-alt fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Attributes" }) .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Attributes" }) .Ajax(); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml index c0e28de45d..6da614eefe 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/Edit.cshtml index 5985378ad4..dad114749a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml index 29d92eaf35..098d1c78dc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml @@ -22,7 +22,7 @@ @T("Admin.Catalog.ProductReviews.DisapproveSelected") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml index 41d7320538..965b8600ce 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml index 2e6588dedd..7a9991407d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue") @if (Model.SendManually) @@ -28,7 +28,7 @@ } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml index b31449248f..6a6d0f4123 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml @@ -8,7 +8,7 @@ {
    - + @T("Admin.System.QueuedEmails")
    @@ -27,11 +27,11 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml index ed04ef37b0..0eba5eb7ec 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml @@ -17,7 +17,7 @@ @T("Admin.Common.SaveContinue")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml index 3aaf4761bc..5e496ca4cb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml @@ -8,7 +8,7 @@ }
    - + @T("Admin.RecurringPayments")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml index 6dd81bd552..6241edb6bf 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml @@ -33,7 +33,7 @@ @T("Admin.Common.SaveContinue")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/RoxyFileManager/Index.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/RoxyFileManager/Index.cshtml index 07e25b370b..62d5175927 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/RoxyFileManager/Index.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/RoxyFileManager/Index.cshtml @@ -38,11 +38,11 @@
    @@ -66,11 +66,11 @@
    @@ -160,7 +160,7 @@ Select - + Preview @@ -177,16 +177,16 @@ Copy - + Paste - + Rename - + Delete @@ -209,16 +209,16 @@ Copy - + Paste - + Rename - + Delete diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml index 2a7590b235..7e4b02b0a1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml @@ -14,7 +14,7 @@
    - + @ViewBag.Title (@T("Common.Back"))
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml index ea74c651fc..6c007ca0ed 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml @@ -7,7 +7,7 @@
    - + @T("Admin.System.ScheduleTasks")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml index c499516c35..e14d009412 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml @@ -27,7 +27,7 @@ {
    @@ -75,7 +75,7 @@ @if (item.CanCancel) { } @@ -125,7 +125,7 @@ diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasShoppingCart.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasShoppingCart.cshtml index 90332d9818..d0b66eddca 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasShoppingCart.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasShoppingCart.cshtml @@ -127,7 +127,7 @@ data-name="@item.ProductName" data-type="cart" data-action="addfromcart"> - + } - + diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasWishlist.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasWishlist.cshtml index 29993c2a65..626744fc56 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasWishlist.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Partials/OffCanvasWishlist.cshtml @@ -15,10 +15,10 @@ @T("ShoppingCart.Mini.EmptyWishlist.Title")

    - +

    - @T("ShoppingCart.Mini.EmptyWishlist.Info", "fa fa-lg fa-heart-o") + @T("ShoppingCart.Mini.EmptyWishlist.Info", "fal fa-lg fa-heart")

    } @@ -127,7 +127,7 @@ data-type="wishlist" data-action="remove" title='@T("Common.Remove")'> - + diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml index d7a2bb0a33..c547c7a46a 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml @@ -83,7 +83,7 @@ {
    @foreach (var product in Model.Items) { - var foundProductSpec = product.SpecificationAttributes.Where(psa => psa.SpecificationAttributeId == specificationAttribute.SpecificationAttributeId).FirstOrDefault(); - var specValue = foundProductSpec != null ? foundProductSpec.SpecificationAttributeOption : null; + var foundProductSpecs = product.SpecificationAttributes + .Where(psa => psa.SpecificationAttributeId == specificationAttribute.SpecificationAttributeId); } From 0624d7b414d83c297762c2850d5c83e01b2ef5f8 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Wed, 23 Jan 2019 21:13:05 +0100 Subject: [PATCH 102/657] Added StoryExportAssetAttribute --- .../Modelling/StoryExportAssetAttribute.cs | 30 +++++++++++++++++++ .../SmartStore.Web.Framework.csproj | 1 + .../Installation/InstallDataSeeder.cs | 1 - 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs new file mode 100644 index 0000000000..fafd448ad3 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace SmartStore.Web.Framework.Modelling +{ + /// + /// Specifies whether a property refers to an asset to be included in the story export. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class StoryExportAssetAttribute : Attribute + { + public StoryExportAssetAttribute(StoryExportPropertyType type) + { + Type = type; + } + + /// + /// The property type. + /// + public StoryExportPropertyType Type { get; private set; } + } + + + public enum StoryExportPropertyType + { + /// + /// The property value is a picture identifier. + /// + PictureId = 0 + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index c9c5625a5d..8bf2d23ed1 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -315,6 +315,7 @@ + diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs index 65cbc3b6a0..be9399e40d 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs @@ -33,7 +33,6 @@ using SmartStore.Services.Stores; using SmartStore.Utilities; using SmartStore.Web.Framework; -using SmartStore.Core.Infrastructure.DependencyManagement; namespace SmartStore.Web.Infrastructure.Installation { From ecb58eb936c661041fcc19b6c13e6df8e3058617 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 24 Jan 2019 17:00:03 +0100 Subject: [PATCH 103/657] More on StoryExportAssetAttribute --- .../Modelling/StoryExportAssetAttribute.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs index fafd448ad3..89e30f3613 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs @@ -8,15 +8,34 @@ namespace SmartStore.Web.Framework.Modelling [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class StoryExportAssetAttribute : Attribute { - public StoryExportAssetAttribute(StoryExportPropertyType type) + public StoryExportAssetAttribute( + StoryExportPropertyType type, + Type root, + string rootProperty) { + Guard.NotNull(root, nameof(root)); + Guard.NotEmpty(rootProperty, nameof(rootProperty)); + Type = type; + Root = root; + RootProperty = rootProperty; } /// - /// The property type. + /// The asset property type. /// public StoryExportPropertyType Type { get; private set; } + + /// + /// The root entity type that contains the model data. + /// + public Type Root { get; private set; } + + /// + /// The root property name that contains the model data. + /// It is expected that the property value is always a Json serialized string. + /// + public string RootProperty { get; private set; } } @@ -27,4 +46,12 @@ public enum StoryExportPropertyType /// PictureId = 0 } + + + /// + /// Required to identify export assets containing models. + /// + public interface IStoryExportModel + { + } } From 02c82c9a8aa3255123c4bf6b27bfaa7e44295be8 Mon Sep 17 00:00:00 2001 From: Murat Cakir Date: Thu, 24 Jan 2019 19:24:19 +0100 Subject: [PATCH 104/657] Story assets: code conventions --- .../SmartStore.Web.Framework.csproj | 2 +- .../UI/Blocks/IBlockEntity.cs | 4 ---- .../Blocks/StoryAssetAttribute.cs} | 16 ++++++++-------- 3 files changed, 9 insertions(+), 13 deletions(-) rename src/Presentation/SmartStore.Web.Framework/{Modelling/StoryExportAssetAttribute.cs => UI/Blocks/StoryAssetAttribute.cs} (78%) diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index 8bf2d23ed1..e432dd216a 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -315,7 +315,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs index b7b868f042..ef70797399 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Web.Framework.UI.Blocks { diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs similarity index 78% rename from src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs rename to src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs index 89e30f3613..9f99442ce9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/StoryExportAssetAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs @@ -1,22 +1,22 @@ using System; -namespace SmartStore.Web.Framework.Modelling +namespace SmartStore.Web.Framework.UI.Blocks { /// /// Specifies whether a property refers to an asset to be included in the story export. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public sealed class StoryExportAssetAttribute : Attribute + public sealed class StoryAssetAttribute : Attribute { - public StoryExportAssetAttribute( - StoryExportPropertyType type, + public StoryAssetAttribute( + StoryAssetKind type, Type root, string rootProperty) { Guard.NotNull(root, nameof(root)); Guard.NotEmpty(rootProperty, nameof(rootProperty)); - Type = type; + Kind = type; Root = root; RootProperty = rootProperty; } @@ -24,7 +24,7 @@ public StoryExportAssetAttribute( /// /// The asset property type. /// - public StoryExportPropertyType Type { get; private set; } + public StoryAssetKind Kind { get; private set; } /// /// The root entity type that contains the model data. @@ -39,12 +39,12 @@ public StoryExportAssetAttribute( } - public enum StoryExportPropertyType + public enum StoryAssetKind { /// /// The property value is a picture identifier. /// - PictureId = 0 + Picture = 0 } From af89a95caa89496ee59c0a0e00b2dc6852d49f9b Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 25 Jan 2019 12:58:44 +0100 Subject: [PATCH 105/657] More on StoryExportAssetAttribute --- .../Collections/ConcurrentMultimap.cs | 6 +++- .../UI/Blocks/StoryAssetAttribute.cs | 33 ++----------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs index 0e02723ec4..3929c80b79 100644 --- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs @@ -81,7 +81,11 @@ public virtual IProducerConsumerCollection this[TKey key] IProducerConsumerCollection value; if (!_items.TryGetValue(key, out value)) { - _items.TryAdd(key, _bagCreator(Enumerable.Empty())); + value = _bagCreator(Enumerable.Empty()); + if (!_items.TryAdd(key, value)) + { + value = null; + } } return value; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs index 9f99442ce9..95e2956caa 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs @@ -8,34 +8,15 @@ namespace SmartStore.Web.Framework.UI.Blocks [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class StoryAssetAttribute : Attribute { - public StoryAssetAttribute( - StoryAssetKind type, - Type root, - string rootProperty) + public StoryAssetAttribute(StoryAssetKind kind) { - Guard.NotNull(root, nameof(root)); - Guard.NotEmpty(rootProperty, nameof(rootProperty)); - - Kind = type; - Root = root; - RootProperty = rootProperty; + Kind = kind; } /// - /// The asset property type. + /// The asset property kind. /// public StoryAssetKind Kind { get; private set; } - - /// - /// The root entity type that contains the model data. - /// - public Type Root { get; private set; } - - /// - /// The root property name that contains the model data. - /// It is expected that the property value is always a Json serialized string. - /// - public string RootProperty { get; private set; } } @@ -46,12 +27,4 @@ public enum StoryAssetKind /// Picture = 0 } - - - /// - /// Required to identify export assets containing models. - /// - public interface IStoryExportModel - { - } } From a6394184b7fced304926162310f9acdc31d87b6d Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 28 Jan 2019 12:24:54 +0100 Subject: [PATCH 106/657] GMC: fixes custom labels were not exported --- changelog.md | 1 + .../Providers/GmcXmlExportProvider.cs | 23 +++++++++++++------ .../UI/Blocks/StoryAssetAttribute.cs | 7 +++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index 7d39f8c679..2f87697c35 100644 --- a/changelog.md +++ b/changelog.md @@ -72,6 +72,7 @@ * Export the product images if no attribute images are defined * Do not export the first image twice for additional images * Export image URL of full size image (not default size) for additional images + * Custom labels were not exported * Media middleware: 0-byte files should be treated as missing. * Megamenu alpha/omega blends do not toggle correctly on touch devices * Summernote HTML editor exceeds parent container width when CodeMirror is activated diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index 84c27f6cf7..1f57fcf317 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -480,16 +480,25 @@ protected override void Export(ExportExecuteContext context) } var customLabel0 = GetAttributeValue(attributeValues, "custom_label_0", languageId, gmc?.CustomLabel0, null); - var customLabel1 = GetAttributeValue(attributeValues, "custom_label_1", languageId, gmc?.CustomLabel1, null); - var customLabel2 = GetAttributeValue(attributeValues, "custom_label_2", languageId, gmc?.CustomLabel2, null); - var customLabel3 = GetAttributeValue(attributeValues, "custom_label_3", languageId, gmc?.CustomLabel3, null); - var customLabel4 = GetAttributeValue(attributeValues, "custom_label_4", languageId, gmc?.CustomLabel4, null); + WriteString(writer, "custom_label_0", gmc.CustomLabel0.NullEmpty()); - ++context.RecordsSucceeded; + var customLabel1 = GetAttributeValue(attributeValues, "custom_label_1", languageId, gmc?.CustomLabel1, null); + WriteString(writer, "custom_label_1", gmc.CustomLabel1.NullEmpty()); + + var customLabel2 = GetAttributeValue(attributeValues, "custom_label_2", languageId, gmc?.CustomLabel2, null); + WriteString(writer, "custom_label_2", gmc.CustomLabel2.NullEmpty()); + + var customLabel3 = GetAttributeValue(attributeValues, "custom_label_3", languageId, gmc?.CustomLabel3, null); + WriteString(writer, "custom_label_3", gmc.CustomLabel3.NullEmpty()); + + var customLabel4 = GetAttributeValue(attributeValues, "custom_label_4", languageId, gmc?.CustomLabel4, null); + WriteString(writer, "custom_label_4", gmc.CustomLabel4.NullEmpty()); + + ++context.RecordsSucceeded; } - catch (Exception exception) + catch (Exception ex) { - context.RecordException(exception, entity.Id); + context.RecordException(ex, entity.Id); } writer.WriteEndElement(); // item diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs index 95e2956caa..707dc55e9d 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs @@ -25,6 +25,11 @@ public enum StoryAssetKind /// /// The property value is a picture identifier. /// - Picture = 0 + Picture = 0, + + /// + /// The property value is a video path. + /// + Video } } From 16278947654a8172c7cae648011cf3b99bba1769 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Thu, 31 Jan 2019 21:47:48 +0100 Subject: [PATCH 107/657] More on StoryAssetAttribute --- .../UI/Blocks/StoryAssetAttribute.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs index 707dc55e9d..3257ee879b 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs @@ -30,6 +30,21 @@ public enum StoryAssetKind /// /// The property value is a video path. /// - Video + Video, + + /// + /// The property value is a product identifier. + /// + Product, + + /// + /// The property value is a category identifier. + /// + Category, + + /// + /// The property value is a manufacturer identifier. + /// + Manufacturer } } From 1179cfd4af49a3f9ef995d535118d8caca1fb27b Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 4 Feb 2019 09:36:00 +0100 Subject: [PATCH 108/657] Link builder: what about localized URLs? --- .../Migrations/MigrationsConfiguration.cs | 10 ++ .../Seo/UrlRecordService.cs | 4 +- .../EntityPicker/EntityPickerBuilder.cs | 2 - .../Controllers/EntityController.cs | 115 +++++++++--------- .../SmartStore.Web/SmartStore.Web.csproj | 1 + .../Views/Shared/EditorTemplates/Link.cshtml | 49 ++++++++ 6 files changed, 119 insertions(+), 62 deletions(-) create mode 100644 src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Link.cshtml diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index c24d77ffd2..5455cd9a51 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -641,6 +641,16 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages.Note", "Click Download to install a new language including all localized resources. On translate.smartstore.com you will find more details about available resources.", "Klicken Sie auf Download, um eine neue Sprache mit allen lokalisierten Ressourcen zu installieren. Auf translate.smartstore.com finden Sie weitere Details zu verfügbaren Ressourcen."); + + builder.AddOrUpdate("Common.Entity.Product", "Product", "Produkt"); + builder.AddOrUpdate("Common.Entity.Category", "Category", "Warengruppe"); + builder.AddOrUpdate("Common.Entity.Manufacturer", "Manufacturer", "Hersteller"); + builder.AddOrUpdate("Common.Entity.Topic", "Topic", "Seite"); + + builder.AddOrUpdate("Common.Entity.SelectProduct", "Select product", "Produkt auswählen"); + builder.AddOrUpdate("Common.Entity.SelectCategory", "Select category", "Warengruppe auswählen"); + builder.AddOrUpdate("Common.Entity.SelectManufacturer", "Select manufacturer", "Hersteller auswählen"); + builder.AddOrUpdate("Common.Entity.SelectTopic", "Select topic", "Seite auswählen"); } } } diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index bbdb23440b..af827690c3 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -246,7 +246,9 @@ public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityN { if (languageIds.Length == 1) { - query = query.Where(x => x.LanguageId == languageIds[0]); + // Avoid "The LINQ expression node type 'ArrayIndex' is not supported in LINQ to Entities". + var languageId = languageIds[0]; + query = query.Where(x => x.LanguageId == languageId); } else { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs index a17551a51e..a7b8503942 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs @@ -71,8 +71,6 @@ public EntityPickerBuilder For(Expression> public EntityPickerBuilder For(string expression) { - Guard.NotEmpty(expression, nameof(expression)); - base.Component.TargetInputSelector = "#" + this.HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(expression); return this; } diff --git a/src/Presentation/SmartStore.Web/Controllers/EntityController.cs b/src/Presentation/SmartStore.Web/Controllers/EntityController.cs index 539ca5080e..4c32bd524f 100644 --- a/src/Presentation/SmartStore.Web/Controllers/EntityController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/EntityController.cs @@ -1,28 +1,27 @@ using System; using System.Collections.Generic; +using System.Data.Entity; using System.Linq; using System.Web.Mvc; -using System.Data.Entity; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Media; using SmartStore.Core.Search; -using SmartStore.Services; using SmartStore.Services.Catalog; +using SmartStore.Services.Customers; using SmartStore.Services.Media; using SmartStore.Services.Search; using SmartStore.Services.Security; +using SmartStore.Services.Seo; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Models.Entity; -using SmartStore.Services.Customers; -using SmartStore.Core.Domain.Customers; namespace SmartStore.Web.Controllers { - public partial class EntityController : PublicControllerBase + public partial class EntityController : PublicControllerBase { - private readonly ICommonServices _services; private readonly ICatalogSearchService _catalogSearchService; private readonly CatalogSettings _catalogSettings; private readonly MediaSettings _mediaSettings; @@ -31,11 +30,9 @@ public partial class EntityController : PublicControllerBase private readonly IManufacturerService _manufacturerService; private readonly ICustomerService _customerService; private readonly ICategoryService _categoryService; - private readonly IProductService _productService; - private readonly CatalogHelper _catalogHelper; + private readonly IUrlRecordService _urlRecordService; public EntityController( - ICommonServices services, ICatalogSearchService catalogSearchService, CatalogSettings catalogSettings, MediaSettings mediaSettings, @@ -44,10 +41,8 @@ public EntityController( IManufacturerService manufacturerService, ICustomerService customerService, ICategoryService categoryService, - IProductService productService, - CatalogHelper catalogHelper) + IUrlRecordService urlRecordService) { - _services = services; _catalogSearchService = catalogSearchService; _catalogSettings = catalogSettings; _mediaSettings = mediaSettings; @@ -56,15 +51,14 @@ public EntityController( _manufacturerService = manufacturerService; _customerService = customerService; _categoryService = categoryService; - _productService = productService; - _catalogHelper = catalogHelper; + _urlRecordService = urlRecordService; } #region Entity Picker public ActionResult Picker(EntityPickerModel model) { - model.PageSize = 96; // _commonSettings.EntityPickerPageSize; + model.PageSize = 96; if (model.EntityType.IsCaseInsensitiveEqual("product")) { @@ -77,7 +71,7 @@ public ActionResult Picker(EntityPickerModel model) .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) .ToList(); - ViewBag.AvailableStores = _services.StoreService.GetAllStores() + ViewBag.AvailableStores = Services.StoreService.GetAllStores() .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) .ToList(); @@ -90,7 +84,7 @@ public ActionResult Picker(EntityPickerModel model) new SelectListItem { Text = "Email", Value = "Email" } }; - if (_services.Settings.GetSettingByKey("CustomerSettings.CustomerNumberMethod") != CustomerNumberMethod.Disabled) + if (Services.Settings.GetSettingByKey("CustomerSettings.CustomerNumberMethod") != CustomerNumberMethod.Disabled) { ViewBag.AvailableCustomerSearchTypes.Add(new SelectListItem { Text = T("Account.Fields.CustomerNumber"), Value = "CustomerNumber" }); } @@ -102,24 +96,22 @@ public ActionResult Picker(EntityPickerModel model) [HttpPost] public ActionResult Picker(EntityPickerModel model, FormCollection form) { - model.PageSize = 96; // _commonSettings.EntityPickerPageSize; + model.PageSize = 96; try { + var language = Services.WorkContext.WorkingLanguage; var disableIf = model.DisableIf.SplitSafe(",").Select(x => x.ToLower().Trim()).ToList(); var disableIds = model.DisableIds.SplitSafe(",").Select(x => x.ToInt()).ToList(); - var selIds = new HashSet(model.PreselectedEntityIds.ToIntArray()); - using (var scope = new DbContextScope(_services.DbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) + using (var scope = new DbContextScope(Services.DbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) { if (model.EntityType.IsCaseInsensitiveEqual("product")) { - #region Product - model.SearchTerm = model.SearchTerm.TrimSafe(); - var hasPermission = _services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog); + var hasPermission = Services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog); var disableIfNotSimpleProduct = disableIf.Contains("notsimpleproduct"); var disableIfGroupedProduct = disableIf.Contains("groupedproduct"); var labelTextGrouped = T("Admin.Catalog.Products.ProductType.GroupedProduct.Label").Text; @@ -137,7 +129,7 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) if (!hasPermission) { - searchQuery = searchQuery.VisibleOnly(_services.WorkContext.CurrentCustomer); + searchQuery = searchQuery.VisibleOnly(Services.WorkContext.CurrentCustomer); } if (model.ProductTypeId > 0) @@ -179,28 +171,34 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) var allPictureIds = products.Select(x => x.MainPictureId.GetValueOrDefault()); var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); + var slugs = model.ReturnField.IsCaseInsensitiveEqual("link") + ? _urlRecordService.GetUrlRecordCollection(nameof(Product), null, products.Select(x => x.Id).ToArray()) + : null; - model.SearchResult = products + model.SearchResult = products .Select(x => - { - var item = new EntityPickerModel.SearchResultModel - { - Id = x.Id, - ReturnValue = (model.ReturnField.IsCaseInsensitiveEqual("sku") ? x.Sku : x.Id.ToString()), - Title = x.Name, - Summary = x.Sku, - SummaryTitle = "{0}: {1}".FormatInvariant(sku, x.Sku.NaIfEmpty()), - Published = (hasPermission ? x.Published : (bool?)null), - Selected = selIds.Contains(x.Id) - }; + { + var item = new EntityPickerModel.SearchResultModel + { + Id = x.Id, + Title = x.Name, + Summary = x.Sku, + SummaryTitle = "{0}: {1}".FormatInvariant(sku, x.Sku.NaIfEmpty()), + Published = hasPermission ? x.Published : (bool?)null, + Selected = selIds.Contains(x.Id) + }; + + item.ReturnValue = slugs != null + ? Url.RouteUrl("Product", new { SeName = slugs.GetSlug(language.Id, x.Id) }) + : (model.ReturnField.IsCaseInsensitiveEqual("sku") ? x.Sku : x.Id.ToString()); if (disableIfNotSimpleProduct) { - item.Disable = (x.ProductTypeId != (int)ProductType.SimpleProduct); + item.Disable = x.ProductTypeId != (int)ProductType.SimpleProduct; } else if (disableIfGroupedProduct) { - item.Disable = (x.ProductTypeId == (int)ProductType.GroupedProduct); + item.Disable = x.ProductTypeId == (int)ProductType.GroupedProduct; } if (!item.Disable && disableIds.Contains(x.Id)) @@ -230,16 +228,15 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) return item; }) .ToList(); - - #endregion } else if (model.EntityType.IsCaseInsensitiveEqual("category")) - { - #region Category - + { var categories = _categoryService.GetAllCategories(model.SearchTerm, showHidden: true); var allPictureIds = categories.Select(x => x.PictureId.GetValueOrDefault()); var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); + var slugs = model.ReturnField.IsCaseInsensitiveEqual("link") + ? _urlRecordService.GetUrlRecordCollection(nameof(Category), null, categories.Select(x => x.Id).ToArray()) + : null; model.SearchResult = categories .Select(x => @@ -247,14 +244,17 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) var item = new EntityPickerModel.SearchResultModel { Id = x.Id, - ReturnValue = x.Id.ToString(), Title = x.Name, - Summary = x.Description.Truncate(120, "..."), + Summary = x.Description.Truncate(120, "…"), SummaryTitle = x.Name, Published = x.Published, Selected = selIds.Contains(x.Id) }; + item.ReturnValue = slugs != null + ? Url.RouteUrl("Category", new { SeName = slugs.GetSlug(language.Id, x.Id) }) + : x.Id.ToString(); + if (!item.Disable && disableIds.Contains(x.Id)) { item.Disable = true; @@ -271,16 +271,15 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) return item; }) .ToList(); - - #endregion } else if (model.EntityType.IsCaseInsensitiveEqual("manufacturer")) { - #region Manufacturer - var manufacturers = _manufacturerService.GetAllManufacturers(model.SearchTerm, model.PageIndex, model.PageSize, showHidden: true); var allPictureIds = manufacturers.Select(x => x.PictureId.GetValueOrDefault()); var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); + var slugs = model.ReturnField.IsCaseInsensitiveEqual("link") + ? _urlRecordService.GetUrlRecordCollection(nameof(Manufacturer), null, manufacturers.Select(x => x.Id).ToArray()) + : null; model.SearchResult = manufacturers .Select(x => @@ -294,7 +293,11 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) Published = x.Published, Selected = selIds.Contains(x.Id) }; - + + item.ReturnValue = slugs != null + ? Url.RouteUrl("Manufacturer", new { SeName = slugs.GetSlug(language.Id, x.Id) }) + : x.Id.ToString(); + if (!item.Disable && disableIds.Contains(x.Id)) { item.Disable = true; @@ -311,18 +314,14 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) return item; }) .ToList(); - - #endregion } else if (model.EntityType.IsCaseInsensitiveEqual("customer")) { - #region Customer - var registeredRoleId = _customerService.GetCustomerRoleBySystemName("Registered").Id; - var searchTermName = String.Empty; - var searchTermEmail = String.Empty; - var searchTermCustomerNumber = String.Empty; + var searchTermName = string.Empty; + var searchTermEmail = string.Empty; + var searchTermCustomerNumber = string.Empty; if (model.CustomerSearchType.IsCaseInsensitiveEqual("Name")) searchTermName = model.SearchTerm; @@ -365,8 +364,6 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) return item; }) .ToList(); - - #endregion } } } diff --git a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj index a20e6d6eae..0dea0171ff 100644 --- a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj +++ b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj @@ -1828,6 +1828,7 @@ + Web.config diff --git a/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Link.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Link.cshtml new file mode 100644 index 0000000000..e446fd9403 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Shared/EditorTemplates/Link.cshtml @@ -0,0 +1,49 @@ +@functions +{ + private dynamic GetTypeInfo(string type) + { + switch (type) + { + case "product": + return new { icon = "fa fa-cube", title = T("Common.Entity.Product").Text }; + case "category": + return new { icon = "fa fa-sitemap", title = T("Common.Entity.Category").Text }; + case "manufacturer": + return new { icon = "far fa-building", title = T("Common.Entity.Manufacturer").Text }; + case "topic": + return new { icon = "far fa-file", title = T("Common.Entity.Topic").Text }; + default: + return new { icon = "fa fa-link", title = "URL" }; + } + } + + private string Value => ViewData.Model != null ? Convert.ToString(ViewData.Model) : null; +} + +@{ + var type = (GetMetadata("type").NullEmpty() ?? "product").ToLower(); + var typeInfo = GetTypeInfo(type); + var maxItems = GetMetadata("maxItems") ?? 1; + var appendMode = GetMetadata("appendMode") ?? false; +} + +
    +
    + + + +
    + @Html.TextBox("", Value, new { @class = "form-control" }) +
    + @(Html.SmartStore().EntityPicker() + .For(string.Empty) + .EntityType("product") + .MaxItems(maxItems) + .AppendMode(appendMode) + .FieldName("link") + .DialogTitle(T("Common.Entity.SelectProduct").Text.EncodeJsString('\'', false)) + .Caption(T("Common.Search").Text.EncodeJsString('\'', false))) +
    +
    + +@**@ \ No newline at end of file From 404b6cfb9bca62f50e84c600a0c99631e2d5e839 Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 4 Feb 2019 12:03:35 +0100 Subject: [PATCH 109/657] Resolves #1570 Filter option "Only deactivated customers" filters deleted instead of deactivated customers Fixes the "apply filter" button has misaligned a bit --- changelog.md | 3 +- .../Migrations/MigrationsConfiguration.cs | 3 ++ .../Customers/CustomerSearchQuery.cs | 13 +++++--- .../Customers/CustomerService.cs | 13 +++++--- .../Extensions/HtmlExtensions.cs | 30 +++++++++++-------- .../Controllers/CustomerController.cs | 9 +++--- .../Models/Customers/CustomerListModel.cs | 8 ++--- .../Views/ActivityLog/ListLogs.cshtml | 2 +- .../Administration/Views/Category/List.cshtml | 2 +- .../Administration/Views/Customer/List.cshtml | 25 ++++++++-------- ...ReportBestCustomersByNumberOfOrders.cshtml | 2 +- .../_ReportBestCustomersByOrderTotal.cshtml | 2 +- .../Administration/Views/GiftCard/List.cshtml | 2 +- .../Administration/Views/Log/List.cshtml | 2 +- .../Views/Manufacturer/List.cshtml | 2 +- .../Views/MessageTemplate/List.cshtml | 2 +- .../Administration/Views/News/List.cshtml | 2 +- .../Views/NewsLetterSubscription/List.cshtml | 2 +- .../Administration/Views/Order/List.cshtml | 2 +- .../Views/Order/NeverSoldReport.cshtml | 4 +-- .../Views/Order/ShipmentList.cshtml | 2 +- .../Views/Product/BulkEdit.cshtml | 2 +- .../Administration/Views/Product/List.cshtml | 2 +- .../Views/ProductReview/List.cshtml | 2 +- .../Views/QueuedEmail/List.cshtml | 2 +- .../Views/ReturnRequest/List.cshtml | 2 +- .../Administration/Views/Topic/List.cshtml | 2 +- .../Views/UrlRecord/List.cshtml | 2 +- 28 files changed, 83 insertions(+), 63 deletions(-) diff --git a/changelog.md b/changelog.md index 2f87697c35..13db42f67b 100644 --- a/changelog.md +++ b/changelog.md @@ -107,7 +107,8 @@ * Import: Fixes invalid conversion "System.Double" to "SmartStore.Core.Domain.Catalog.QuantityControlType". * Topics: Fixes "Cannot insert duplicate key row in object 'dbo.UrlRecord' with unique index 'IX_UrlRecord_Slug'". * #1566 Santander: eliminate the 1 cent rounding difference at amountTotalNet. -* Fixed redirection to the homepage for pages which are loaded while the application is restarted +* Fixed redirection to the homepage for pages which are loaded while the application is restarted. +* #1570 Filter option "Only deactivated customers" filters deleted instead of deactivated customers. ## SmartStore.NET 3.1.5 diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 5455cd9a51..2fc48aa446 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -651,6 +651,9 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Common.Entity.SelectCategory", "Select category", "Warengruppe auswählen"); builder.AddOrUpdate("Common.Entity.SelectManufacturer", "Select manufacturer", "Hersteller auswählen"); builder.AddOrUpdate("Common.Entity.SelectTopic", "Select topic", "Seite auswählen"); + + builder.Delete("Admin.Customers.Customers.List.SearchDeletedOnly"); + builder.AddOrUpdate("Admin.Customers.Customers.List.SearchActiveOnly", "Only activated customers", "Nur aktivierte Kunden"); } } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs b/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs index 92710cc92e..e0737ac075 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs @@ -86,10 +86,15 @@ public class CustomerSearchQuery : ICloneable /// public bool? Deleted { get; set; } = false; - /// - /// Whether only system account records should be loaded. Default: false (meaning: ignore system accounts) - /// - public bool? IsSystemAccount { get; set; } = false; + /// + /// Whether only active customers should be loaded. + /// + public bool? Active { get; set; } + + /// + /// Whether only system account records should be loaded. Default: false (meaning: ignore system accounts) + /// + public bool? IsSystemAccount { get; set; } = false; /// /// Searches in FullName (FirstName + LastName) and Company fields. Default: null. diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index db565351e5..3709c7a3e2 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -182,6 +182,11 @@ select grp query = query.Where(c => c.Deleted == q.Deleted.Value); } + if (q.Active.HasValue) + { + query = query.Where(c => c.Active == q.Active.Value); + } + if (q.IsSystemAccount.HasValue) { query = q.IsSystemAccount.Value == true @@ -200,9 +205,9 @@ select grp { query = query .Join(_gaRepository.Table, x => x.Id, y => y.EntityId, (x, y) => new { Customer = x, Attribute = y }) - .Where((z => z.Attribute.KeyGroup == "Customer" && + .Where(z => z.Attribute.KeyGroup == "Customer" && z.Attribute.Key == SystemCustomerAttributeNames.Phone && - z.Attribute.Value.Contains(q.Phone))) + z.Attribute.Value.Contains(q.Phone)) .Select(z => z.Customer); } @@ -211,9 +216,9 @@ select grp { query = query .Join(_gaRepository.Table, x => x.Id, y => y.EntityId, (x, y) => new { Customer = x, Attribute = y }) - .Where((z => z.Attribute.KeyGroup == "Customer" && + .Where(z => z.Attribute.KeyGroup == "Customer" && z.Attribute.Key == SystemCustomerAttributeNames.ZipPostalCode && - z.Attribute.Value.Contains(q.ZipPostalCode))) + z.Attribute.Value.Contains(q.ZipPostalCode)) .Select(z => z.Customer); } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index 40e05538a9..325144c315 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -148,20 +148,26 @@ public static MvcHtmlString SmartLabel(this HtmlHelper helper, string expression { var result = new StringBuilder(); - var labelAttrs = new RouteValueDictionary(htmlAttributes); - //labelAttrs.AppendCssClass("col-form-label"); + result.Append("
    "); - var label = helper.Label(expression, labelText, labelAttrs); + if (expression.IsEmpty() && labelText.IsEmpty()) + { + result.Append(""); + } + else + { + var labelAttrs = new RouteValueDictionary(htmlAttributes); + var label = helper.Label(expression, labelText, labelAttrs); - result.Append("
    "); - { - result.Append(label); - if (hint.HasValue()) - { - result.Append(helper.Hint(hint).ToHtmlString()); - } - } - result.Append("
    "); + result.Append(label); + } + + if (hint.HasValue()) + { + result.Append(helper.Hint(hint).ToHtmlString()); + } + + result.Append("
    "); return MvcHtmlString.Create(result.ToString()); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs index bd17596d1a..41f8438212 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs @@ -74,7 +74,6 @@ public partial class CustomerController : AdminControllerBase private readonly IPermissionService _permissionService; private readonly AdminAreaSettings _adminAreaSettings; private readonly IQueuedEmailService _queuedEmailService; - private readonly EmailAccountSettings _emailAccountSettings; private readonly IEmailAccountService _emailAccountService; private readonly ForumSettings _forumSettings; private readonly IForumService _forumService; @@ -108,7 +107,7 @@ public CustomerController( ICustomerActivityService customerActivityService, IPriceCalculationService priceCalculationService, IPermissionService permissionService, AdminAreaSettings adminAreaSettings, - IQueuedEmailService queuedEmailService, EmailAccountSettings emailAccountSettings, + IQueuedEmailService queuedEmailService, IEmailAccountService emailAccountService, ForumSettings forumSettings, IForumService forumService, IOpenAuthenticationService openAuthenticationService, AddressSettings addressSettings, IStoreService storeService, @@ -142,7 +141,6 @@ public CustomerController( _permissionService = permissionService; _adminAreaSettings = adminAreaSettings; _queuedEmailService = queuedEmailService; - _emailAccountSettings = emailAccountSettings; _emailAccountService = emailAccountService; _forumSettings = forumSettings; _forumService = forumService; @@ -501,7 +499,7 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult CustomerList(GridCommand command, CustomerListModel model) { - // We use own own binder for searchCustomerRoleIds property + // We use own own binder for searchCustomerRoleIds property. var gridModel = new GridModel(); if (_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) @@ -516,7 +514,8 @@ public ActionResult CustomerList(GridCommand command, CustomerListModel model) MonthOfBirth = model.SearchMonthOfBirth.ToInt(), Phone = model.SearchPhone, ZipPostalCode = model.SearchZipPostalCode, - Deleted = model.SearchDeletedOnly, + Deleted = false, + Active = model.SearchActiveOnly, PageIndex = command.Page - 1, PageSize = command.PageSize }; diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Customers/CustomerListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Customers/CustomerListModel.cs index b6bea763ea..6bc3c7a0dd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Customers/CustomerListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Customers/CustomerListModel.cs @@ -8,7 +8,7 @@ namespace SmartStore.Admin.Models.Customers { public class CustomerListModel : ModelBase { - public CustomerListModel () + public CustomerListModel() { AvailableCustomerRoles = new List(); } @@ -53,7 +53,7 @@ public CustomerListModel () public string SearchZipPostalCode { get; set; } public bool ZipPostalCodeEnabled { get; set; } - [SmartResourceDisplayName("Admin.Customers.Customers.List.SearchDeletedOnly")] - public bool SearchDeletedOnly { get; set; } - } + [SmartResourceDisplayName("Admin.Customers.Customers.List.SearchActiveOnly")] + public bool? SearchActiveOnly { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml index 104618e045..f01056f3b5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml @@ -46,7 +46,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml index 0d8722653d..a5bd650d60 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml @@ -46,7 +46,7 @@
    }
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/List.cshtml index cc55977547..da65f0c747 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/List.cshtml @@ -97,20 +97,15 @@
    @Html.SmartLabelFor(model => model.SearchCustomerRoleIds) - @*@Html.DropDownListFor(model => model.SearchCustomerRoleIds, Model.AvailableCustomerRoles, null, new { multiple = "multiple", @class = "form-control" })*@ @Html.DropDownList("SearchCustomerRoleIds", Model.AvailableCustomerRoles, null, new { multiple = "multiple", @class = "form-control" })
    - -
    - @Html.CheckBoxFor(model => model.SearchDeletedOnly, new { @class = "form-check-input" }) - -
    + @Html.SmartLabelFor(model => model.SearchActiveOnly) + @Html.EditorFor(model => Model.SearchActiveOnly, new { @class = "form-control sf" })
    - @@ -122,7 +117,8 @@ .Name("customers-grid") .ClientEvents(events => events .OnDataBinding("onDataBinding") - .OnDataBound("onDataBound")) + .OnDataBound("onDataBound") + .OnRowDataBound("onRowDataBound")) .Columns(columns => { columns.Bound(x => x.Id) @@ -216,8 +212,7 @@ }); }); - function onDataBound(e) { - + function onDataBound() { $('#customers-grid input[type=checkbox][id!=mastercheckbox]').each(function () { var currentId = $(this).val(); var checked = jQuery.inArray(currentId, selectedIds); @@ -226,7 +221,13 @@ }); updateMasterCheckbox(); - } + } + + function onRowDataBound(e) { + if (!e.dataItem.Active) { + $(e.row).find('td').wrapInner(''); + } + } function updateMasterCheckbox() { var numChkBoxes = $('#customers-grid input[type=checkbox][id!=mastercheckbox]').length; @@ -244,7 +245,7 @@ SearchMonthOfBirth: $('#@Html.FieldIdFor(model => model.SearchMonthOfBirth)').val(), SearchPhone: $('#@Html.FieldIdFor(model => model.SearchPhone)').val(), SearchZipPostalCode: $('#@Html.FieldIdFor(model => model.SearchZipPostalCode)').val(), - SearchDeletedOnly: $('#@Html.FieldIdFor(model => model.SearchDeletedOnly)').prop('checked') + SearchActiveOnly: $('#@Html.FieldIdFor(model => model.SearchActiveOnly)').val() }; e.data = searchModel; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml index 389e0ffc36..5da15e34b7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml @@ -24,7 +24,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml index 7e07fd7b5c..a693c83bce 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml @@ -24,7 +24,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml index a22c72cf7e..2cc47b5526 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml @@ -28,7 +28,7 @@ @Html.TextBoxFor(model => model.CouponCode, new { @class = "form-control" })
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml index 7409eae3f8..82a4890ee1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml @@ -45,7 +45,7 @@ @Html.DropDownList("LogLevelId", Model.AvailableLogLevels, T("Admin.Common.All"))
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml index ce22947113..543372a417 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml @@ -33,7 +33,7 @@
    }
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml index 5b75096ee9..7ad2be365a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/List.cshtml @@ -25,7 +25,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml index a88278a11e..2a45db4bf7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml @@ -37,7 +37,7 @@ @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All"), new { @class = "form-control" })
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml index 6ea6c224f0..982e5f05e2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml @@ -28,7 +28,7 @@
    }
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml index ff4f515541..100b11a8d0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml @@ -100,7 +100,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/NeverSoldReport.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/NeverSoldReport.cshtml index 86f13c1590..3762130c58 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/NeverSoldReport.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/NeverSoldReport.cshtml @@ -25,9 +25,9 @@ @Html.SmartLabelFor(model => model.EndDate) @Html.EditorFor(model => model.EndDate)
    - +
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml index 61330f9f09..a340893b3c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml @@ -43,7 +43,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml index 7e1ebc3ba5..6884fb03d0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml @@ -46,7 +46,7 @@ @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, allString)
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml index 1fe243f910..3b8924e689 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml @@ -84,7 +84,7 @@
    }
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml index 098d1c78dc..6fc0887407 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml @@ -40,7 +40,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml index 6a6d0f4123..5038df4680 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml @@ -74,7 +74,7 @@
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml index 1649806229..3ee82d345f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml @@ -30,7 +30,7 @@ @Html.DropDownListFor(model => model.SearchReturnRequestStatusId, Model.AvailableReturnRequestStatus, T("Admin.Common.All"))
    - + @Html.SmartLabel(string.Empty, string.Empty) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml index 9d85d4d644..3da1caad9d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml @@ -48,7 +48,7 @@
    @if (Model.AvailableStores.Count > 1) { - + @Html.SmartLabel(string.Empty, string.Empty) }
    - + @Html.SmartLabel(string.Empty, string.Empty) From f5b62c3a5aea2c5dd76e329851a784ecad2fcb0b Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 8 Feb 2019 11:46:12 +0100 Subject: [PATCH 110/657] Fixes broken unit tests --- changelog.md | 1 + .../Discounts/DiscountService.cs | 22 +---- .../Infrastructure/AutoMapperAdminProfile.cs | 24 +++-- .../Discounts/DiscountServiceTests.cs | 92 ++++++++----------- .../Helpers/DateTimeHelperTests.cs | 35 +++---- 5 files changed, 68 insertions(+), 106 deletions(-) diff --git a/changelog.md b/changelog.md index 13db42f67b..2b93b75d58 100644 --- a/changelog.md +++ b/changelog.md @@ -44,6 +44,7 @@ * Added options for alternating price display (in badges). * #1515 Poll: Add result tab with a list of answers and customers for a poll * BMEcat: Added export and import of product tags. +* Santander instalment purchase. ### Improvements * (Perf) Significantly increased query performance for products with a lot of category assignments (> 10). diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs index 666c53dfdb..660e5f23a1 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs @@ -1,19 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Events; -using SmartStore.Core.Plugins; using SmartStore.Core.Domain.Orders; -using SmartStore.Services.Customers; +using SmartStore.Core.Plugins; using SmartStore.Services.Common; -using SmartStore.Services.Configuration; -using SmartStore.Collections; -using SmartStore.Core.Data.Hooks; +using SmartStore.Services.Customers; namespace SmartStore.Services.Discounts { @@ -28,21 +25,16 @@ public partial class DiscountService : IDiscountService private readonly IRequestCache _requestCache; private readonly IStoreContext _storeContext; private readonly IGenericAttributeService _genericAttributeService; - private readonly IPluginFinder _pluginFinder; - private readonly IEventPublisher _eventPublisher; - private readonly ISettingService _settingService; private readonly IProviderManager _providerManager; private readonly IDictionary _discountValidityCache; - public DiscountService(IRequestCache requestCache, + public DiscountService( + IRequestCache requestCache, IRepository discountRepository, IRepository discountRequirementRepository, IRepository discountUsageHistoryRepository, IStoreContext storeContext, IGenericAttributeService genericAttributeService, - IPluginFinder pluginFinder, - IEventPublisher eventPublisher, - ISettingService settingService, IProviderManager providerManager) { _requestCache = requestCache; @@ -51,9 +43,6 @@ public DiscountService(IRequestCache requestCache, _discountUsageHistoryRepository = discountUsageHistoryRepository; _storeContext = storeContext; _genericAttributeService = genericAttributeService; - _pluginFinder = pluginFinder; - _eventPublisher = eventPublisher; - _settingService = settingService; _providerManager = providerManager; _discountValidityCache = new Dictionary(); } @@ -207,7 +196,6 @@ public virtual Discount GetDiscountByCouponCode(string couponCode, bool showHidd return discount; } - private System.Diagnostics.Stopwatch _watch = new System.Diagnostics.Stopwatch(); public virtual bool IsDiscountValid(Discount discount, Customer customer) { var couponCodeToValidate = ""; diff --git a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperAdminProfile.cs b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperAdminProfile.cs index 068081f3dd..7833616bb4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperAdminProfile.cs +++ b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperAdminProfile.cs @@ -431,16 +431,19 @@ public AutoMapperAdminProfile() CreateMap() .ForMember(dest => dest.CreatedOnUtc, mo => mo.Ignore()) .ForMember(dest => dest.NewsLetterSubscriptionGuid, mo => mo.Ignore()) - .ForMember(dest => dest.StoreId, mo => mo.Ignore()); - //forums - CreateMap() - .ForMember(dest => dest.Locales, mo => mo.Ignore()) - .ForMember(dest => dest.SeName, mo => mo.MapFrom(src => src.GetSeName(0, true, false))) - .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) - .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) - .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()) - .ForMember(dest => dest.ForumModels, mo => mo.Ignore()); - CreateMap() + .ForMember(dest => dest.StoreId, mo => mo.Ignore()) + .ForMember(dest => dest.WorkingLanguageId, mo => mo.Ignore()); + //forums + CreateMap() + .ForMember(dest => dest.Locales, mo => mo.Ignore()) + .ForMember(dest => dest.SeName, mo => mo.MapFrom(src => src.GetSeName(0, true, false))) + .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) + .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableCustomerRoles, mo => mo.Ignore()) + .ForMember(dest => dest.SelectedCustomerRoleIds, mo => mo.Ignore()) + .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()) + .ForMember(dest => dest.ForumModels, mo => mo.Ignore()); + CreateMap() .ForMember(dest => dest.CreatedOnUtc, mo => mo.Ignore()) .ForMember(dest => dest.UpdatedOnUtc, mo => mo.Ignore()) .ForMember(dest => dest.Forums, mo => mo.Ignore()); @@ -499,6 +502,7 @@ public AutoMapperAdminProfile() .ForMember(dest => dest.EndDate, mo => mo.Ignore()) .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableLanguages, mo => mo.Ignore()) .ForMember(dest => dest.UsernamesEnabled, mo => mo.Ignore()) .ForMember(dest => dest.GridPageSize, mo => mo.Ignore()); CreateMap() diff --git a/src/Tests/SmartStore.Services.Tests/Discounts/DiscountServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Discounts/DiscountServiceTests.cs index 5c3fb0e389..44368552fb 100644 --- a/src/Tests/SmartStore.Services.Tests/Discounts/DiscountServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Discounts/DiscountServiceTests.cs @@ -1,21 +1,17 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; +using Rhino.Mocks; +using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Plugins; +using SmartStore.Core.Domain.Stores; +using SmartStore.Services.Common; using SmartStore.Services.Discounts; -using SmartStore.Core.Events; using SmartStore.Tests; -using NUnit.Framework; -using Rhino.Mocks; -using SmartStore.Core; -using SmartStore.Services.Common; -using SmartStore.Core.Domain.Common; -using SmartStore.Services.Configuration; -using SmartStore.Core.Domain.Stores; namespace SmartStore.Services.Tests.Discounts { @@ -25,11 +21,9 @@ public class DiscountServiceTests : ServiceTest IRepository _discountRepo; IRepository _discountRequirementRepo; IRepository _discountUsageHistoryRepo; - IEventPublisher _eventPublisher; IGenericAttributeService _genericAttributeService; IDiscountService _discountService; IStoreContext _storeContext; - ISettingService _settingService; [SetUp] public new void SetUp() @@ -62,9 +56,6 @@ public class DiscountServiceTests : ServiceTest _discountRepo.Expect(x => x.Table).Return(new List() { discount1, discount2 }.AsQueryable()); - _eventPublisher = MockRepository.GenerateMock(); - _eventPublisher.Expect(x => x.Publish(Arg.Is.Anything)); - _storeContext = MockRepository.GenerateMock(); _storeContext.Expect(x => x.CurrentStore).Return(new Store { @@ -72,17 +63,12 @@ public class DiscountServiceTests : ServiceTest Name = "MyStore" }); - _settingService = MockRepository.GenerateMock(); - - var cacheManager = new NullCache(); _discountRequirementRepo = MockRepository.GenerateMock>(); _discountUsageHistoryRepo = MockRepository.GenerateMock>(); - var pluginFinder = new PluginFinder(); _genericAttributeService = MockRepository.GenerateMock(); _discountService = new DiscountService(NullRequestCache.Instance, _discountRepo, _discountRequirementRepo, - _discountUsageHistoryRepo, _storeContext, _genericAttributeService, pluginFinder, _eventPublisher, - _settingService, base.ProviderManager); + _discountUsageHistoryRepo, _storeContext, _genericAttributeService, ProviderManager); } [Test] @@ -133,17 +119,8 @@ public void Should_accept_valid_discount_code() LastActivityDateUtc = new DateTime(2010, 01, 02) }; - _genericAttributeService.Expect(x => x.GetAttributesForEntity(customer.Id, "Customer")) - .Return(new List() - { - new GenericAttribute() - { - EntityId = customer.Id, - Key = SystemCustomerAttributeNames.DiscountCouponCode, - KeyGroup = "Customer", - Value = "CouponCode 1" - } - }); + _genericAttributeService.Expect(x => x.GetAttribute(nameof(Customer), customer.Id, SystemCustomerAttributeNames.DiscountCouponCode, 0)) + .Return("CouponCode 1"); var result1 = _discountService.IsDiscountValid(discount, customer); result1.ShouldEqual(true); @@ -174,29 +151,30 @@ public void Should_not_accept_wrong_discount_code() LastActivityDateUtc = new DateTime(2010, 01, 02) }; - _genericAttributeService.Expect(x => x.GetAttributesForEntity(customer.Id, "Customer")) - .Return(new List() - { - new GenericAttribute() - { - EntityId = customer.Id, - Key = SystemCustomerAttributeNames.DiscountCouponCode, - KeyGroup = "Customer", - Value = "CouponCode 2" - } - }); - - var result2 = _discountService.IsDiscountValid(discount, customer); + _genericAttributeService.Expect(x => x.GetAttribute(nameof(Customer), customer.Id, SystemCustomerAttributeNames.DiscountCouponCode, 0)) + .Return("CouponCode 2"); + + var result2 = _discountService.IsDiscountValid(discount, customer); result2.ShouldEqual(false); } [Test] public void Can_validate_discount_dateRange() { - var discount = new Discount + var customer = new Customer + { + CustomerGuid = Guid.NewGuid(), + AdminComment = "", + Active = true, + Deleted = false, + CreatedOnUtc = new DateTime(2010, 01, 01), + LastActivityDateUtc = new DateTime(2010, 01, 02) + }; + + var discount1 = new Discount { DiscountType = DiscountType.AssignedToSkus, - Name = "Discount 2", + Name = "Discount 1", UsePercentage = false, DiscountPercentage = 0, DiscountAmount = 5, @@ -206,21 +184,23 @@ public void Can_validate_discount_dateRange() DiscountLimitation = DiscountLimitationType.Unlimited, }; - var customer = new Customer + var discount2 = new Discount { - CustomerGuid = Guid.NewGuid(), - AdminComment = "", - Active = true, - Deleted = false, - CreatedOnUtc = new DateTime(2010, 01, 01), - LastActivityDateUtc = new DateTime(2010, 01, 02) + DiscountType = DiscountType.AssignedToSkus, + Name = "Discount 2", + UsePercentage = false, + DiscountPercentage = 0, + DiscountAmount = 5, + StartDateUtc = DateTime.UtcNow.AddDays(1), + EndDateUtc = DateTime.UtcNow.AddDays(2), + RequiresCouponCode = false, + DiscountLimitation = DiscountLimitationType.Unlimited, }; - var result1 = _discountService.IsDiscountValid(discount, customer); + var result1 = _discountService.IsDiscountValid(discount1, customer); result1.ShouldEqual(true); - discount.StartDateUtc = DateTime.UtcNow.AddDays(1); - var result2 = _discountService.IsDiscountValid(discount, customer); + var result2 = _discountService.IsDiscountValid(discount2, customer); result2.ShouldEqual(false); } } diff --git a/src/Tests/SmartStore.Services.Tests/Helpers/DateTimeHelperTests.cs b/src/Tests/SmartStore.Services.Tests/Helpers/DateTimeHelperTests.cs index 29160fe4b9..ed5a793f53 100644 --- a/src/Tests/SmartStore.Services.Tests/Helpers/DateTimeHelperTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Helpers/DateTimeHelperTests.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; +using NUnit.Framework; +using Rhino.Mocks; using SmartStore.Core; +using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Domain.Common; +using SmartStore.Services.Common; using SmartStore.Services.Configuration; -using SmartStore.Services.Customers; using SmartStore.Services.Helpers; -using SmartStore.Services.Common; using SmartStore.Tests; -using NUnit.Framework; -using Rhino.Mocks; namespace SmartStore.Services.Tests.Helpers { @@ -33,18 +32,17 @@ public class DateTimeHelperTests : ServiceTest _workContext = MockRepository.GenerateMock(); - _store = new Store() { Id = 1 }; + _store = new Store { Id = 1 }; _storeContext = MockRepository.GenerateMock(); _storeContext.Expect(x => x.CurrentStore).Return(_store); - _dateTimeSettings = new DateTimeSettings() + _dateTimeSettings = new DateTimeSettings { AllowCustomersToSetTimeZone = false, DefaultStoreTimeZoneId = "" }; - _dateTimeHelper = new DateTimeHelper(_workContext, _genericAttributeService, - _settingService, _dateTimeSettings); + _dateTimeHelper = new DateTimeHelper(_workContext, _genericAttributeService, _settingService, _dateTimeSettings); } [Test] @@ -67,25 +65,16 @@ public void Can_get_all_systemTimeZones() public void Can_get_customer_timeZone_with_customTimeZones_enabled() { _dateTimeSettings.AllowCustomersToSetTimeZone = true; - _dateTimeSettings.DefaultStoreTimeZoneId = "E. Europe Standard Time"; //(GMT+02:00) Minsk; + _dateTimeSettings.DefaultStoreTimeZoneId = "E. Europe Standard Time"; // (GMT+02:00) Minsk; - var customer = new Customer() + var customer = new Customer { Id = 10 }; - _genericAttributeService.Expect(x => x.GetAttributesForEntity(customer.Id, "Customer")) - .Return(new List() - { - new GenericAttribute() - { - StoreId = 0, - EntityId = customer.Id, - Key = SystemCustomerAttributeNames.TimeZoneId, - KeyGroup = "Customer", - Value = "Russian Standard Time" //(GMT+03:00) Moscow, St. Petersburg, Volgograd - } - }); + _genericAttributeService + .Expect(x => x.GetAttribute(nameof(Customer), customer.Id, SystemCustomerAttributeNames.TimeZoneId, 0)) + .Return("Russian Standard Time"); // (GMT+03:00) Moscow, St. Petersburg, Volgograd var timeZone = _dateTimeHelper.GetCustomerTimeZone(customer); timeZone.ShouldNotBeNull(); From 7fe1ffbd9690b4b82f283ab75d22e8a46cdde2de Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Fri, 8 Feb 2019 17:35:10 +0100 Subject: [PATCH 111/657] Resolves #1569 Show login note if no prices are displayed due to customer group permissions --- changelog.md | 1 + .../Domain/Catalog/CatalogSettings.cs | 15 ++- .../Migrations/MigrationsConfiguration.cs | 10 ++ .../Security/PermissionService.cs | 52 +++++---- .../Models/Settings/CatalogSettingsModel.cs | 3 + .../Views/Setting/Catalog.cshtml | 9 ++ .../Controllers/CatalogHelper.cs | 86 +++++++-------- .../Models/Catalog/ProductDetailsModel.cs | 3 +- .../Themes/Flex/Content/_product.scss | 3 +- .../Partials/Product.Offer.Price.cshtml | 103 +++++++++--------- .../Product/Partials/Product.Offer.cshtml | 4 +- 11 files changed, 154 insertions(+), 135 deletions(-) diff --git a/changelog.md b/changelog.md index 2b93b75d58..2a54b1a8c6 100644 --- a/changelog.md +++ b/changelog.md @@ -66,6 +66,7 @@ * OpenTrans: added customer number to parties * Do not filter cookie using resources if cookie usage has not yet been consented to. * #1563 QueuedMessagesClearTask: add a setting for the age of the mails to be deleted. +* #1569 Added a setting to show login note if no prices are displayed due to customer group permissions. ### Bugfixes * In a multi-store environment, multiple topics with the same system name cannot be resolved reliably. diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index c6d208659f..394e9e4e9e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -68,7 +68,7 @@ public CatalogSettings() ShowColorSquaresInLists = true; ShowDiscountSign = true; ShowVariantCombinationPriceAdjustment = true; - ShowLinkedAttributeValueImage = true; + ShowLinkedAttributeValueImage = true; EnableDynamicPriceUpdate = true; ShowProductReviewsInProductDetail = true; EnableHtmlTextCollapser = true; @@ -129,10 +129,15 @@ public CatalogSettings() /// public bool ShowVariantCombinationPriceAdjustment { get; set; } - /// - /// Gets or sets a value indicating whether to display quantity of linked product at attribute values - /// - public bool ShowLinkedAttributeValueQuantity { get; set; } + /// + /// Indicates whether to show a login note if the user is not authorized to see prices. + /// + public bool ShowLoginForPriceNote { get; set; } + + /// + /// Gets or sets a value indicating whether to display quantity of linked product at attribute values + /// + public bool ShowLinkedAttributeValueQuantity { get; set; } /// /// Gets or sets a value indicating whether to display the image of linked product at attribute values diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 2fc48aa446..977bc6ccb4 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -654,6 +654,16 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.Delete("Admin.Customers.Customers.List.SearchDeletedOnly"); builder.AddOrUpdate("Admin.Customers.Customers.List.SearchActiveOnly", "Only activated customers", "Nur aktivierte Kunden"); + + builder.AddOrUpdate("Products.LoginForPrice", + "Prices will be displayed after login.", + "Preise werden nach Anmeldung angezeigt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowLoginForPriceNote", + "Show login for price note", + "Hinweis \"Preis nach Anmeldung\" anzeigen", + "Specifies whether to display a message stating that prices will not be displayed until login.", + "Legt fest, ob ein Hinweis erscheinen soll, dass Preise erst nach Anmeldung angezeigt werden."); } } } diff --git a/src/Libraries/SmartStore.Services/Security/PermissionService.cs b/src/Libraries/SmartStore.Services/Security/PermissionService.cs index 16d0fa0ac0..dec98fd0fe 100644 --- a/src/Libraries/SmartStore.Services/Security/PermissionService.cs +++ b/src/Libraries/SmartStore.Services/Security/PermissionService.cs @@ -10,12 +10,11 @@ namespace SmartStore.Services.Security { - /// - /// Permission service - /// - public partial class PermissionService : IPermissionService + /// + /// Permission service + /// + public partial class PermissionService : IPermissionService { - #region Constants /// /// Cache key for storing a valie indicating whether a certain customer role has a permission /// @@ -25,20 +24,12 @@ public partial class PermissionService : IPermissionService /// private const string PERMISSIONS_ALLOWED_KEY = "permission:allowed-{0}-{1}"; private const string PERMISSIONS_PATTERN_KEY = "permission:*"; - #endregion - - #region Fields private readonly IRepository _permissionRecordRepository; - private readonly IRepository _customerRoleRepository; private readonly ICustomerService _customerService; private readonly IWorkContext _workContext; private readonly ICacheManager _cacheManager; - #endregion - - #region Ctor - /// /// Ctor /// @@ -48,19 +39,16 @@ public partial class PermissionService : IPermissionService /// Cache manager public PermissionService( IRepository permissionRecordRepository, - IRepository customerRoleRepository, ICustomerService customerService, - IWorkContext workContext, ICacheManager cacheManager) + IWorkContext workContext, + ICacheManager cacheManager) { - this._permissionRecordRepository = permissionRecordRepository; - this._customerRoleRepository = customerRoleRepository; - this._customerService = customerService; - this._workContext = workContext; - this._cacheManager = cacheManager; + _permissionRecordRepository = permissionRecordRepository; + _customerService = customerService; + _workContext = workContext; + _cacheManager = cacheManager; } - #endregion - #region Utilities /// @@ -71,15 +59,21 @@ public PermissionService( /// true - authorized; otherwise, false protected virtual bool Authorize(string permissionRecordSystemName, CustomerRole customerRole) { - if (String.IsNullOrEmpty(permissionRecordSystemName)) + if (string.IsNullOrEmpty(permissionRecordSystemName)) + { return false; + } - string key = string.Format(PERMISSIONS_ALLOWED_KEY, customerRole.Id, permissionRecordSystemName); + var key = string.Format(PERMISSIONS_ALLOWED_KEY, customerRole.Id, permissionRecordSystemName); return _cacheManager.Get(key, () => { foreach (var permission1 in customerRole.PermissionRecords) + { if (permission1.SystemName.Equals(permissionRecordSystemName, StringComparison.InvariantCultureIgnoreCase)) + { return true; + } + } return false; }); @@ -311,16 +305,20 @@ public virtual bool Authorize(string permissionRecordSystemName) /// true - authorized; otherwise, false public virtual bool Authorize(string permissionRecordSystemName, Customer customer) { - if (String.IsNullOrEmpty(permissionRecordSystemName)) + if (string.IsNullOrEmpty(permissionRecordSystemName)) + { return false; + } var customerRoles = customer.CustomerRoles.Where(cr => cr.Active); foreach (var role in customerRoles) + { if (Authorize(permissionRecordSystemName, role)) - //yes, we have such permission + { return true; + } + } - //no permission found return false; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs index 8c2e1acf84..d842fbdda7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs @@ -300,6 +300,9 @@ public CatalogSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowVariantCombinationPriceAdjustment")] public bool ShowVariantCombinationPriceAdjustment { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowLoginForPriceNote")] + public bool ShowLoginForPriceNote { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowLinkedAttributeValueQuantity")] public bool ShowLinkedAttributeValueQuantity { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml index 44f9f4db7f..b0cdb0d98e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml @@ -799,6 +799,15 @@ @Html.ValidationMessageFor(model => model.ShowVariantCombinationPriceAdjustment) +
    + + +
    - + @T("Plugins.Payments.AmazonPay.RegisterNow")
    @Model.FileNamePatternExample @@ -144,7 +144,7 @@ @deployment.Name
    -  @deployment.DeploymentTypeName +  @deployment.DeploymentTypeName
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForum.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForum.cshtml index 29a844c8a7..11fa1679fa 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForum.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForum.cshtml @@ -20,7 +20,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForumGroup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForumGroup.cshtml index de3588e0f0..f0b3ca5909 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForumGroup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Forum/EditForumGroup.cshtml @@ -20,7 +20,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/Edit.cshtml index 640f4f3bdc..22da56cf86 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/Edit.cshtml @@ -18,7 +18,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/_CreateOrUpdate.cshtml index 06a72ac536..14c43c7f65 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/_CreateOrUpdate.cshtml @@ -116,7 +116,7 @@
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Home/Index.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Home/Index.cshtml index f8c5f0b314..750aa84167 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Home/Index.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Home/Index.cshtml @@ -4,7 +4,7 @@
    - + @T("Admin.Dashboard.StoreStatistics")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Home/UaTester.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Home/UaTester.cshtml index f4c0bc92fb..95af86f0b8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Home/UaTester.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Home/UaTester.cshtml @@ -7,7 +7,7 @@
    - + User Agent Tester
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml index f4f7e253bd..0889579148 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml @@ -17,14 +17,14 @@ @if (Model.Id != 0) { } @if (Model.LogFileExists) { - + @T("Admin.Configuration.ActivityLog") } @@ -36,7 +36,7 @@ @T("Admin.Common.SaveContinue") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml index a01b8aa3c2..2f1d5fdc18 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml @@ -93,7 +93,7 @@ - + @T("Admin.Configuration.ActivityLog")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml index acdf321841..d5ec3dcfa6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml @@ -17,7 +17,7 @@ @{ Html.RenderWidget("admin_button_toolbar_before"); } - + @T("Admin.Catalog.Products.Copy") @@ -89,7 +89,7 @@ ) .FooterContent(@ ) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml index e12b6bd09d..1fe243f910 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml @@ -35,7 +35,7 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/LowStockReport.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/LowStockReport.cshtml index 9e2ffae784..81921f53e4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/LowStockReport.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/LowStockReport.cshtml @@ -13,7 +13,7 @@ {
    - + @T("Admin.Catalog.LowStockReport")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Attributes.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Attributes.cshtml index cafb1db22b..b94502e231 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Attributes.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Attributes.cshtml @@ -197,7 +197,7 @@ .Centered(); columns.Bound(x => x.AllowOutOfStockOrders) .Width(60) - .HeaderTemplate("") + .HeaderTemplate("") .Template(item => @Html.SymbolForBool(item.AllowOutOfStockOrders)) .ClientTemplate(@Html.SymbolForBool("AllowOutOfStockOrders")) .Centered(); @@ -254,7 +254,7 @@ } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Downloads.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Downloads.cshtml index 9850e54922..101f7681e8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Downloads.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Downloads.cshtml @@ -102,7 +102,7 @@ - +
    - + @string.Format(@T("Account.CustomerOrders.RecurringOrders.ViewInitialOrder").Text, item.InitialOrderId) @specificationAttribute.SpecificationAttributeName - @if (specValue != null && specValue.Value.HasValue()) - { - @specValue - } - else - { -   - } + @foreach (var spec in foundProductSpecs) + { + var specValue = spec != null ? spec.SpecificationAttributeOption : null; + + if (specValue != null && specValue.Value.HasValue()) + { +
    @specValue
    + } + else + { +
     
    + } + }
    + @Html.SmartLabelFor(model => model.ShowLoginForPriceNote) + + @Html.SettingEditorFor(model => model.ShowLoginForPriceNote) + @Html.ValidationMessageFor(model => model.ShowLoginForPriceNote) +
    @Html.SmartLabelFor(model => model.ShowLinkedAttributeValueQuantity) diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs index 4fffd68631..b563a34d37 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs @@ -634,36 +634,34 @@ public ProductDetailsModel PrepareProductDetailModel( IList productBundleItems = null, int selectedQuantity = 1) { - if (product == null) - throw new ArgumentNullException("product"); - - if (model == null) - throw new ArgumentNullException("model"); + Guard.NotNull(model, nameof(model)); + Guard.NotNull(product, nameof(product)); var store = _services.StoreContext.CurrentStore; var customer = _services.WorkContext.CurrentCustomer; var currency = _services.WorkContext.WorkingCurrency; - decimal preSelectedPriceAdjustmentBase = decimal.Zero; - decimal preSelectedWeightAdjustment = decimal.Zero; - bool displayPrices = _services.Permissions.Authorize(StandardPermissionProvider.DisplayPrices); - bool isBundle = (product.ProductType == ProductType.BundledProduct); - bool isBundleItemPricing = (productBundleItem != null && productBundleItem.Item.BundleProduct.BundlePerItemPricing); - bool isBundlePricing = (productBundleItem != null && !productBundleItem.Item.BundleProduct.BundlePerItemPricing); - int bundleItemId = (productBundleItem == null ? 0 : productBundleItem.Item.Id); + var preSelectedPriceAdjustmentBase = decimal.Zero; + var preSelectedWeightAdjustment = decimal.Zero; + var displayPrices = _services.Permissions.Authorize(StandardPermissionProvider.DisplayPrices); + var isBundle = product.ProductType == ProductType.BundledProduct; + var isBundleItemPricing = productBundleItem != null && productBundleItem.Item.BundleProduct.BundlePerItemPricing; + var isBundlePricing = productBundleItem != null && !productBundleItem.Item.BundleProduct.BundlePerItemPricing; + var bundleItemId = productBundleItem == null ? 0 : productBundleItem.Item.Id; - bool hasSelectedAttributesValues = false; - bool hasSelectedAttributes = query.Variants.Count > 0; + var hasSelectedAttributesValues = false; + var hasSelectedAttributes = query.Variants.Count > 0; List selectedAttributeValues = null; - - var variantAttributes = (isBundle ? new List() : _productAttributeService.GetProductVariantAttributesByProductId(product.Id)); + var variantAttributes = isBundle ? new List() : _productAttributeService.GetProductVariantAttributesByProductId(product.Id); model.IsBundlePart = product.ProductType != ProductType.BundledProduct && productBundleItem != null; model.ProductPrice.DynamicPriceUpdate = _catalogSettings.EnableDynamicPriceUpdate; model.ProductPrice.BundleItemShowBasePrice = _catalogSettings.BundleItemShowBasePrice; - if (!model.ProductPrice.DynamicPriceUpdate) - selectedQuantity = 1; + if (!model.ProductPrice.DynamicPriceUpdate) + { + selectedQuantity = 1; + } #region Product attributes @@ -1075,11 +1073,11 @@ public ProductDetailsModel PrepareProductDetailModel( #region Product price model.ProductPrice.ProductId = product.Id; + model.ProductPrice.HidePrices = !displayPrices; + model.ProductPrice.ShowLoginNote = !displayPrices && productBundleItem == null && _catalogSettings.ShowLoginForPriceNote; - if (displayPrices) + if (displayPrices) { - model.ProductPrice.HidePrices = false; - if (product.CustomerEntersPrice && !isBundleItemPricing) { model.ProductPrice.CustomerEntersPrice = true; @@ -1092,26 +1090,25 @@ public ProductDetailsModel PrepareProductDetailModel( } else { - decimal taxRate = decimal.Zero; - decimal oldPrice = decimal.Zero; - decimal finalPriceWithoutDiscountBase = decimal.Zero; - decimal finalPriceWithDiscountBase = decimal.Zero; - decimal attributesTotalPriceBase = decimal.Zero; - decimal attributesTotalPriceBaseOrig = decimal.Zero; - decimal finalPriceWithoutDiscount = decimal.Zero; - decimal finalPriceWithDiscount = decimal.Zero; - - decimal oldPriceBase = _taxService.GetProductPrice(product, product.OldPrice, out taxRate); + var taxRate = decimal.Zero; + var oldPrice = decimal.Zero; + var finalPriceWithoutDiscountBase = decimal.Zero; + var finalPriceWithDiscountBase = decimal.Zero; + var attributesTotalPriceBase = decimal.Zero; + var attributesTotalPriceBaseOrig = decimal.Zero; + var finalPriceWithoutDiscount = decimal.Zero; + var finalPriceWithDiscount = decimal.Zero; + var oldPriceBase = _taxService.GetProductPrice(product, product.OldPrice, out taxRate); if (model.ProductPrice.DynamicPriceUpdate && !isBundlePricing) { if (selectedAttributeValues != null) { selectedAttributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.GetProductVariantAttributeValuePriceAdjustment(x, - product, _services.WorkContext.CurrentCustomer, null, selectedQuantity)); + product, customer, null, selectedQuantity)); selectedAttributeValues.Each(x => attributesTotalPriceBaseOrig += _priceCalculationService.GetProductVariantAttributeValuePriceAdjustment(x, - product, _services.WorkContext.CurrentCustomer, null, 1)); + product, customer, null, 1)); } else { @@ -1208,36 +1205,32 @@ public ProductDetailsModel PrepareProductDetailModel( } else { - model.ProductPrice.HidePrices = true; - model.ProductPrice.OldPrice = null; + model.ProductPrice.OldPrice = null; model.ProductPrice.Price = null; } + #endregion #region 'Add to cart' model model.AddToCart.ProductId = product.Id; - - //quantity model.AddToCart.EnteredQuantity = product.OrderMinimumQuantity; - model.AddToCart.MinOrderAmount = product.OrderMinimumQuantity; model.AddToCart.MaxOrderAmount = product.OrderMaximumQuantity; model.AddToCart.QuantityUnitName = model.QuantityUnitName; // TODO: (mc) remove 'QuantityUnitName' from parent model later model.AddToCart.QuantityStep = product.QuantityStep > 0 ? product.QuantityStep : 1; model.AddToCart.HideQuantityControl = product.HideQuantityControl; model.AddToCart.QuantiyControlType = product.QuantiyControlType; + model.AddToCart.AvailableForPreOrder = product.AvailableForPreOrder; - //'add to cart', 'add to wishlist' buttons - model.AddToCart.DisableBuyButton = !displayPrices || product.DisableBuyButton || !_services.Permissions.Authorize(StandardPermissionProvider.EnableShoppingCart); - model.AddToCart.DisableWishlistButton = !displayPrices || product.DisableWishlistButton - || !_services.Permissions.Authorize(StandardPermissionProvider.EnableWishlist) - || product.ProductType == ProductType.GroupedProduct; + // 'add to cart', 'add to wishlist' buttons. + model.AddToCart.DisableBuyButton = !displayPrices || product.DisableBuyButton || + !_services.Permissions.Authorize(StandardPermissionProvider.EnableShoppingCart); - //pre-order - model.AddToCart.AvailableForPreOrder = product.AvailableForPreOrder; + model.AddToCart.DisableWishlistButton = !displayPrices || product.DisableWishlistButton + || product.ProductType == ProductType.GroupedProduct + || !_services.Permissions.Authorize(StandardPermissionProvider.EnableWishlist); - //customer entered price model.AddToCart.CustomerEntersPrice = product.CustomerEntersPrice; if (model.AddToCart.CustomerEntersPrice) { @@ -1251,7 +1244,6 @@ public ProductDetailsModel PrepareProductDetailModel( _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, true, false)); } - //allowed quantities var allowedQuantities = product.ParseAllowedQuatities(); foreach (var qty in allowedQuantities) { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs index 0eca3e49ef..dd24d6b44e 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs @@ -200,8 +200,9 @@ public partial class ProductPriceModel : ModelBase public int ProductId { get; set; } public bool HidePrices { get; set; } + public bool ShowLoginNote { get; set; } - public bool DynamicPriceUpdate { get; set; } + public bool DynamicPriceUpdate { get; set; } public bool BundleItemShowBasePrice { get; set; } public string NoteWithDiscount { get; set; } diff --git a/src/Presentation/SmartStore.Web/Themes/Flex/Content/_product.scss b/src/Presentation/SmartStore.Web/Themes/Flex/Content/_product.scss index 61eaf65e91..f98a98a2dd 100644 --- a/src/Presentation/SmartStore.Web/Themes/Flex/Content/_product.scss +++ b/src/Presentation/SmartStore.Web/Themes/Flex/Content/_product.scss @@ -240,7 +240,8 @@ $pd-offer-spacing: $grid-gutter-width / 2; color: $danger; } -.pd-callforprice { +.pd-callforprice, +.pd-loginforprice { font-size: 1.2rem; } diff --git a/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.Price.cshtml b/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.Price.cshtml index 082bb6b0e5..aaf802985b 100644 --- a/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.Price.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.Price.cshtml @@ -6,71 +6,70 @@ @using SmartStore.Core.Domain.Catalog; @{ - if (Model.AddToCart.DisableBuyButton && Model.AddToCart.DisableWishlistButton) + if (Model.AddToCart.DisableBuyButton && Model.AddToCart.DisableWishlistButton && !Model.ProductPrice.ShowLoginNote) { return; } +} - var isBundleItem = Model.IsBundlePart; - var priceModel = Model.ProductPrice; - - var currentPrice = priceModel.PriceWithDiscount.NullEmpty() ?? priceModel.Price; - var currentPriceValue = priceModel.PriceWithDiscountValue > 0 ? priceModel.PriceWithDiscountValue : priceModel.PriceValue; - var currentPriceLabel = priceModel.NoteWithDiscount; +
    + @if (Model.ProductPrice.ShowLoginNote) + { +

    @T("Products.LoginForPrice")

    + } + else if (Model.ProductPrice.CallForPrice) + { +

    @T("Products.CallForPrice")

    + } + else + { + var isBundleItem = Model.IsBundlePart; + var priceModel = Model.ProductPrice; + var currentPrice = priceModel.PriceWithDiscount.NullEmpty() ?? priceModel.Price; + var currentPriceValue = priceModel.PriceWithDiscountValue > 0 ? priceModel.PriceWithDiscountValue : priceModel.PriceValue; + var currentPriceLabel = priceModel.NoteWithDiscount; - var oldPrice1 = priceModel.OldPrice; - var oldPrice1Label = T("Products.Price.OldPrice").Text; + var oldPrice1 = priceModel.OldPrice; + var oldPrice1Label = T("Products.Price.OldPrice").Text; - var oldPrice2 = priceModel.PriceWithDiscountValue < priceModel.PriceValue ? priceModel.Price : ""; - var oldPrice2Label = priceModel.NoteWithoutDiscount; + var oldPrice2 = priceModel.PriceWithDiscountValue < priceModel.PriceValue ? priceModel.Price : ""; + var oldPrice2Label = priceModel.NoteWithoutDiscount; - var basePriceEnabled = isBundleItem ? priceModel.BundleItemShowBasePrice : Model.IsBasePriceEnabled; + var basePriceEnabled = isBundleItem ? priceModel.BundleItemShowBasePrice : Model.IsBasePriceEnabled; - if (oldPrice2.HasValue() && !oldPrice1.HasValue()) - { - oldPrice1 = oldPrice2; - oldPrice2 = null; - oldPrice2Label = null; - } + if (oldPrice2.HasValue() && !oldPrice1.HasValue()) + { + oldPrice1 = oldPrice2; + oldPrice2 = null; + oldPrice2Label = null; + } - if (priceModel.PriceValue == 0 && Model.DisplayTextForZeroPrices) - { - currentPrice = T("Products.Free").Text; - oldPrice1 = null; - oldPrice2 = null; - oldPrice2Label = null; - } + if (priceModel.PriceValue == 0 && Model.DisplayTextForZeroPrices) + { + currentPrice = T("Products.Free").Text; + oldPrice1 = null; + oldPrice2 = null; + oldPrice2Label = null; + } - var blocksHaveLabel = currentPriceLabel.HasValue() && oldPrice2Label.HasValue(); + var blocksHaveLabel = currentPriceLabel.HasValue() && oldPrice2Label.HasValue(); - var artPriceClasses = "pd-price"; - if (isBundleItem) - { - artPriceClasses += " pd-price-sm"; - } - if (priceModel.OldPrice.HasValue()) - { - artPriceClasses += " pd-price--offer"; - } - if (priceModel.CallForPrice) - { - artPriceClasses += " pd-price--call"; - } + var artPriceClasses = "pd-price"; + if (isBundleItem) + { + artPriceClasses += " pd-price-sm"; + } + if (priceModel.OldPrice.HasValue()) + { + artPriceClasses += " pd-price--offer"; + } - var badgeClasses = ""; - if (Model.PriceDisplayStyle == PriceDisplayStyle.BadgeAll || (Model.PriceDisplayStyle == PriceDisplayStyle.BadgeFreeProductsOnly && priceModel.PriceValue == 0)) - { - badgeClasses = Model.PriceDisplayStyle == PriceDisplayStyle.BadgeAll && priceModel.SavingAmount.HasValue() ? "text-danger" : "text-success"; - } -} + var badgeClasses = ""; + if (Model.PriceDisplayStyle == PriceDisplayStyle.BadgeAll || (Model.PriceDisplayStyle == PriceDisplayStyle.BadgeFreeProductsOnly && priceModel.PriceValue == 0)) + { + badgeClasses = Model.PriceDisplayStyle == PriceDisplayStyle.BadgeAll && priceModel.SavingAmount.HasValue() ? "text-danger" : "text-success"; + } -
    - @if (priceModel.CallForPrice) - { -

    @T("Products.CallForPrice")

    - } - else - {
    diff --git a/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.cshtml b/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.cshtml index 38c74770a6..2f3a25865e 100644 --- a/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Product/Partials/Product.Offer.cshtml @@ -7,9 +7,9 @@
    - @if (!Model.AddToCart.DisableBuyButton || !Model.AddToCart.DisableWishlistButton) + @if (!Model.AddToCart.DisableBuyButton || !Model.AddToCart.DisableWishlistButton || Model.ProductPrice.ShowLoginNote) { - if (Model.AddToCart.CustomerEntersPrice) + if (Model.AddToCart.CustomerEntersPrice && !Model.ProductPrice.HidePrices) { var dataDictCustomerPrice = new ViewDataDictionary(); dataDictCustomerPrice.TemplateInfo.HtmlFieldPrefix = string.Format("addtocart_{0}", Model.Id); From c2ff8a41f62c05b3dd4e0a1d1c952115d01b842e Mon Sep 17 00:00:00 2001 From: Marcus Gesing Date: Mon, 11 Feb 2019 15:18:18 +0100 Subject: [PATCH 112/657] Link builder --- .../Migrations/MigrationsConfiguration.cs | 2 + .../Components/EntityPicker/EntityPicker.cs | 8 +- .../EntityPicker/EntityPickerBuilder.cs | 16 +- .../Controllers/EntityController.cs | 344 ++++++++++-------- .../Models/Entity/EntityPickerModel.cs | 14 +- .../Scripts/smartstore.entityPicker.js | 54 +-- .../Views/Entity/Partials/Picker.List.cshtml | 29 +- .../Views/Entity/Partials/Picker.cshtml | 3 +- .../Shared/Components/EntityPicker.cshtml | 6 +- .../Views/Shared/EditorTemplates/Link.cshtml | 125 +++++-- 10 files changed, 363 insertions(+), 238 deletions(-) diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 977bc6ccb4..620cabff20 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -642,6 +642,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Click Download to install a new language including all localized resources. On translate.smartstore.com you will find more details about available resources.", "Klicken Sie auf Download, um eine neue Sprache mit allen lokalisierten Ressourcen zu installieren. Auf translate.smartstore.com finden Sie weitere Details zu verfügbaren Ressourcen."); + builder.AddOrUpdate("Common.Url", "URL", "URL"); + builder.AddOrUpdate("Common.File", "File", "Datei"); builder.AddOrUpdate("Common.Entity.Product", "Product", "Produkt"); builder.AddOrUpdate("Common.Entity.Category", "Category", "Warengruppe"); builder.AddOrUpdate("Common.Entity.Manufacturer", "Manufacturer", "Hersteller"); diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs index 161f88beca..fcca6a595b 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs @@ -1,5 +1,4 @@ -using System; -namespace SmartStore.Web.Framework.UI +namespace SmartStore.Web.Framework.UI { public class EntityPicker : Component { @@ -16,8 +15,9 @@ public EntityPicker() } public string EntityType { get; set; } + public int LanguageId { get; set; } - public string TargetInputSelector + public string TargetInputSelector { get { return HtmlAttributes["data-target"] as string; } set { HtmlAttributes["data-target"] = value; } @@ -32,7 +32,7 @@ public string TargetInputSelector public bool DisableGroupedProducts { get; set; } public bool DisableBundleProducts { get; set; } public int[] DisabledEntityIds { get; set; } - public int[] SelectedEntityIds { get; set; } + public string[] Selected { get; set; } public bool EnableThumbZoomer { get; set; } public bool HighlightSearchTerm { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs index a7b8503942..78b3d4867b 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -using System.Text; using System.Web.Mvc; using System.Web.Routing; -using System.Web.WebPages; namespace SmartStore.Web.Framework.UI { @@ -31,7 +27,13 @@ public EntityPickerBuilder EntityType(string value) return this; } - public EntityPickerBuilder Caption(string value) + public EntityPickerBuilder LanguageId(int value) + { + base.Component.LanguageId = value; + return this; + } + + public EntityPickerBuilder Caption(string value) { base.Component.Caption = value; return this; @@ -94,9 +96,9 @@ public EntityPickerBuilder DisabledEntityIds(params int[] values) return this; } - public EntityPickerBuilder SelectedEntityIds(params int[] values) + public EntityPickerBuilder Selected(params string[] values) { - base.Component.SelectedEntityIds = values; + base.Component.Selected = values; return this; } diff --git a/src/Presentation/SmartStore.Web/Controllers/EntityController.cs b/src/Presentation/SmartStore.Web/Controllers/EntityController.cs index 4c32bd524f..84c6ce1e7d 100644 --- a/src/Presentation/SmartStore.Web/Controllers/EntityController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/EntityController.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Topics; using SmartStore.Core.Search; using SmartStore.Services.Catalog; using SmartStore.Services.Customers; @@ -14,6 +15,7 @@ using SmartStore.Services.Search; using SmartStore.Services.Security; using SmartStore.Services.Seo; +using SmartStore.Services.Topics; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Models.Entity; @@ -31,8 +33,9 @@ public partial class EntityController : PublicControllerBase private readonly ICustomerService _customerService; private readonly ICategoryService _categoryService; private readonly IUrlRecordService _urlRecordService; + private readonly ITopicService _topicService; - public EntityController( + public EntityController( ICatalogSearchService catalogSearchService, CatalogSettings catalogSettings, MediaSettings mediaSettings, @@ -41,7 +44,8 @@ public EntityController( IManufacturerService manufacturerService, ICustomerService customerService, ICategoryService categoryService, - IUrlRecordService urlRecordService) + IUrlRecordService urlRecordService, + ITopicService topicService) { _catalogSearchService = catalogSearchService; _catalogSettings = catalogSettings; @@ -52,14 +56,13 @@ public EntityController( _customerService = customerService; _categoryService = categoryService; _urlRecordService = urlRecordService; + _topicService = topicService; } #region Entity Picker public ActionResult Picker(EntityPickerModel model) { - model.PageSize = 96; - if (model.EntityType.IsCaseInsensitiveEqual("product")) { ViewBag.AvailableCategories = _categoryService.GetCategoryTree(includeHidden: true) @@ -79,7 +82,8 @@ public ActionResult Picker(EntityPickerModel model) } else if (model.EntityType.IsCaseInsensitiveEqual("customer")) { - ViewBag.AvailableCustomerSearchTypes = new List { + ViewBag.AvailableCustomerSearchTypes = new List + { new SelectListItem { Text = "Name", Value = "Name", Selected = true }, new SelectListItem { Text = "Email", Value = "Email" } }; @@ -96,87 +100,87 @@ public ActionResult Picker(EntityPickerModel model) [HttpPost] public ActionResult Picker(EntityPickerModel model, FormCollection form) { - model.PageSize = 96; - try { - var language = Services.WorkContext.WorkingLanguage; - var disableIf = model.DisableIf.SplitSafe(",").Select(x => x.ToLower().Trim()).ToList(); + var languageId = model.LanguageId == 0 ? Services.WorkContext.WorkingLanguage.Id : model.LanguageId; + var disableIf = model.DisableIf.SplitSafe(",").Select(x => x.ToLower().Trim()).ToList(); var disableIds = model.DisableIds.SplitSafe(",").Select(x => x.ToInt()).ToList(); - var selIds = new HashSet(model.PreselectedEntityIds.ToIntArray()); + var selected = model.Selected.SplitSafe(","); + var returnLink = model.ReturnField.IsCaseInsensitiveEqual("link"); + var returnSku = model.ReturnField.IsCaseInsensitiveEqual("sku"); - using (var scope = new DbContextScope(Services.DbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) + using (var scope = new DbContextScope(Services.DbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) { - if (model.EntityType.IsCaseInsensitiveEqual("product")) - { - model.SearchTerm = model.SearchTerm.TrimSafe(); - - var hasPermission = Services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog); - var disableIfNotSimpleProduct = disableIf.Contains("notsimpleproduct"); - var disableIfGroupedProduct = disableIf.Contains("groupedproduct"); - var labelTextGrouped = T("Admin.Catalog.Products.ProductType.GroupedProduct.Label").Text; - var labelTextBundled = T("Admin.Catalog.Products.ProductType.BundledProduct.Label").Text; - var sku = T("Products.Sku").Text; - - var fields = new List { "name" }; - if (_searchSettings.SearchFields.Contains("sku")) - fields.Add("sku"); - if (_searchSettings.SearchFields.Contains("shortdescription")) - fields.Add("shortdescription"); - - var searchQuery = new CatalogSearchQuery(fields.ToArray(), model.SearchTerm) - .HasStoreId(model.StoreId); - - if (!hasPermission) - { - searchQuery = searchQuery.VisibleOnly(Services.WorkContext.CurrentCustomer); - } - - if (model.ProductTypeId > 0) - { - searchQuery = searchQuery.IsProductType((ProductType)model.ProductTypeId); - } - - if (model.ManufacturerId != 0) - { - searchQuery = searchQuery.WithManufacturerIds(null, model.ManufacturerId); - } - - if (model.CategoryId != 0) - { - var node = _categoryService.GetCategoryTree(model.CategoryId, true); - if (node != null) - { - searchQuery = searchQuery.WithCategoryIds(null, node.Flatten(true).Select(x => x.Id).ToArray()); - } - } + if (model.EntityType.IsCaseInsensitiveEqual("product")) + { + model.SearchTerm = model.SearchTerm.TrimSafe(); + + var hasPermission = Services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog); + var disableIfNotSimpleProduct = disableIf.Contains("notsimpleproduct"); + var disableIfGroupedProduct = disableIf.Contains("groupedproduct"); + var labelTextGrouped = T("Admin.Catalog.Products.ProductType.GroupedProduct.Label").Text; + var labelTextBundled = T("Admin.Catalog.Products.ProductType.BundledProduct.Label").Text; + var sku = T("Products.Sku").Text; + + var fields = new List { "name" }; + if (_searchSettings.SearchFields.Contains("sku")) + fields.Add("sku"); + if (_searchSettings.SearchFields.Contains("shortdescription")) + fields.Add("shortdescription"); + + var searchQuery = new CatalogSearchQuery(fields.ToArray(), model.SearchTerm) + .HasStoreId(model.StoreId); + + if (!hasPermission) + { + searchQuery = searchQuery.VisibleOnly(Services.WorkContext.CurrentCustomer); + } + + if (model.ProductTypeId > 0) + { + searchQuery = searchQuery.IsProductType((ProductType)model.ProductTypeId); + } + + if (model.ManufacturerId != 0) + { + searchQuery = searchQuery.WithManufacturerIds(null, model.ManufacturerId); + } + + if (model.CategoryId != 0) + { + var node = _categoryService.GetCategoryTree(model.CategoryId, true); + if (node != null) + { + searchQuery = searchQuery.WithCategoryIds(null, node.Flatten(true).Select(x => x.Id).ToArray()); + } + } var skip = model.PageIndex * model.PageSize; var query = _catalogSearchService.PrepareQuery(searchQuery); - var products = query - .Select(x => new - { - x.Id, - x.Sku, - x.Name, - x.Published, - x.ProductTypeId, - x.MainPictureId - }) - .OrderBy(x => x.Name) - .Skip(() => skip) - .Take(() => model.PageSize) - .ToList(); - - var allPictureIds = products.Select(x => x.MainPictureId.GetValueOrDefault()); - var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); - var slugs = model.ReturnField.IsCaseInsensitiveEqual("link") + var products = query + .Select(x => new + { + x.Id, + x.Sku, + x.Name, + x.Published, + x.ProductTypeId, + x.MainPictureId + }) + .OrderBy(x => x.Name) + .Skip(() => skip) + .Take(() => model.PageSize) + .ToList(); + + var allPictureIds = products.Select(x => x.MainPictureId.GetValueOrDefault()); + var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); + var slugs = returnLink ? _urlRecordService.GetUrlRecordCollection(nameof(Product), null, products.Select(x => x.Id).ToArray()) : null; model.SearchResult = products - .Select(x => + .Select(x => { var item = new EntityPickerModel.SearchResultModel { @@ -184,80 +188,94 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) Title = x.Name, Summary = x.Sku, SummaryTitle = "{0}: {1}".FormatInvariant(sku, x.Sku.NaIfEmpty()), - Published = hasPermission ? x.Published : (bool?)null, - Selected = selIds.Contains(x.Id) + Published = hasPermission ? x.Published : (bool?)null }; - item.ReturnValue = slugs != null - ? Url.RouteUrl("Product", new { SeName = slugs.GetSlug(language.Id, x.Id) }) - : (model.ReturnField.IsCaseInsensitiveEqual("sku") ? x.Sku : x.Id.ToString()); - - if (disableIfNotSimpleProduct) - { - item.Disable = x.ProductTypeId != (int)ProductType.SimpleProduct; - } - else if (disableIfGroupedProduct) - { - item.Disable = x.ProductTypeId == (int)ProductType.GroupedProduct; - } - - if (!item.Disable && disableIds.Contains(x.Id)) - { - item.Disable = true; - } - - if (x.ProductTypeId == (int)ProductType.GroupedProduct) - { - item.LabelText = labelTextGrouped; - item.LabelClassName = "badge-success"; - } - else if (x.ProductTypeId == (int)ProductType.BundledProduct) - { - item.LabelText = labelTextBundled; - item.LabelClassName = "badge-info"; - } - - var pictureInfo = allPictureInfos.Get(x.MainPictureId.GetValueOrDefault()); - var fallbackType = _catalogSettings.HideProductDefaultPictures ? FallbackPictureType.NoFallback : FallbackPictureType.Entity; - - item.ImageUrl = _pictureService.GetUrl( - allPictureInfos.Get(x.MainPictureId.GetValueOrDefault()), - _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage, - fallbackType); - - return item; - }) - .ToList(); - } + if (returnLink) + { + item.ReturnValue = Url.RouteUrl("Product", new { SeName = slugs.GetSlug(languageId, x.Id) }); + } + else if (returnSku) + { + item.ReturnValue = x.Sku; + } + else + { + item.ReturnValue = x.Id.ToString(); + } + + item.Selected = selected.Contains(item.ReturnValue); + + if (disableIfNotSimpleProduct) + { + item.Disable = x.ProductTypeId != (int)ProductType.SimpleProduct; + } + else if (disableIfGroupedProduct) + { + item.Disable = x.ProductTypeId == (int)ProductType.GroupedProduct; + } + + if (!item.Disable && disableIds.Contains(x.Id)) + { + item.Disable = true; + } + + if (x.ProductTypeId == (int)ProductType.GroupedProduct) + { + item.LabelText = labelTextGrouped; + item.LabelClassName = "badge-success"; + } + else if (x.ProductTypeId == (int)ProductType.BundledProduct) + { + item.LabelText = labelTextBundled; + item.LabelClassName = "badge-info"; + } + + var pictureInfo = allPictureInfos.Get(x.MainPictureId.GetValueOrDefault()); + var fallbackType = _catalogSettings.HideProductDefaultPictures ? FallbackPictureType.NoFallback : FallbackPictureType.Entity; + + item.ImageUrl = _pictureService.GetUrl( + allPictureInfos.Get(x.MainPictureId.GetValueOrDefault()), + _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage, + fallbackType); + + return item; + }) + .ToList(); + } else if (model.EntityType.IsCaseInsensitiveEqual("category")) - { + { var categories = _categoryService.GetAllCategories(model.SearchTerm, showHidden: true); var allPictureIds = categories.Select(x => x.PictureId.GetValueOrDefault()); var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); - var slugs = model.ReturnField.IsCaseInsensitiveEqual("link") + var slugs = returnLink ? _urlRecordService.GetUrlRecordCollection(nameof(Category), null, categories.Select(x => x.Id).ToArray()) : null; model.SearchResult = categories .Select(x => { + var path = ((ICategoryNode)x).GetCategoryPath(_categoryService, languageId, "({0})"); + var returnValue = returnLink + ? Url.RouteUrl("Category", new { SeName = slugs.GetSlug(languageId, x.Id) }) + : x.Id.ToString(); + var item = new EntityPickerModel.SearchResultModel { Id = x.Id, Title = x.Name, - Summary = x.Description.Truncate(120, "…"), - SummaryTitle = x.Name, + Summary = path, + SummaryTitle = path, Published = x.Published, - Selected = selIds.Contains(x.Id) + ReturnValue = returnValue, + Selected = selected.Contains(returnValue), + Disable = disableIds.Contains(x.Id) }; - item.ReturnValue = slugs != null - ? Url.RouteUrl("Category", new { SeName = slugs.GetSlug(language.Id, x.Id) }) - : x.Id.ToString(); - - if (!item.Disable && disableIds.Contains(x.Id)) + if (x.Alias.HasValue()) { - item.Disable = true; + item.LabelText = x.Alias; + item.LabelClassName = "badge-secondary"; } var pictureInfo = allPictureInfos.Get(x.PictureId.GetValueOrDefault()); @@ -272,37 +290,32 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) }) .ToList(); } - else if (model.EntityType.IsCaseInsensitiveEqual("manufacturer")) + else if (model.EntityType.IsCaseInsensitiveEqual("manufacturer")) { var manufacturers = _manufacturerService.GetAllManufacturers(model.SearchTerm, model.PageIndex, model.PageSize, showHidden: true); var allPictureIds = manufacturers.Select(x => x.PictureId.GetValueOrDefault()); var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); - var slugs = model.ReturnField.IsCaseInsensitiveEqual("link") + var slugs = returnLink ? _urlRecordService.GetUrlRecordCollection(nameof(Manufacturer), null, manufacturers.Select(x => x.Id).ToArray()) : null; model.SearchResult = manufacturers .Select(x => { + var returnValue = returnLink + ? Url.RouteUrl("Manufacturer", new { SeName = slugs.GetSlug(languageId, x.Id) }) + : x.Id.ToString(); + var item = new EntityPickerModel.SearchResultModel { Id = x.Id, - ReturnValue = x.Id.ToString(), Title = x.Name, - SummaryTitle = x.Name, Published = x.Published, - Selected = selIds.Contains(x.Id) + ReturnValue = returnValue, + Selected = selected.Contains(returnValue), + Disable = disableIds.Contains(x.Id) }; - item.ReturnValue = slugs != null - ? Url.RouteUrl("Manufacturer", new { SeName = slugs.GetSlug(language.Id, x.Id) }) - : x.Id.ToString(); - - if (!item.Disable && disableIds.Contains(x.Id)) - { - item.Disable = true; - } - var pictureInfo = allPictureInfos.Get(x.PictureId.GetValueOrDefault()); var fallbackType = _catalogSettings.HideProductDefaultPictures ? FallbackPictureType.NoFallback : FallbackPictureType.Entity; @@ -318,14 +331,13 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) else if (model.EntityType.IsCaseInsensitiveEqual("customer")) { var registeredRoleId = _customerService.GetCustomerRoleBySystemName("Registered").Id; - var searchTermName = string.Empty; var searchTermEmail = string.Empty; var searchTermCustomerNumber = string.Empty; if (model.CustomerSearchType.IsCaseInsensitiveEqual("Name")) searchTermName = model.SearchTerm; - else if(model.CustomerSearchType.IsCaseInsensitiveEqual("Email")) + else if (model.CustomerSearchType.IsCaseInsensitiveEqual("Email")) searchTermEmail = model.SearchTerm; else if (model.CustomerSearchType.IsCaseInsensitiveEqual("CustomerNumber")) searchTermCustomerNumber = model.SearchTerm; @@ -341,26 +353,54 @@ public ActionResult Picker(EntityPickerModel model, FormCollection form) }; var customers = _customerService.SearchCustomers(q); - + model.SearchResult = customers .Select(x => { + var fullName = x.GetFullName(); + var item = new EntityPickerModel.SearchResultModel { Id = x.Id, ReturnValue = x.Id.ToString(), - Title = x.Username, - Summary = x.GetFullName(), - SummaryTitle = x.GetFullName(), + Title = x.Username.NullEmpty() ?? x.Email, + Summary = fullName, + SummaryTitle = fullName, Published = true, - Selected = selIds.Contains(x.Id) + Selected = selected.Contains(x.Id.ToString()), + Disable = disableIds.Contains(x.Id) }; - if (!item.Disable && disableIds.Contains(x.Id)) + return item; + }) + .ToList(); + } + else if (model.EntityType.IsCaseInsensitiveEqual("topic")) + { + var topics = _topicService.GetAllTopics(0, model.PageIndex, model.PageSize, true); + var slugs = returnLink + ? _urlRecordService.GetUrlRecordCollection(nameof(Topic), null, topics.Select(x => x.Id).ToArray()) + : null; + + model.SearchResult = topics + .Select(x => + { + var returnValue = returnLink + ? Url.RouteUrl("Topic", new { SeName = slugs.GetSlug(languageId, x.Id) }) + : x.Id.ToString(); + + var item = new EntityPickerModel.SearchResultModel { - item.Disable = true; - } - + Id = x.Id, + Published = x.IsPublished, + Title = x.Title.NullEmpty() ?? x.SystemName, + Summary = x.SystemName, + SummaryTitle = x.SystemName, + ReturnValue = returnValue, + Selected = selected.Contains(returnValue), + Disable = disableIds.Contains(x.Id) + }; + return item; }) .ToList(); diff --git a/src/Presentation/SmartStore.Web/Models/Entity/EntityPickerModel.cs b/src/Presentation/SmartStore.Web/Models/Entity/EntityPickerModel.cs index 643c0128ec..eb6a248e31 100644 --- a/src/Presentation/SmartStore.Web/Models/Entity/EntityPickerModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Entity/EntityPickerModel.cs @@ -6,20 +6,26 @@ namespace SmartStore.Web.Models.Entity { public class EntityPickerModel : ModelBase { - public string EntityType { get; set; } + public EntityPickerModel() + { + PageSize = 96; + } + + public string EntityType { get; set; } public bool HighligtSearchTerm { get; set; } public string DisableIf { get; set; } public string DisableIds { get; set; } public string SearchTerm { get; set; } public string ReturnField { get; set; } public int MaxItems { get; set; } - public string PreselectedEntityIds { get; set; } + public string Selected { get; set; } public int PageIndex { get; set; } public int PageSize { get; set; } + public int LanguageId { get; set; } - public List SearchResult { get; set; } + public List SearchResult { get; set; } - #region SearchProperties + #region Search properties [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] public string ProductName { get; set; } diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js index 856834fc66..e775021c03 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js @@ -76,7 +76,7 @@ returnField: 'id', delim: ',', targetInput: null, - preselectedIds: null, + selected: null, appendMode: true, maxItems: 0, onDialogLoading: null, @@ -84,7 +84,10 @@ onSelectionCompleted: null }; - options = $.extend({}, defaults, options); + // Use self.attr, not self.data! + options = $.extend({}, defaults, options); + options.entityType = self.attr('data-entitytype') || options.entityType; + options.caption = self.attr('data-caption') || options.caption; if (_.isEmpty(options.url)) { options.url = self.attr('data-url'); @@ -98,20 +101,22 @@ options.targetInput = $(self.data('target')).first(); } - if (options.targetInput && !_.isArray(options.preselectedIds)) { - var val = $(options.targetInput).val(); + if (options.targetInput && !_.isArray(options.selected)) { + var val = $(options.targetInput).val(); if (val.length > 0) { - options.preselectedIds = _.map(val.split(options.delim), function (x) { + options.selected = _.map(val.split(options.delim), function (x) { var result = x.trim(); - if (options.returnField.toLowerCase() === 'id') result = toInt(result); + if (options.returnField.toLowerCase() === 'id') { + result = toInt(result); + } return result; - }); + }); } } - if (!_.isArray(options.preselectedIds)) { - options.preselectedIds = []; - } + if (!_.isArray(options.selected)) { + options.selected = []; + } return options; } @@ -132,7 +137,7 @@ } function loadDialog(context /* button */, opt) { - var btn = $(context); + var btn = $(context); var dialog = $('#entpicker-' + opt.entityType + '-dialog'); function showAndFocusDialog() { @@ -151,16 +156,16 @@ if (dialog.length) { showAndFocusDialog(); } - else { + else { $.ajax({ cache: false, type: 'GET', data: { - "EntityType": opt.entityType, + "EntityType": opt.entityType, "HighligtSearchTerm": opt.highligtSearchTerm, "ReturnField": opt.returnField, "MaxItems": opt.maxItems, - "PreselectedEntityIds": opt.preselectedIds.join(), + "Selected": opt.selected.join(), "DisableIf": opt.disableIf, "DisableIds": opt.disableIds }, @@ -238,8 +243,8 @@ // lazy loading dialog.find('.modal-body').on('scroll', function (e) { - if ($('.load-more:not(.loading)').visible(true, false, 'vertical')) { - fillList(this, { append: true }); + if (dialog.find('.load-more:not(.loading)').visible(true, false, 'vertical')) { + fillList(this, { append: true }); } }); @@ -298,25 +303,27 @@ }; }); - var selectedIds = _.uniq(_.map(selectedItems, function (x) { + var selectedValues = _.uniq(_.map(selectedItems, function (x) { var result = x.id; - if (opts.returnField.toLowerCase() === 'id' && !_.isNumber(result)) result = toInt(result); + if (opts.returnField.toLowerCase() === 'id' && !_.isNumber(result)) { + result = toInt(result); + } return result; })); - if (opts.appendMode && _.isArray(opts.preselectedIds)) { - selectedIds = _.union(opts.preselectedIds, selectedIds); + if (opts.appendMode && _.isArray(opts.selected)) { + selectedValues = _.union(opts.selected, selectedValues); } if (opts.targetInput) { $(opts.targetInput) - .val(selectedIds.join(opts.delim)) + .val(selectedValues.join(opts.delim)) .focus() .blur(); } if (_.isFunction(opts.onSelectionCompleted)) { - if (opts.onSelectionCompleted(selectedIds, selectedItems, dialog)) { + if (opts.onSelectionCompleted(selectedValues, selectedItems, dialog)) { dialog.modal('hide'); } } @@ -340,7 +347,7 @@ var pageElement = dialog.find('input[name=PageIndex]'), pageIndex = parseInt(pageElement.val()); - pageElement.val(pageIndex + 1); + pageElement.val(pageIndex + 1); } else { dialog.find('input[name=PageIndex]').val('0'); @@ -370,7 +377,6 @@ list.stop().append(response); if (!opt.append) { - //dialog.find('.entpicker-filter').slideUp(200); showStatus(dialog); } diff --git a/src/Presentation/SmartStore.Web/Views/Entity/Partials/Picker.List.cshtml b/src/Presentation/SmartStore.Web/Views/Entity/Partials/Picker.List.cshtml index a56418423e..d598add3d4 100644 --- a/src/Presentation/SmartStore.Web/Views/Entity/Partials/Picker.List.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Entity/Partials/Picker.List.cshtml @@ -1,6 +1,5 @@ -@model EntityPickerModel - -@using SmartStore.Web.Models.Entity; +@using SmartStore.Web.Models.Entity; +@model EntityPickerModel @helper HighlightSearchTermInTitle(EntityPickerModel.SearchResultModel item) { @@ -18,16 +17,16 @@ {
    -
    -
    - @if (item.ImageUrl.HasValue()) - { -
    - -
    - } -
    -
    + @if (item.ImageUrl.HasValue()) + { +
    +
    +
    + +
    +
    +
    + }
    @@ -43,13 +42,13 @@ } @item.Summary -
    - +
    } +